Является ли это хорошим подходом для иерархии классов на основе 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 я не понимаю. Открытый базовый класс использует идиому 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

Единственное улучшение, которое я могу увидеть здесь, - позволить конкретным классам определять поле реализации. Если это необходимо абстрактным базовым классам, они могут определить абстрактное свойство, которое легко реализовать в конкретных классах:

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в середине дерева.

wigy
источник