Pimpl成语与Pure虚拟类接口

Ark*_*nez 117 c++ abstract-class pimpl-idiom

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

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

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

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

Pau*_*rth 61

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

  1. 价值类型

    按值复制,身份永远不重要.它适合作为std :: map中的键.示例,"字符串"类,"日期"类或"复数"类."复制"这样一个类的实例是有道理的.

  2. 实体类型

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

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

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

  • 请参阅Paul de Vrieze对[此答案]的评论(http://stackoverflow.com/a/2330745/213376).如果您在图书馆并希望在不重建客户端的情况下交换.so/.dll,则Pimpl和Pure Virtual会有很大差异.客户端按名称链接到pimpl前端,因此保留旧方法签名就足够了.OTOH在纯抽象的情况下,它们通过vtable索引有效地链接,因此重新排序方法或插入中间将破坏兼容性. (6认同)
  • 您只能在 Pimpl 类前端中添加(或重新排序)方法以保持二进制可比性。按道理来说,你还是换了个界面,看起来有点狡猾。这里的答案是一个合理的平衡,它也可能有助于通过“依赖注入”进行单元测试;但答案总是取决于要求。第三方库编写者(与在您自己的组织中使用库不同)可能非常喜欢 Pimpl。 (2认同)

D.S*_*ley 32

Pointer to implementation通常是隐藏结构实现细节.Interfaces是关于实现不同的实现.他们真的有两个不同的目的.

  • 但您可以使用接口来分离实现细节并隐藏它们 (14认同)
  • 不一定,我已经看到了根据所需的实现存储多个pimpls的类.通常这是一个win32 impl vs一个linux impl,需要在每个平台上以不同的方式实现. (13认同)
  • 为什么过度杀伤?我的意思是,接口方法比pimpl慢吗?可能有逻辑上的原因,但从实际的角度来看,我会说用抽象的界面更容易做到这一点 (10认同)
  • 虽然您可以使用接口实现pimpl,但通常没有理由将实现细节分离.所以没有理由去多态.pimpl的*reason*是为了使实现细节远离客户端(在C++中使它们远离标题).您可以使用抽象基础/接口来执行此操作,但通常这是不必要的过度杀伤. (6认同)

Pon*_*gge 27

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

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


Ili*_*ini 16

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

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

唯一的缺点(我试图对此进行调查)是pimpl成语可能更快

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

  • 还要记住,通过使用继承,您可以引入对vtable布局的依赖性.为了维护ABI,你不能再改变虚函数了(如果没有添加自己虚拟方法的子类,最后添加是安全的). (21认同)

小智 9

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


小智 8

共享库存在一个非常现实的问题,即pimpl习惯用尽巧妙地规避纯虚拟不能:你不能安全地修改/删除类的数据成员而不强迫类的用户重新编译它们的代码.在某些情况下这可能是可以接受的,但对于系统库则不是.

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

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

// library 
A::A()
  : a(0)
{}
Run Code Online (Sandbox Code Playgroud)

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

在代码的用户端,a new A将首先分配sizeof(A)内存字节,然后将指向该内存的指针交给A::A()构造函数as 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())
{}
Run Code Online (Sandbox Code Playgroud)

您现在需要做的就是保持您的公共接口不受指向实现对象的指针之外的数据成员的影响,并且您可以避免此类错误.

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

  • 这是不正确的.如果您将其他类设为"纯抽象基类",则这两种方法都可以隐藏类的实现状态. (10认同)
  • 你的anser中的第一句话暗示具有相关工厂方法的纯虚拟不知道你不会隐藏类的内部状态.这不是真的.这两种技术都可以隐藏类的内部状态.不同之处在于它对用户的看法.pImpl允许您仍然表示具有值语义的类,但也隐藏内部状态.纯抽象基类+工厂方法允许您表示实体类型,还允许您隐藏内部状态.后者正是COM的工作原理."基本COM"的第1章对此有很大的讨论. (10认同)
  • 我不明白,你不应该在你打算用作接口的虚拟纯类中声明私有成员,这个想法是保持类pruely抽象,没有大小,只有纯虚方法,我什么都看不到你不能通过共享库 (9认同)
  • 而不是void*,我认为转发声明实现类更传统:`class A_impl*impl_;` (4认同)

小智 6

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


Sup*_*Jon 5

尽管其他答案广泛涵盖,但也许我可以更明确地说明 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();
Run Code Online (Sandbox Code Playgroud)