Pimpl 习惯用法与纯虚拟类接口

发布于 2024-07-19 03:12:45 字数 234 浏览 8 评论 0原文

我想知道是什么让程序员选择 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

檐上三寸雪 2024-07-26 03:12:45

编写 C++ 类时,应该考虑它是否是

  1. 值类型

    按值复制,身份从来不重要。 它适合作为 std::map 中的键。 例如,“字符串”类、“日期”类或“复数”类。 “复制”此类的实例是有意义的。

  2. 实体类型

    身份很重要。 始终通过引用传递,而不是通过“值”传递。 通常,“复制”类的实例根本没有意义。 当确实有意义时,多态“克隆”方法通常更合适。 示例:Socket 类、数据库类、“策略”类、任何函数式语言中的“闭包”类。

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

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

When writing a C++ class, it's appropriate to think about whether it's going to be

  1. 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.

  2. 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).

当梦初醒 2024-07-26 03:12:45

实现指针通常是为了隐藏结构实现细节。 接口是关于实例化不同的实现。 它们实际上有两个不同的目的。

Pointer to implementation is usually about hiding structural implementation details. Interfaces are about instancing different implementations. They really serve two different purposes.

尾戒 2024-07-26 03:12:45

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

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

The pimpl idiom helps you reduce build dependencies and times especially in large applications, and minimizes header exposure of the implementation details of your class to one compilation unit. The users of your class should not even need to be aware of the existence of a pimple (except as a cryptic pointer to which they are not privy!).

Abstract classes (pure virtuals) is something of which your clients must be aware: if you try to use them to reduce coupling and circular references, you need to add some way of allowing them to create your objects (e.g. through factory methods or classes, dependency injection or other mechanisms).

匿名。 2024-07-26 03:12:45

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

  1. 他们更直接(这是主观意见)。 Pimpl 惯用法让我觉得我是在“为编译器”编写代码,而不是为将阅读我的代码的“下一个开发人员”。
  2. 一些测试框架直接支持模拟纯虚拟类。
  3. 确实,您需要一个可以从外部访问的工厂。
    但如果你想利用多态性:这也是“优点”,而不是“缺点”。 ...并且一个简单的工厂方法并没有真正造成太大的伤害

唯一的缺点(我正在尝试对此进行调查)是

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

I was searching an answer for the same question.
After reading some articles and some practice I prefer using "Pure virtual class interfaces".

  1. They are more straight forward (this is a subjective opinion). Pimpl idiom makes me feel I'm writing code "for the compiler", not for the "next developer" that will read my code.
  2. Some testing frameworks have direct support for Mocking pure virtual classes
  3. It's true that you need a factory to be accessible from the outside.
    But if you want to leverage polymorphism: that's also "pro", not a "con". ...and a simple factory method does not really hurts so much

The only drawback (I'm trying to investigate on this) is that pimpl idiom could be faster

  1. when the proxy-calls are inlined, while inheriting necessarily need an extra access to the object VTABLE at runtime
  2. the memory footprint the pimpl public-proxy-class is smaller (you can do easily optimizations for faster swaps and other similar optimizations)
颜漓半夏 2024-07-26 03:12:45

共享库存在一个非常现实的问题,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())
{}

您现在需要做的就是保持公共接口不包含指针以外的数据成员到实现对象,并且您可以避免此类错误。

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

There's a very real problem with shared libraries that the pimpl idiom circumvents neatly that pure virtuals can't: you cannot safely modify/remove data members of a class without forcing users of the class to recompile their code. That may be acceptable under some circumstances, but not e.g. for system libraries.

To explain the problem in detail, consider the following code in your shared library/header:

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

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

The compiler emits code in the shared library that calculates the address of the integer to be initialized to be a certain offset (probably zero in this case, because it's the only member) from the pointer to the A object it knows to be this.

On the user side of the code, a new A will first allocate sizeof(A) bytes of memory, then hand a pointer to that memory to the A::A() constructor as this.

If in a later revision of your library you decide to drop the integer, make it larger, smaller, or add members, there'll be a mismatch between the amount of memory user's code allocates, and the offsets the constructor code expects. The likely result is a crash, if you're lucky - if you're less lucky, your software behaves oddly.

By pimpl'ing, you can safely add and remove data members to the inner class, as the memory allocation and constructor call happen in the shared library:

// 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())
{}

All you need to do now is keep your public interface free of data members other than the pointer to the implementation object, and you're safe from this class of errors.

Edit: I should maybe add that the only reason I'm talking about the constructor here is that I didn't want to provide more code - the same argumentation applies to all functions that access data members.

眉目亦如画i 2024-07-26 03:12:45

我讨厌痘痘! 它们使课程变得丑陋且不可读。 所有方法均重定向至痘痘。 您永远不会在标头中看到该类具有哪些功能,因此您无法重构它(例如,仅更改方法的可见性)。 上课的感觉就像是“怀孕了”。 我认为使用 iterfaces 更好,并且确实足以向客户端隐藏实现。 您可以让一个类实现多个接口以保持它们的精简。 人们应该更喜欢界面!
注意:您不一定需要工厂类。 相关的是类客户端通过适当的接口与其实例进行通信。
我认为隐藏私有方法是一种奇怪的偏执,并且看不出这样做的原因,因为我们有接口。

I hate pimples! They do the class ugly and not readable. All methods are redirected to pimple. You never see in headers, what functionalities has the class, so you can not refactor it (e. g. simply change the visibility of a method). The class feels like "pregnant". I think using iterfaces is better and really enough to hide the implementation from the client. You can event let one class implement several interfaces to hold them thin. One should prefer interfaces!
Note: You do not necessary need the factory class. Relevant is that the class clients communicate with it's instances via the appropriate interface.
The hiding of private methods I find as a strange paranoia and do not see reason for this since we hav interfaces.

听闻余生 2024-07-26 03:12:45

我们决不能忘记,继承比委托具有更强、更紧密的耦合性。 在决定采用哪些设计习惯来解决特定问题时,我还会考虑答案中提出的所有问题。

We must not forget that inheritance is a stronger, closer coupling than delegation. I would also take into account all the issues raised in the answers given when deciding what design idioms to employ in solving a particular problem.

缱绻入梦 2024-07-26 03:12:45

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

Although broadly covered in the other answers maybe I can be a bit more explicit about one benefit of pimpl over virtual base classes:

A pimpl approach is transparent from the user view point, meaning you can e.g. create objects of the class on the stack and use them directly in containers. If you try to hide the implementation using an abstract virtual base class, you will need to return a shared pointer to the base class from a factory, complicating it's use. Consider the following equivalent client code:

// 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();
∞琼窗梦回ˉ 2024-07-26 03:12:45

根据我的理解,这两件事有完全不同的目的。 粉刺习惯用法的目的基本上是为您提供实现的句柄,以便您可以执行诸如快速交换某种排序之类的操作。

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

确实是苹果和橙子。

In my understanding these two things serve completely different purposes. The purpose of the pimple idiom is basically give you a handle to your implementation so you can do things like fast swaps for a sort.

The purpose of virtual classes is more along the line of allowing polymorphism, i.e. you have a unknown pointer to an object of a derived type and when you call function x you always get the right function for whatever class the base pointer actually points to.

Apples and oranges really.

亚希 2024-07-26 03:12:45

pimpl 习惯用法最烦人的问题是它使得维护和分析现有代码变得极其困难。 因此,使用 pimpl,您付出了开发人员的时间和挫折,只是为了“减少构建依赖性和时间,并最大限度地减少实现细节的标头暴露”。 自己决定吧,如果真的值得的话。

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

The most annoying problem about the pimpl idiom is it makes it extremely hard to maintain and analyse existing code. So using pimpl you pay with developer time and frustration only to "reduce build dependencies and times and minimize header exposure of the implementation details". Decide yourself, if it is really worth it.

Especially "build times" is a problem you can solve by better hardware or using tools like Incredibuild ( www.incredibuild.com, also already included in Visual Studio 2017 ), thus not affecting your software design. Software design should be generally independent of the way the software is built.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文