Пимпл идиома с наследством - PullRequest
16 голосов
/ 29 января 2009

Я хочу использовать идиому 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's private.
};        

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

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

Итак, я вижу несколько способов решить эту проблему:

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

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

Ответы [ 5 ]

7 голосов
/ 30 января 2009
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 голосов
/ 29 января 2009

Я думаю, что лучший способ с чисто объектно-теоретической точки зрения - не делать BImpl наследуемым от AImpl (это то, что вы имели в виду в варианте 3?). Тем не менее, наличие BImpl, производного от AImpl (и передача требуемого значения impl в конструктор A) также допустимо при условии, что переменная-член pimpl равна const. На самом деле не имеет значения, используете ли вы функции get или напрямую обращаетесь к переменной из производных классов, если только вы не хотите принудительно установить const-корректность в производных классах. Позволить производным классам изменять 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's BImpl being out of sync with its AImpl, though.
}

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

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

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

1 голос
/ 29 января 2009

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

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

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

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

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

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

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

EDIT

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

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

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

0 голосов
/ 29 января 2009

Я бы сделал (1), потому что рядовые лица А занимаются бизнесом или не занимаются им. Б.

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

...