代码之家  ›  专栏  ›  技术社区  ›  skrebbel

在C++标题外保持私有部分:纯虚拟基类与piml

  •  7
  • skrebbel  · 技术社区  · 15 年前

    最近,我从Java和露比切换到C++,让我惊讶的是,当我更改私有方法的方法签名时,我不得不重新编译使用公共接口的文件,因为私有部分也在.h文件中。

    我很快想出了一个解决方案,我想,这对于Java程序员来说是典型的:接口(=纯虚拟基类)。例如:

    香蕉:

    class Banana;
    
    class BananaTree
    {
    public:
      virtual Banana* getBanana(std::string const& name) = 0;
    
      static BananaTree* create(std::string const& name);
    };
    

    BananaTree.cpp:

    class BananaTreeImpl : public BananaTree
    {
    private:
      string name;
    
      Banana* findBanana(string const& name)
      {
        return //obtain banana, somehow;
      }
    
    public:
      BananaTreeImpl(string name) 
        : name(name)
      {}
    
      virtual Banana* getBanana(string const& name)
      {
        return findBanana(name);
      }
    };
    
    BananaTree* BananaTree::create(string const& name)
    {
      return new BananaTreeImpl(name);
    }
    

    这里唯一的麻烦是我不能用 new ,必须改为调用 BananaTree::create() . 我不认为这是一个真正的问题,特别是因为我希望使用工厂很多。

    现在,C++名人的智者想出了另一个解决方案, pImpl idiom . 这样,如果我理解正确,我的代码将如下所示:

    香蕉:

    class BananaTree
    {
    public:
      Banana* addStep(std::string const& name);
    
    private:
      struct Impl;
      shared_ptr<Impl> pimpl_;
    };
    

    BananaTree.cpp:

    struct BananaTree::Impl
    {
      string name;
    
      Banana* findBanana(string const& name)
      {
        return //obtain banana, somehow;
      }
    
      Banana* getBanana(string const& name)
      {
        return findBanana(name);
      }
    
      Impl(string const& name) : name(name) {}
    }
    
    BananaTree::BananaTree(string const& name)
      : pimpl_(shared_ptr<Impl>(new Impl(name)))
    {}
    
    Banana* BananaTree::getBanana(string const& name)
    {
      return pimpl_->getBanana(name);
    }
    

    这意味着我必须为 BananaTree 在这种情况下 getBanana . 这听起来像是一个额外的复杂度和维护工作,我不想要求。

    所以,现在问一个问题:纯虚拟类方法有什么问题?为什么PIMPL方法有这么多更好的文档记录?我错过什么了吗?

    2 回复  |  直到 15 年前
        1
  •  12
  •   Stack Overflow is garbage    15 年前

    我可以想到一些不同之处:

    使用虚拟基类,可以打破人们期望从行为良好的C++类中得到的一些语义:

    我希望(或要求,甚至)类在堆栈上被实例化,如下所示:

    BananaTree myTree("somename");
    

    否则,我会丢失RAII,并且必须手动开始跟踪分配,这会导致许多头痛和内存泄漏。

    我也希望能复制这个类,我可以简单地做这个

    BananaTree tree2 = mytree;
    

    当然,除非将复制构造函数标记为private,否则不允许复制,在这种情况下,该行甚至不会编译。

    在上面的例子中,我们显然遇到了这样一个问题:您的接口类没有真正有意义的构造函数。但是如果我尝试使用上面的示例这样的代码,我也会遇到很多切片问题。 对于多态对象,通常需要保存指向对象的指针或引用,以防止切片。正如我的第一点,这通常是不可取的,并使内存管理更加困难。

    您的代码的读者是否会理解 BananaTree 基本上不起作用,他必须使用 BananaTree* BananaTree& 相反?

    基本上,你的界面和现代C++没有那么好,我们更喜欢

    • 尽量避免使用指针,以及
    • 堆栈将所有对象分配给从自动生命周期管理中获益的对象。

    顺便说一下,您的虚拟基类忘记了虚拟析构函数。这是个明显的错误。

    最后,我有时使用PIMPL的一个简单变体来减少样板代码的数量,这是为了让“外部”对象访问内部对象的数据成员,这样就避免了复制接口。外部对象上的函数直接从内部对象访问所需的数据,或者在内部对象上调用辅助函数,而外部对象上没有等效的辅助函数。

    在您的示例中,您可以删除函数并 Impl::getBanana ,并改为实现 BananaTree::getBanana 这样地:

    Banana* BananaTree::getBanana(string const& name)
    {
      return pimpl_->findBanana(name);
    }
    

    then you only have to implement one getBanana 功能(在 香蕉果 一类) findBanana 功能(在 Impl 班级)。

        2
  •  1
  •   stefaanv    15 年前

    实际上,这只是一个设计决策。即使你做出了“错误”的决定,也不难改变。

    PIMPL还用于在堆栈上提供ligthweight对象,或者通过引用同一个实现对象来呈现“副本”。
    委派功能可能很麻烦,但这只是一个小问题(简单,因此没有真正增加的复杂性),特别是在有限的类中。

    C++中的接口通常更多地用于策略,比如您希望能够选择实现的方式,尽管这不是必需的。