Pimpl成语与Pure虚拟类接口

时间:2009-05-05 14:05:14

标签: c++ abstract-class pimpl-idiom

我想知道什么会让程序员选择Pimpl习语或纯虚拟类和继承。

据我所知,pimpl习惯用于为每个公共方法和对象创建开销提供一个明确的额外间接。

另一方面,Pure虚拟类带有继承实现的隐式间接(vtable),我理解没有对象创建开销。
编辑:但如果您从外部创建对象,则需要工厂

是什么让纯虚拟类比pimpl成语更不可取?

10 个答案:

答案 0 :(得分:59)

在编写C ++类时,考虑它是否适合

是合适的
  1. 值类型

    按值复制,身份永远不重要。它适合作为std :: map中的键。示例,“字符串”类,“日期”类或“复数”类。 “复制”这类课程的实例是有道理的。

  2. 实体类型

    身份很重要。总是通过引用传递,而不是“价值”。通常,完全“复制”该类的实例是没有意义的。当它确实有意义时,多态“克隆”方法通常更合适。示例:一个Socket类,一个Database类,一个“策略”类,任何在函数式语言中都是“闭包”的东西。

  3. pImpl和纯抽象基类都是减少编译时依赖性的技术。

    但是,我只使用pImpl来实现Value类型(类型1),有时只是在我真正想要最小化耦合和编译时依赖性时。通常,这不值得打扰。正如您正确指出的那样,语法开销更多,因为您必须为所有公共方法编写转发方法。对于类型2类,我总是使用带有关联工厂方法的纯抽象基类。

答案 1 :(得分:33)

Pointer to implementation通常是隐藏结构实现细节。 Interfaces是关于实现不同实现的实例。它们真的有两个不同的用途。

答案 2 :(得分:27)

pimpl习惯用法可以帮助您减少构建依赖性和时间,尤其是在大型应用程序中,并最大限度地减少类的实现细节到一个编译单元的标头暴露。你班上的用户甚至不需要知道是否存在疙瘩(除非是一个他们不知情的神秘指针!)。

抽象类(纯虚拟)是客户必须注意的事项:如果您尝试使用它们来减少耦合和循环引用,您需要添加一些允许它们创建对象的方法(例如通过工厂方法)或类,依赖注入或其他机制)。

答案 3 :(得分:13)

我正在寻找同一个问题的答案。 在阅读了一些文章和一些练习之后我更喜欢使用“纯虚拟类接口”

  1. 他们更直接(这是主观意见)。 Pimpl成语让我觉得我正在为编译器编写代码,而不是用于读取我的代码的“下一个开发人员”。
  2. 某些测试框架直接支持Mocking纯虚拟类
  3. 需要 确实可以从外部访问工厂。 但是如果你想利用多态性:那也是“亲”,而不是“骗局”。 ......而一个简单的工厂方法并没有真正伤害到这么多
  4. 唯一的缺点(我正在尝试调查此)是pimpl成语可能更快

    1. 代理调用内联时,继承必须在运行时需要额外访问对象VTABLE
    2. pimpl public-proxy-class的内存占用量较小(您可以轻松优化更快的交换和其他类似的优化)

答案 4 :(得分:9)

共享库存在一个非常现实的问题,即pimpl习惯用尽巧妙地避免了纯虚拟机无法解决的问题:您无法安全地修改/删除类的数据成员而不强迫类的用户重新编译其代码。在某些情况下这可能是可以接受的,但不是例如对于系统库。

要详细解释此问题,请考虑共享库/标题中的以下代码:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

编译器在共享库中发出代码,用于计算要初始化的整数的地址,该指针是指向它知道的A对象的指针的某个偏移量(在这种情况下可能为零,因为它是唯一的成员) this

在代码的用户端,new A将首先分配sizeof(A)字节的内存,然后将指向该内存的指针作为A::A()传递给this构造函数

如果在库的更高版本中决定删除整数,使其更大,更小或添加成员,则用户代码分配的内存量与构造函数代码所需的偏移量之间将存在不匹配。可能的结果是崩溃,如果你很幸运 - 如果你不太幸运,你的软件表现得很奇怪。

通过pimpl'ing,您可以安全地向内部类添加和删除数据成员,因为内存分配和构造函数调用发生在共享库中:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

现在你需要做的就是保持你的公共接口没有指向实现对象的指针之外的数据成员,并且你可以避免这类错误。

编辑:我应该补充一点,我在这里谈论构造函数的唯一原因是我不想提供更多代码 - 相同的论证适用于访问数据的所有函数成员。

答案 5 :(得分:8)

我讨厌青春痘!他们做的课很难看,也不易读。所有方法都被重定向到疙瘩。你永远不会在标题中看到这个类有什么功能,所以你不能重构它(例如,只是改变一个方法的可见性)。这堂课感觉像是“怀孕”。我认为使用iterfaces更好,真的足以隐藏客户端的实现。您可以让一个类实现多个接口来保持它们的精简。人们应该更喜欢接口! 注意:您不需要工厂类。相关的是,类客户端通过适当的接口与其实例进行通信。 私有方法的隐藏我发现是一种奇怪的偏执狂,并且由于我们没有接口,所以没有看到这个原因。

答案 6 :(得分:6)

我们不能忘记,继承是一种比授权更强大,更紧密的耦合。在决定解决特定问题时使用的设计习语时,我还会考虑所给出的答案中提出的所有问题。

答案 7 :(得分:3)

虽然在其他答案中有广泛的涵盖,但我可以更明确地了解pimpl相对于虚拟基类的一个好处:

pimpl方法在用户视点中是透明的,这意味着您可以例如在堆栈上创建类的对象,并直接在容器中使用它们。如果您尝试使用抽象虚拟基类隐藏实现,则需要从工厂返回指向基类的共享指针,从而使其复杂化。请考虑以下等效客户端代码:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();

答案 8 :(得分:2)

据我所知,这两件事完全不同。疙瘩成语的目的基本上是让你掌握你的实现,这样你就可以做一些快速交换等事情。

虚拟类的目的更多地是允许多态,即你有一个指向派生类型对象的未知指针,当你调用函数x时,总是得到基本指针实际指向的任何类的正确函数到。

苹果和橘子真的。

答案 9 :(得分:1)

关于pimpl习语的最烦人的问题是它使得维护和分析现有代码变得非常困难。因此,使用pimpl,您需要花费开发人员的时间和挫败感,以“减少构建依赖性和时间,并最大限度地减少实现细节的标头暴露”。如果确实值得,请自己决定。

特别是“构建时间”是一个问题,您可以通过更好的硬件或使用Incredibuild(www.incredibuild.com,也已包含在Visual Studio 2017中)等工具来解决这个问题,从而不影响您的软件设计。软件设计通常应与软件的构建方式无关。