7

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

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

香蕉树.h:

class Banana;

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

  static BananaTree* create(std::string const& name);
};

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

香蕉树.h:

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

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

香蕉树.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 方法的文档记录得这么好?我错过了什么吗?

4

2 回答 2

12

我能想到几个不同点:

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

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

BananaTree myTree("somename");

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

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

BananaTree tree2 = mytree;

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

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

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

基本上,您的界面不能很好地与现代 C++ 配合使用,而我们更喜欢在现代 C++ 中使用

  • 尽可能避免使用指针,并且
  • 堆栈分配所有对象以受益于自动生命周期管理。

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

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

在您的示例中,您可以删除函数 and Impl::getBanana,而是BananaTree::getBanana像这样实现:

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

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

于 2010-06-22T11:17:57.990 回答
1

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

pimpl 还用于在堆栈上提供轻量级对象或通过引用相同的实现对象来呈现“副本”。
委托功能可能很麻烦,但这是一个小问题(简单,因此不会增加真正的复杂性),尤其是对于有限的类。

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

于 2010-06-22T11:16:20.477 回答