Czy to dobre podejście do hierarchii klas opartej na „pImpl” w C ++?

9

Mam hierarchię klas, dla której chciałbym oddzielić interfejs od implementacji. Moim rozwiązaniem jest posiadanie dwóch hierarchii: hierarchii klas uchwytów dla interfejsu i niepublicznej hierarchii klas dla implementacji. Podstawowa klasa uchwytu ma wskaźnik do implementacji, który pochodne klasy uchwytów rzutują na wskaźnik typu pochodnego (patrz funkcja getPimpl()).

Oto szkic mojego rozwiązania dla klasy bazowej z dwiema klasami pochodnymi. Czy jest lepsze rozwiązanie?

Plik „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;
    ...
};

Plik „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(...); }
Steve Emmerson
źródło
Która z tych klas będzie widoczna z zewnątrz biblioteki / komponentu? Jeśli tylko Base, wystarczająca może być normalna abstrakcyjna klasa bazowa („interfejs”) i konkretne implementacje bez pimpl.
D. Jurcau,
@ D.Jurcau Wszystkie podstawowe i pochodne klasy będą publicznie widoczne. Oczywiście klasy implementacji tego nie zrobią.
Steve Emmerson
Dlaczego przygnębiony? Klasa podstawowa znajduje się tutaj w dziwnej pozycji, można ją zastąpić wspólnym wskaźnikiem z poprawionym bezpieczeństwem typu i mniejszym kodem.
Basilevs,
@Basilevs Nie rozumiem. Publiczna klasa bazowa używa idiomu pimpl, aby ukryć implementację. Nie widzę, jak zastąpienie go wspólnym wskaźnikiem może utrzymać hierarchię klas bez rzutowania lub powielania wskaźnika. Czy możesz podać przykład kodu?
Steve Emmerson
Proponuję zduplikować wskaźnik zamiast replikacji downcastu.
Basilevs,

Odpowiedzi:

1

Myślę, że Derived_1::Implczerpanie z tego jest kiepską strategią Base::Impl.

Głównym celem użycia idiomu Pimpl jest ukrycie szczegółów implementacyjnych klasy. Pozwalając Derived_1::Implpochodzić Base::Impl, pokonałeś ten cel. Teraz nie tylko Basezależy od Base::Implwdrożenia, ale Derived_1także od wdrożenia Base::Impl.

Czy jest lepsze rozwiązanie?

To zależy od tego, jakie kompromisy są dla Ciebie dopuszczalne.

Rozwiązanie 1

Uczyń Implzajęcia całkowicie niezależnymi. Oznacza to, że będą dwa wskaźniki do Implklas - jeden w, Basea drugi w 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;
};

Rozwiązanie 2

Ujawnij klasy tylko jako uchwyty. W ogóle nie ujawniaj definicji klas i implementacji.

Plik nagłówka publicznego:

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); 

Oto szybka implementacja

#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
}

Plusy i minusy

Przy pierwszym podejściu możesz konstruować Derivedklasy na stosie. Przy drugim podejściu nie jest to opcja.

Przy pierwszym podejściu ponosisz koszt dwóch dynamicznych alokacji i dezalokacji na budowę i zniszczenie Derivedstosu. Jeśli konstruujesz i niszczysz Derivedobiekt ze stosu, ponosisz koszty jeszcze jednego przydziału i zwolnienia. Przy drugim podejściu ponosisz tylko koszt jednej dynamicznej alokacji i jednej dezalokacji dla każdego obiektu.

Przy pierwszym podejściu masz możliwość korzystania z virtualfunkcji członka Base. Przy drugim podejściu nie jest to opcja.

Moja sugestia

Wybrałbym pierwsze rozwiązanie, aby móc korzystać z hierarchii klas i virtualfunkcji Baseskładowych, nawet jeśli jest to nieco droższe.

R Sahu
źródło
0

Jedyne ulepszenie, które widzę tutaj, to pozwolenie konkretnym klasom zdefiniować pole implementacji. Jeśli abstrakcyjne klasy podstawowe tego potrzebują, mogą zdefiniować właściwość abstrakcyjną, którą można łatwo zaimplementować w konkretnych klasach:

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(...); }

Wydaje mi się to bezpieczniejsze. Jeśli masz duże drzewo, możesz również przedstawić je virtual std::shared_ptr<Impl1> getImpl1() =0na środku drzewa.

wigy
źródło