我最近从Java和Ruby切换回C++,而令我惊讶的是,当我更改私有(private)方法的方法签名时,我不得不重新编译使用公共(public)接口(interface)的文件,因为私有(private)部分也位于.h文件中。

我很快想到了一个Java程序员典型的解决方案:接口(interface)(=纯虚拟基类)。例如:

BananaTree.h:

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。这样,如果我正确理解它,我的代码将如下所示:

BananaTree.h:
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的每个公共(public)方法(在本例中为getBanana)实现一个装饰器风格的转发方法。这听起来像是我不愿意不需要的复杂性和维护工作水平的提高。

因此,现在要解决的问题是:纯虚拟类方法有什么问题?为什么对pImpl方法有更好的记录?我想念什么吗?

最佳答案

我可以想到一些区别:

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

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

BananaTree myTree("somename");

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

我也希望复制类(class),我可以简单地做到这一点
BananaTree tree2 = mytree;

除非当然可以通过将复制构造函数标记为私有(private)来禁止复制,在这种情况下,该行甚至无法编译。

在上述情况下,我们显然会遇到一个问题,即您的接口(interface)类实际上没有有意义的构造函数。但是,如果我尝试使用上述示例中的代码,则还会遇到很多切片问题。
对于多态对象,通常需要持有指向对象的指针或引用,以防止切片。在我的第一点上,这通常是不希望的,并且使内存管理更加困难。

您的代码阅读者是否会理解BananaTree基本不起作用,而必须改用BananaTree*BananaTree&

基本上,您的界面在现代C++中不能很好地发挥作用,我们更喜欢
  • 尽可能避免使用指针,而
  • 堆栈分配所有对象,以受益于自动生命周期管理。

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

    最后,我有时用来减少样板代码数量的pimpl的一个更简单的变体是使“外部”对象可以访问内部对象的数据成员,因此避免了重复接口(interface)。外部对象上的函数只是直接从内部对象访问所需的数据,或者在内部对象上调用辅助函数,而外部对象上没有等效函数。

    在您的示例中,您可以删除函数和Impl::getBanana,而改为实现BananaTree::getBanana,如下所示:
    Banana* BananaTree::getBanana(string const& name)
    {
      return pimpl_->findBanana(name);
    }
    

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

    07-26 01:24