将私有部分保留在c ++头之外:纯虚基类vs pimpl

skr*_*bel 7 c++ pimpl-idiom header pure-virtual

我最近从Java和Ruby切换回C++,令我惊讶的是,当我更改私有方法的方法签名时,我必须重新编译使用公共接口的文件,因为私有部分也在.h文件中.

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

BananaTree.h:

class Banana;

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

  static BananaTree* create(std::string const& name);
};
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

这里唯一的麻烦就是我不能使用new,而必须打电话BananaTree::create().我不认为这确实是一个问题,特别是因为我希望无论如何都要使用很多工厂.

然而,现在,C++成名的聪明人提出了另一个解决方案,即pImpl成语.有了它,如果我理解正确,我的代码将如下所示:

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

这意味着BananaTree在这种情况下,我必须为每个公共方法实现装饰器样式的转发方法getBanana.这听起来像是我不想要的复杂性和维护工作的附加级别.

那么,现在问题是:纯虚拟类方法有什么问题?为什么pImpl方法有更好的记录?我错过了什么吗?

jal*_*alf 12

我可以想到一些差异:

使用虚拟基类,您可以打破一些人们对行为良好的C++类所期望的语义:

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

BananaTree myTree("somename");
Run Code Online (Sandbox Code Playgroud)

否则,我失去了RAII,我必须手动开始跟踪分配,这会导致很多麻烦和内存泄漏.

我也希望复制课程,我可以简单地做到这一点

BananaTree tree2 = mytree;
Run Code Online (Sandbox Code Playgroud)

当然,除非通过将复制构造函数标记为私有来禁止复制,在这种情况下该行甚至不会编译.

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

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

基本上,你的界面在我们喜欢的现代C++中表现不佳

  • 尽量避免使用指针,并且
  • stack-分配所有对象以从形式自动生命周期管理中受益.

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

最后,我有时用来减少样板代码量的pimpl的一个更简单的变体是让"外部"对象访问内部对象的数据成员,这样就可以避免重复接口.外部对象上的函数只是直接从内部对象访问它需要的数据,或者它调用内部对象上的辅助函数,该函数在外部对象上没有等价物.

在您的示例中,您可以删除该函数Impl::getBanana,并改为执行BananaTree::getBanana如下:

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}
Run Code Online (Sandbox Code Playgroud)

那么你只需要实现一个getBanana函数(在BananaTree类中)和一个findBanana函数(在Impl类中).