Це хороший підхід для ієрархії класів на основі "pImpl" в C ++?


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 Базові та похідні класи будуть загальнодоступними. Очевидно, заняття з реалізації не будуть.
Стів Еммерсон

Чому вниз? Базовий клас тут знаходиться в дивному положенні, його можна замінити спільним покажчиком з поліпшеним типом безпеки та меншим кодом.
Basilevs

@Basilevs я не розумію. Загальнодоступний базовий клас використовує ідіому pimpl, щоб приховати реалізацію. Я не бачу, як заміна його спільним покажчиком може підтримувати ієрархію класів без викидання чи дублювання покажчика. Чи можете ви навести приклад коду?
Стів Еммерсон

Я пропоную дублювати покажчик, замість того, щоб реплікувати downcast.
Василевс

Відповіді:


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

Єдине вдосконалення, яке я бачу тут, - це дати конкретним класам визначення області реалізації. Якщо потрібні абстрактні базові класи, вони можуть визначити абстрактну властивість, яку легко реалізувати в конкретних класах:

База.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посередині дерева.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.