Идиома Pimpl с наследованием

Я хочу использовать идиому pimpl с наследованием.

Вот базовый публичный класс и его класс реализации:

class A
{
    public:
      A(){pAImpl = new AImpl;};
      void foo(){pAImpl->foo();};
    private:
      AImpl* pAImpl;  
};
class AImpl
{
    public:
      void foo(){/*do something*/};
};

И я хочу иметь возможность создать производный публичный класс с его классом реализации:

class B : public A
{
    public:
      void bar(){pAImpl->bar();};    // Can't do! pAimpl is A private.
};        

class BImpl : public AImpl
{
    public:
      void bar(){/*do something else*/};
};

Но я не могу использовать pAimpl в B, потому что это A private.

Итак, я вижу некоторые способы его решения:

  • Создайте член BImpl * pBImpl в B и передайте его A с дополнительным конструктором A, A (AImpl *).
  • Изменить pAImpl для защиты (или добавить функцию Get) и использовать его в B.
  • B не должен наследовать от A. Создайте член BImpl * pBImpl в B и создайте foo() и bar() в B, который будет использовать pBImpl.
  • Любой другой способ?

Что мне выбрать?

Ответ 1

class A
{
    public:
      A(bool DoNew = true){
        if(DoNew)
          pAImpl = new AImpl;
      };
      void foo(){pAImpl->foo();};
    protected:
      void SetpAImpl(AImpl* pImpl) {pAImpl = pImpl;};
    private:
      AImpl* pAImpl;  
};
class AImpl
{
    public:
      void foo(){/*do something*/};
};

class B : public A
{
    public:
      B() : A(false){
          pBImpl = new BImpl;
          SetpAImpl(pBImpl);
      };
      void bar(){pBImpl->bar();};    
    private:
      BImpl* pBImpl;  
};        

class BImpl : public AImpl
{
    public:
      void bar(){/*do something else*/};
};

Ответ 2

Я думаю, что лучший путь от чисто объектно-теоретической перспективы - не превращать BImpl в наследование из AImpl (это то, что вы имели в виду в варианте 3?). Однако, если BImpl выводится из AImpl (и передавая желаемый имп для конструктора A), тоже ОК, при условии, что переменная элемента pimpl имеет значение const. На самом деле не имеет значения, используете ли вы функции get или напрямую получаете доступ к переменной из производных классов, если вы не хотите применять const-correctness на производных классах. Предоставление производным классам изменение pimpl не является хорошей идеей - они могут разрушить всю инициализацию A, - и также не позволяют базовому классу изменить это. Рассмотрим это расширение для вашего примера:

class A
{
protected:
   struct AImpl {void foo(); /*...*/};
   A(AImpl * impl): pimpl(impl) {}
   AImpl * GetImpl() { return pimpl; }
   const AImpl * GetImpl() const { return pimpl; }
private:
   AImpl * pimpl;
public:
   void foo() {pImpl->foo();}


   friend void swap(A&, A&);
};

void swap(A & a1, A & a2)
{
   using std::swap;
   swap(a1.pimpl, a2.pimpl);
}

class B: public A
{
protected:
   struct BImpl: public AImpl {void bar();};
public:
   void bar(){static_cast<BImpl *>(GetImpl())->bar();}
   B(): A(new BImpl()) {}

};

class C: public A
{
protected:
   struct CImpl: public AImpl {void baz();};
public:
   void baz(){static_cast<CImpl *>(GetImpl())->baz();}
   C(): A(new CImpl()) {}
};

int main()
{
   B b;
   C c;
   swap(b, c); //calls swap(A&, A&)
   //This is now a bad situation - B.pimpl is a CImpl *, and C.pimpl is a BImpl *!
   //Consider:
   b.bar(); 
   //If BImpl and CImpl weren't derived from AImpl, then this wouldn't happen.
   //You could have b BImpl being out of sync with its AImpl, though.
}

Хотя у вас может не быть функции swap(), вы можете легко представить себе похожие проблемы, особенно если A назначается, случайно или намеренно. Это несколько незначительное нарушение принципа замены Лискова. Решения должны либо:

  • Не меняйте элементы pimpl после сборки. Объявите их AImpl * const pimpl. Затем производные конструкторы могут передавать соответствующий тип, а остальная часть производного класса может свести на нет уверенность. Однако, тогда вы не можете, например. делать неметающие свопы, назначения или копирование на запись, потому что эти методы требуют, чтобы вы могли изменить член pimpl. Однако, однако, вы, вероятно, не намереваетесь делать это, если у вас есть иерархия наследования.

  • Имеют не связанные (и немые) классы AImpl и BImpl для частных переменных A и B, соответственно. Если B хочет что-то сделать с A, используйте открытый или защищенный интерфейс. Это также сохраняет наиболее распространенную причину использования pimpl: возможность скрыть определение AImpl в файле cpp, который не могут использовать производные классы, поэтому половина вашей программы не нуждается в перекомпиляции при изменении реализации.

Ответ 3

Как сказал stefan.ciobaca, если вы действительно хотите, чтобы A был расширяемым, вы бы хотели, чтобы pAImpl был защищен.

Однако ваше определение в B of void bar(){pAImpl->bar();}; кажется нечетным, так как bar - это метод на BImpl, а не AImpl.

Существует не менее трех простых альтернатив, которые могли бы избежать этой проблемы:

  • Ваша альтернатива (3).
  • Вариант (3), в котором BImpl расширяет AImpl (наследуя существующую реализацию foo вместо определения другого), BImpl определяет bar, а B использует свой частный BImpl* pBImpl для доступа к обоим.
  • Делегирование, в котором B содержит частные указатели для каждого из AImpl и BImpl и пересылает каждый из foo и bar соответствующему исполнителю.

Ответ 4

Я бы сделал (1), потому что A privates - это или нет для B.

На самом деле я бы не передал его A, как вы предлагаете, потому что A делает свой собственный в A:: A(). Вызов pApimpl->whatever() из Bis также не подходит (частный означает закрытый).

Ответ 5

Правильный способ - сделать (2).

В общем, вы, вероятно, должны подумать о том, чтобы все переменные-члены, защищенные по умолчанию, вместо частного.

Причина, по которой большинство программистов выбирает частный, заключается в том, что они не думают о других, которые хотят извлечь из своего класса, и большинство вводных руководств на С++ обучают этому стилю, в том смысле, что все примеры используют private.

ИЗМЕНИТЬ

Дублирование кода и распределение памяти являются нежелательными побочными эффектами использования шаблона дизайна сутенера, и его нельзя избежать, насколько мне известно.

Если вам нужно, чтобы Bimpl наследовал Aimpl, и вы хотите открыть для них согласованный интерфейс через A и B, B также потребуется наследовать A.

Одна вещь, которую вы можете сделать, чтобы упростить ситуацию в этом сценарии, - это наследование B от A и только изменение конструктора таким образом, чтобы B:: B (...) {} создавал Bimpl и добавлял рассылки для всех методов Bimpl, которые не находятся в Aimpl.