이것이 C ++의 "pImpl"기반 클래스 계층에 대한 좋은 접근 방법입니까?


9

인터페이스와 구현을 분리하려는 클래스 계층이 있습니다. 내 솔루션은 인터페이스에 대한 핸들 클래스 계층 구조와 구현을위한 비공개 클래스 계층 구조라는 두 가지 계층 구조를 갖는 것입니다. 기본 핸들 클래스에는 파생 핸들 클래스가 파생 유형의 포인터로 캐스트하는 구현에 대한 포인터가 있습니다 (함수 참조 getPimpl()).

다음은 파생 클래스가 두 개인 기본 클래스에 대한 솔루션 스케치입니다. 더 나은 해결책이 있습니까?

파일 "Base.h":

#include <memory>

class Base {
protected:
    class Impl;
    std::shared_ptr<Impl> pImpl;
    Base(Impl* pImpl) : pImpl{pImpl} {};
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl;
    inline Derived_1* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

class Derived_2 final : public Base {
protected:
    class Impl;
    inline Derived_2* getPimpl() const noexcept {
        return reinterpret_cast<Impl*>(pImpl.get());
    }
public:
    Derived_2(...);
    void func_2(...) const;
    ...
};

파일 "Base.cpp":

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

class Derived_2::Impl final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_2(...) {...}
    ...
};

Derived_1::Derived_1(...) : Base(new Derived_1::Impl(...)) {...}
Derived_1::func_1(...) const { getPimpl()->func_1(...); }

Derived_2::Derived_2(...) : Base(new Derived_2::Impl(...)) {...}
Derived_2::func_2(...) const { getPimpl()->func_2(...); }

이러한 클래스 중 라이브러리 / 컴포넌트 외부에서 볼 수있는 클래스는 무엇입니까? 이면 Base, 일반적인 추상 기본 클래스 ( "인터페이스")와 pimpl이없는 구체적인 구현으로 충분할 수 있습니다.
D. Jurcau

@ D.Jurcau 기본 클래스와 파생 클래스가 모두 공개됩니다. 분명히 구현 클래스는 그렇지 않습니다.
Steve Emmerson

왜 다운 캐스트인가? 기본 클래스는 여기에서 이상한 위치에 있으며 향상된 형식 안전성과 적은 코드로 공유 포인터로 바꿀 수 있습니다.
Basilevs

@Basilevs 이해가되지 않습니다. 퍼블릭베이스 클래스는 pimpl 관용구를 사용하여 구현을 숨 깁니다. 공유 포인터로 바꾸면 포인터를 캐스트하거나 복제하지 않고 클래스 계층 구조를 유지하는 방법을 알 수 없습니다. 코드 예제를 제공 할 수 있습니까?
Steve Emmerson

다운 캐스트를 복제하는 대신 포인터를 복제 할 것을 제안합니다.
Basilevs

답변:


1

나는 그것을 Derived_1::Impl이끌어 내는 것이 좋지 않은 전략이라고 생각합니다 Base::Impl.

Pimpl 관용구의 주요 목적은 클래스의 구현 세부 사항을 숨기는 것입니다. Derived_1::Impl에서 파생 되도록함으로써 Base::Impl해당 목표를 달성했습니다. 이제의 구현은에 Base의존 할뿐만 아니라 Base::Impl의 구현 Derived_1도에 의존합니다 Base::Impl.

더 나은 해결책이 있습니까?

이는 귀하에게 어떤 트레이드 오프가 허용되는지에 달려 있습니다.

해결책 1

Impl수업을 완전히 독립적으로 만듭니다 . 이것은 Impl클래스에 대한 두 개의 포인터가 있음을 의미합니다 .Base 와 다른 하나에Derived_N .

class Base {

   protected:
      Base() : pImpl{new Impl()} {}

   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;

};

class Derived_1 final : public Base {
   public:
      Derived_1() : Base(), pImpl{new Impl()} {}
      void func_1() const;
   private:
      // It's own Impl class and pointer.
      class Impl { };
      std::shared_ptr<Impl> pImpl;
};

해결책 2

클래스를 핸들로만 노출하십시오. 클래스 정의와 구현을 전혀 공개하지 마십시오.

공개 헤더 파일 :

struct Handle {unsigned long id;};
struct Derived1_tag {};
struct Derived2_tag {};

Handle constructObject(Derived1_tag tag);
Handle constructObject(Derived2_tag tag);

void deleteObject(Handle h);

void fun(Handle h, Derived1_tag tag);
void bar(Handle h, Derived2_tag tag); 

빠른 구현은 다음과 같습니다

#include <map>

class Base
{
   public:
      virtual ~Base() {}
};

class Derived1 : public Base
{
};

class Derived2 : public Base
{
};

namespace Base_Impl
{
   struct CompareHandle
   {
      bool operator()(Handle h1, Handle h2) const
      {
         return (h1.id < h2.id);
      }
   };

   using ObjectMap = std::map<Handle, Base*, CompareHandle>;

   ObjectMap& getObjectMap()
   {
      static ObjectMap theMap;
      return theMap;
   }

   unsigned long getNextID()
   {
      static unsigned id = 0;
      return ++id;
   }

   Handle getHandle(Base* obj)
   {
      auto id = getNextID();
      Handle h{id};
      getObjectMap()[h] = obj;
      return h;
   }

   Base* getObject(Handle h)
   {
      return getObjectMap()[h];
   }

   template <typename Der>
      Der* getObject(Handle h)
      {
         return dynamic_cast<Der*>(getObject(h));
      }
};

using namespace Base_Impl;

Handle constructObject(Derived1_tag tag)
{
   // Construct an object of type Derived1
   Derived1* obj = new Derived1;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

Handle constructObject(Derived2_tag tag)
{
   // Construct an object of type Derived2
   Derived2* obj = new Derived2;

   // Get a handle to the object and return it.
   return getHandle(obj);
}

void deleteObject(Handle h)
{
   // Get a pointer to Base given the Handle.
   //
   Base* obj = getObject(h);

   // Remove it from the map.
   // Delete the object.
   if ( obj != nullptr )
   {
      getObjectMap().erase(h);
      delete obj;
   }
}

void fun(Handle h, Derived1_tag tag)
{
   // Get a pointer to Derived1 given the Handle.
   Derived1* obj = getObject<Derived1>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

void bar(Handle h, Derived2_tag tag)
{
   Derived2* obj = getObject<Derived2>(h);
   if ( obj == nullptr )
   {
      // Problem.
      // Decide how to deal with it.

      return;
   }

   // Use obj
}

장점과 단점

첫 번째 방법으로 구성 할 수 있습니다 Derived 스택에 클래스를 . 두 번째 방법으로는 옵션이 아닙니다.

첫 번째 접근 방식에서는 두 가지 동적 할당 및 할당 취소 비용이 발생합니다. Derived 스택 . Derived힙에서 오브젝트 를 구성하고 파괴하는 경우 하나 이상의 할당 및 할당 취소 비용이 발생합니다. 두 번째 방법에서는 모든 객체에 대해 하나의 동적 할당과 하나의 할당 해제 비용 만 발생합니다.

첫 번째 방법으로 virtual멤버 함수 를 사용할 수 있습니다 Base. 두 번째 방법으로는 옵션이 아닙니다.

나의 제안

조금 더 비싸더라도 클래스 계층 구조 및 virtual멤버 함수를 사용할 수 있도록 첫 번째 솔루션을 사용합니다 Base.


0

여기서 볼 수있는 유일한 개선 사항은 구체적 클래스가 구현 필드를 정의하도록하는 것입니다. 추상 기본 클래스에 필요한 경우 구체적인 클래스에서 구현하기 쉬운 추상 속성을 정의 할 수 있습니다.

Base.h

class Base {
protected:
    class Impl;
    virtual std::shared_ptr<Impl> getImpl() =0;
    ...
};

class Derived_1 final : public Base {
protected:
    class Impl1;
    std::shared_ptr<Impl1> pImpl
    virtual std::shared_ptr<Base::Impl> getImpl();
public:
    Derived_1(...);
    void func_1(...) const;
    ...
};

Base.cpp

class Base::Impl {
public:
    Impl(...) {...}
    ...
};

class Derived_1::Impl1 final : public Base::Impl {
public:
    Impl(...) : Base::Impl(...) {...}
    void func_1(...) {...}
    ...
};

std::shared_ptr<Base::Impl> Derived_1::getImpl() { return pPimpl; }
Derived_1::Derived_1(...) : pPimpl(std::make_shared<Impl1>(...)) {...}
void Derived_1::func_1(...) const { pPimpl->func_1(...); }

이것은 나에게 더 안전한 것 같습니다. 큰 나무가 있다면 나무 virtual std::shared_ptr<Impl1> getImpl1() =0중간에 소개 할 ​​수도 있습니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.