Pimpl成语与纯虚拟类接口
我想知道是什么让程序员selectPimpl成语或纯虚拟类和inheritance。
我明白,pimpl习语为每个公共方法和对象创build开销带来了一个明确的额外间接。
纯虚拟类在另一方面带有隐式间接(vtable)的inheritance实现,我明白,没有对象创build开销。
编辑 :但是你会需要一个工厂,如果你从外面创build对象
是什么使得纯粹的虚拟阶级比俚语成语更不可取?
在编写一个C ++类的时候,应该考虑一下是否会出现这种情况
-
价值types
按价值复制,身份永远不重要。 它是在std :: map中的一个键是合适的。 例如,“string”类或“date”类或“复数”类。 复制这样的类的实例是有意义的。
-
一个实体types
身份是重要的。 始终通过引用传递,从来没有通过“价值”。 通常,“复制”课程实例根本没有意义。 当它有意义时,多态“克隆”方法通常更合适。 例子:一个Socket类,一个数据库类,一个“策略”类,任何在function语言中都是“闭包”的东西。
pImpl和纯粹的抽象基类都是减less编译时依赖的技术。
然而,我只使用pImpl来实现值types(types1),并且有时只有当我真的想要最小化耦合和编译时间依赖。 通常情况下,这是不值得的。 正如您正确地指出的那样,由于必须为所有公共方法编写转发方法,所以会有更多的语法开销。 对于types2类,我总是使用纯粹的抽象基类和相关的工厂方法。
Pointer to implementation
通常是关于隐藏结构实现的细节。 Interfaces
是关于实例化不同的实现。 他们真的有两个不同的目的。
pimpl习惯用法可以帮助您减less构build依赖和时间,特别是在大型应用程序中,并将您的类的实现细节的头部暴露最小化到一个编译单元。 你的class级的用户甚至不需要意识到疙瘩的存在(除了作为他们不知道的秘密指针!)。
抽象类(纯虚拟)是客户必须注意的事情:如果您尝试使用它们来减less耦合和循环引用,则需要添加一些方法来允许它们创build对象(例如,通过工厂方法或类,dependency injection或其他机制)。
我正在寻找同一个问题的答案。 阅读一些文章和一些练习后, 我更喜欢使用“纯虚拟类接口” 。
- 他们更直截了当(这是一种主观的看法)。 Pimpl成语让我觉得我正在编写代码“编译器”,而不是“下一个开发人员”,将读取我的代码。
- 一些testing框架可以直接支持模拟纯虚拟类
- 确实,你需要一个工厂从外面访问。 但是,如果你想利用多态性:这也是“亲”,而不是“骗局”。 …和一个简单的工厂方法并不真的伤害这么多
唯一的缺点( 我试图调查这一点 )是pimpl成语可能会更快
- 当代理调用被内联时,在inheritance时必然需要在运行时对对象VTABLE的额外访问
- pimpl public-proxy-class的内存占用更小(可以轻松优化更快的交换和其他类似的优化)
共享库存在一个非常实际的问题,即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对象的指针的某个偏移量(在这种情况下可能为零,因为它是唯一的成员)。
在代码的用户端,一个new A
将首先分配sizeof(A)
个字节的内存,然后把一个指向这个内存的指针传递给A::A()
构造函数。
如果在以后版本的库中,您决定删除整数,使其变大,变小或添加成员,那么内存用户的代码分配量与构造函数代码所需的偏移量之间就会出现不匹配的情况。 如果幸运的话,可能的结果就是崩溃 – 如果你不那么幸运,那么你的软件的performance会很奇怪。
通过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()) {}
现在你所需要做的就是保持你的公共接口不包含指向实现对象的指针以外的数据成员,并且你可以从这类错误中安全地进行操作。
编辑:我也许应该补充说,我在这里谈论构造函数的唯一原因是我不想提供更多的代码 – 同样的论证适用于所有访问数据成员的函数。
我讨厌粉刺! 他们做的类丑陋,不可读。 所有的方法都redirect到疙瘩。 你永远不会在头文件中看到这个类有什么function,所以你不能重构它(例如,简单地改变一个方法的可见性)。 class级感觉像“怀孕”。 我认为使用iterfaces更好,真的足以隐藏来自客户端的实现。 你可以让事件让一个类实现几个接口来保持它们的精简。 人们应该喜欢接口! 注意:你不需要工厂类。 相关的是,类客户端通过适当的接口与它的实例进行通信。 私人方法的隐藏我觉得是一个奇怪的偏执狂,并没有看到这个原因,因为我们有接口。
我们不能忘记,inheritance比代表团更紧密,更紧密。 在决定用什么样的devise习语来解决一个特定的问题时,我也会考虑到答案中提出的所有问题。
根据我的理解,这两件事情完全不同的目的。 疙瘩成语的目的基本上是给你一个处理你的实现,所以你可以做一些事情,如快速交换一种。
虚拟类的目的更多的是允许多态,也就是说,你有一个未知的指向派生types的对象的指针,当你调用函数x时,你总是得到正确的函数,无论基类指针实际指向什么类。
苹果和橘子真的。
尽pipe在其他答案中有广泛的介绍,但也许我可以更清楚地了解pimpl相对于虚拟基类的一个好处:
从用户的angular度来看,pimpl方法是透明的,这意味着您可以在堆栈中创build类的对象,并直接在容器中使用它们。 如果您尝试使用抽象虚拟基类隐藏实现,则需要从工厂返回一个指向基类的共享指针,使其复杂化。 考虑以下等效的客户端代码:
// 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();