Pimpl 习惯用法与纯虚拟类接口
我想知道是什么让程序员选择 Pimpl 惯用法或纯虚拟类和继承。
据我了解,pimpl 习惯用法为每个公共方法和对象创建开销提供了一个显式的额外间接寻址。
另一方面,纯虚拟类为继承实现提供了隐式间接(vtable),并且我知道没有对象创建开销。
编辑:但是如果你从外部创建对象,你就需要一个工厂。
是什么让纯虚拟类不如 pimpl 习惯用法那么理想?
I was wondering what would make a programmer to choose either Pimpl idiom or pure virtual class and inheritance.
I understand that pimpl idiom comes with one explicit extra indirection for each public method and the object creation overhead.
The Pure virtual class in the other hand comes with implicit indirection(vtable) for the inheriting implementation and I understand that no object creation overhead.
EDIT: But you'd need a factory if you create the object from the outside
What makes the pure virtual class less desirable than the pimpl idiom?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
发布评论
评论(10)
我正在寻找同一问题的答案。
在阅读了一些文章和一些实践之后我更喜欢使用“纯虚拟类接口”。
- 他们更直接(这是主观意见)。 Pimpl 惯用法让我觉得我是在“为编译器”编写代码,而不是为将阅读我的代码的“下一个开发人员”。
- 一些测试框架直接支持模拟纯虚拟类。
- 确实,您需要一个可以从外部访问的工厂。
但如果你想利用多态性:这也是“优点”,而不是“缺点”。 ...并且一个简单的工厂方法并没有真正造成太大的伤害
唯一的缺点(我正在尝试对此进行调查)是
- 当代理调用内联时,pimpl 惯用语可能会更快,而继承必然需要在运行时对对象 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)
{}
编译器在共享库中发出代码,计算要初始化的整数的地址为某个偏移量(在本例中可能为零,因为它是唯一的成员)从指针到它知道是 this
的 A 对象。
在代码的用户端,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())
{}
您现在需要做的就是保持公共接口不包含指针以外的数据成员到实现对象,并且您可以避免此类错误。
编辑:我也许应该补充一点,我在这里讨论构造函数的唯一原因是我不想提供更多代码 - 相同的论证适用于访问数据成员的所有函数。
尽管在其他答案中广泛涵盖,但也许我可以更明确地说明 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();
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
编写 C++ 类时,应该考虑它是否是
值类型
按值复制,身份从来不重要。 它适合作为 std::map 中的键。 例如,“字符串”类、“日期”类或“复数”类。 “复制”此类的实例是有意义的。
实体类型
身份很重要。 始终通过引用传递,而不是通过“值”传递。 通常,“复制”类的实例根本没有意义。 当确实有意义时,多态“克隆”方法通常更合适。 示例:Socket 类、数据库类、“策略”类、任何函数式语言中的“闭包”类。
pImpl 和纯抽象基类都是减少编译时间依赖性的技术。
然而,我只使用 pImpl 来实现值类型(类型 1),并且仅在某些时候当我确实想要最小化耦合和编译时依赖性时才使用。 通常,这是不值得打扰的。 正如您正确指出的那样,存在更多语法开销,因为您必须为所有公共方法编写转发方法。 对于类型 2 类,我总是使用带有关联工厂方法的纯抽象基类。
When writing a C++ class, it's appropriate to think about whether it's going to be
A Value Type
Copy by value, identity is never important. It's appropriate for it to be a key in a std::map. Example, a "string" class, or a "date" class, or a "complex number" class. To "copy" instances of such a class makes sense.
An Entity type
Identity is important. Always passed by reference, never by "value". Often, doesn't make sense to "copy" instances of the class at all. When it does make sense, a polymorphic "Clone" method is usually more appropriate. Examples: A Socket class, a Database class, a "policy" class, anything that would be a "closure" in a functional language.
Both pImpl and pure abstract base class are techniques to reduce compile time dependencies.
However, I only ever use pImpl to implement Value types (type 1), and only sometimes when I really want to minimize coupling and compile-time dependencies. Often, it's not worth the bother. As you rightly point out, there's more syntactic overhead because you have to write forwarding methods for all of the public methods. For type 2 classes, I always use a pure abstract base class with associated factory method(s).