继承中的Pimpl惯用法

20

我希望使用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*/};
};

但我不能在B中使用pAimpl,因为它是A的私有成员。
所以我看到一些解决方法:
  1. 在B中创建BImpl* pBImpl成员,并通过额外的A构造函数A(AImpl*)将其传递给A。
  2. 将pAImpl更改为受保护的(或添加Get函数),并在B中使用它。
  3. B不应该继承自A。在B中创建BImpl* pBImpl成员,并在B中创建foo()和bar(),这将使用pBImpl。
  4. 还有其他方法吗?
我应该选择哪一个呢?
5个回答

9
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
当AImpl和BImpl在不同的.cpp文件中时,我认为这不会起作用。BImple不应该能够从AImple继承--因为AImple在定义BImple的.cpp文件中是不完整的类型。 - Enigma22134
1
@Enigma22134,将它们分成4个文件,例如A.hB.hAImpl.hBImpl.h,可以解决您提到的问题吗? - javaLover
2
@javaLover 是的,但我认为想法是将实现细节隐藏在cpp中。将impl放在头文件中并不能隐藏它们。虽然我想只有A.cpp和B.cpp包含AImpl.h和BImpl.h文件,这样做仍然可以加快编译时间。 - Enigma22134

3

从纯面向对象理论的角度来看,我认为最好的方法是不让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's BImpl being out of sync with its AImpl, though.
}

尽管您可能没有swap()函数,但是类似的问题很容易发生,特别是当A是可分配的时,无论是意外还是有意为之。这是对Liskov替换原则的一种微妙违反。解决方案有两种:
1.不要在构造后更改pimpl成员。将它们声明为。然后,派生构造函数可以传递适当的类型,其余的派生类可以自信地向下转换。但是,如果使用继承层次结构,则不能进行非抛出交换、赋值或写时复制等操作,因为这些技术需要更改pimpl成员。但是,如果您具有继承层次结构,则可能并没有真正打算执行这些操作。
2.为A和B的私有变量分别拥有不相关(且愚蠢的)AImpl和BImpl类。如果B想对A执行某些操作,则使用A的公共或保护接口。这也保留了使用pimpl的最常见原因:能够将AImpl的定义隐藏在无法使用派生类的cpp文件中,因此当A的实现更改时,程序的一半不需要重新编译。

1

我会选择(1),因为A的私有属性与B无关。

实际上,我不会像你建议的那样将其传递给A,因为A在A::A()中自己创建。 从B调用pApimpl->whatever()也不合适(私有意味着私有)。


1
如果我创建A(AImpl *),它将从B接收Aimpl *,并且将不会创建它自己的。 - Igor
我知道这个。但A和B应该有自己的私有成员变量,这就是为什么它们被称为“私有”的原因。如果B从A继承而不使用PIMPL,则B也无法看到和使用A的私有成员变量,那么使用PIMPL会有什么不同呢? - EricSchaefer

1
正如stefan.ciobaca所说,如果您真的想让A可扩展,您需要使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转发到适当的实现者。

-4

正确的方法是做(2)。

一般来说,你应该默认将所有成员变量设置为受保护的,而不是私有的。

大多数程序员选择私有的原因是他们没有考虑到其他想要从他们的类派生的人,而且大部分介绍 C++ 的手册都教授这种风格,也就是所有的示例都使用了私有。

编辑

在使用 pimp 设计模式时,代码重复和内存分配是不希望出现的副作用,据我所知无法避免。

如果您需要让 Bimpl 继承 Aimpl,并且希望通过 A 和 B 向它们公开一致的接口,那么 B 也需要继承 A。

在这种情况下,您可以做一件事情来简化问题,即让 B 从 A 继承,并仅更改构造函数,以便 B::B(...) {} 创建一个 Bimpl,并添加所有不在 Aimpl 中的 Bimpl 方法的调度。


是的,我也会选择使用protected。但是你如何处理pimpl的创建呢?每个类都应该有自己的pimpl吗?还是它们应该共享一个位于基类中的相同pimpl,并由最派生类创建并作为构造函数参数提供? - Johannes Schaub - litb
不加思考的话,我可能会选择为每个派生类使用单独的pimpl。但这需要每个类动态分配一次内存,这可能不是理想的选择。但这可能是最简单的方法。不确定如何在pimpl中处理虚函数。 - Johannes Schaub - litb
6
不。大多数程序员选择私有(private)的原因是他们了解“封装(encapsulation)”这个词的含义。 - Marc Mutz - mmutz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接