【问题标题】:Pimpl idiom with inheritance带有继承的 Pimpl 成语
【发布时间】:2010-10-04 05:27:38
【问题描述】:

我想使用 pimpl idiom 和继承。

这里是基础公共类及其实现类:

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. 还有其他方法吗?

我应该选择什么?

【问题讨论】:

    标签: c++ inheritance oop pimpl-idiom


    【解决方案1】:

    我认为从纯粹的面向对象理论的角度来看,最好的方法是不要让 BImpl 从 AImpl 继承(这就是你在选项 3 中的意思吗?)。但是,如果 pimpl 成员变量是const,让 BImpl 从 AImpl 派生(并将所需的 impl 传递给 A 的构造函数)也是可以的。使用 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 是可赋值的,无论是偶然还是有意。这有点微妙地违反了 Liskov 可替代性原则。解决方案是:

    1. 构建后不要更改 pimpl 成员。将它们声明为AImpl * const pimpl。然后,派生构造函数可以传递适当的类型,派生类的其余部分可以自信地向下转换。但是,你不能例如执行非抛出交换、分配或写时复制,因为这些技术要求您可以更改 pimpl 成员。但是,如果您有继承层次结构,您可能并不真的打算做这些事情。

    2. 对于 A 和 B 的私有变量,分别有不相关(和愚蠢)的 AImpl 和 BImpl 类。如果 B 想对 A 做某事,则使用 A 的公共或受保护接口。这也保留了使用 pimpl 的最常见原因:能够将 AImpl 的定义隐藏在派生类无法使用的 cpp 文件中,因此当 A 的实现发生更改时,您的一半程序不需要重新编译。

    【讨论】:

      【解决方案2】:
      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*/};
      };
      

      【讨论】:

      • 当 AImpl 和 BImpl 在单独的 .cpp 文件中时,我认为这不会起作用。 BImple 不应该能够从 AImple 继承——因为 AImple 是定义 BImple 的 .cpp 中的不完整类型。
      • @Enigma22134 可以将它们分成 4 个文件,例如A.hB.hAImpl.hBImpl.h解决了你提到的问题?
      • @javaLover 是的,但我认为这个想法是在 cpp 中隐藏实现细节。将 impl 放在标题中并不会隐藏它们。虽然我认为仍然会加快编译时间,因为包括 AImpl.h 和 BImpl.h 在内的唯一文件是 A.cpp 和 B.cpp
      【解决方案3】:

      正确的做法是做(2)。

      一般来说,您可能应该考虑将所有成员变量设置为默认受保护而不是私有。

      大多数程序员选择私有的原因是他们不考虑其他人想要从他们的类中派生,大多数介绍性 C++ 手册都教授这种风格,因为所有示例都使用私有。

      编辑

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

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

      在这种情况下,您可以做的简化事情的一件事是让 B 从 A 继承,并且只更改构造器,使得 B::B(...) {} 创建一个 Bimpl,并为所有方法添加调度不在 Aimpl 中的 Bimpl。

      【讨论】:

      • 是的,我也会选择受保护的。但是你如何处理 pimpl 的创建?每个班级都应该有自己的粉刺吗?还是它们应该共享一个驻留在基类中并且由最派生类创建的所有相同的 pimpl,作为构造函数参数提出?
      • 不假思索,我可能会为每个派生类使用一个单独的 pimpl。但这需要为每个分配一个动态内存。不可能。但这可能是最简单的事情。不知道如何处理粉刺中的虚拟。
      • 没有。大多数程序员选择私有的原因是他们知道“封装”这个词的含义......
      【解决方案4】:

      正如 stefan.ciobaca 所说,如果你真的希望 A 可扩展,你会希望 pAImpl 受到保护。

      但是,您在void bar(){pAImpl-&gt;bar();};B 中的定义似乎很奇怪,因为barBImpl 上的一个方法,而不是AImpl

      至少有三个简单的替代方案可以避免这个问题:

      1. 您的替代方案 (3)。
      2. (3) 的变体,其中BImpl 扩展AImpl(继承foo 的现有实现而不是定义另一个),BImpl 定义bar,并且B 使用其私有@987654333 @ 访问两者。
      3. 委托,其中B 持有指向AImplBImpl 的私有指针,并将foobar 中的每一个转发给相应的实施者。

      【讨论】:

        【解决方案5】:

        我会做 (1),因为 A 的私人事务对 B 来说是有或没有的。

        实际上,我不会按照您的建议将其传递给 A,因为 A 在 A::A() 中创建了自己的。 从 Bis 调用 pApimpl-&gt;whatever() 也不合适(私有意味着私有)。

        【讨论】:

        • 如果我创建 A(AImpl*),它将从 B 接收 Aimpl*,并且不会创建自己的。
        • 我知道。但是 A 和 B 真的应该有自己的隐私。这正是它们被称为“私人”的原因。如果 B 在没有 PIMPL 的情况下从 A 继承,B 也无法查看和使用 A 的私有数据,那么 PIMPL 为什么会有所不同?
        猜你喜欢
        • 1970-01-01
        • 2014-08-30
        • 2018-12-27
        • 2011-09-24
        • 2017-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-24
        相关资源
        最近更新 更多