Pimpl 习语的实践

发布于 2024-07-19 04:37:04 字数 347 浏览 6 评论 0原文

SO 上有一些关于 pimpl 惯用法 的问题,但我更好奇它在实践中的使用频率。

我知道性能和封装之间存在一些权衡,再加上额外的重定向带来的一些调试烦恼。

那么,这是应该在每个班级中采用,还是全有或全无的基础上采用? 这是最佳实践还是个人偏好?

我意识到这有些主观,所以让我列出我的首要任务:

  • 代码清晰度
  • 代码可维护性
  • 性能

我总是假设我需要在某个时候将我的代码公开为库,所以这也是一个考虑因素。

编辑:任何其他选项来完成同样的事情将是受欢迎的建议。

There have been a few questions on SO about the pimpl idiom, but I'm more curious about how often it is leveraged in practice.

I understand there are some trade-offs between performance and encapsulation, plus some debugging annoyances due to the extra redirection.

With that, is this something that should be adopted on a per-class, or an all-or-nothing basis? Is this a best-practice or personal preference?

I realize that's somewhat subjective, so let me list my top priorities:

  • Code clarity
  • Code maintainability
  • Performance

I always assume that I will need to expose my code as a library at some point, so that's also a consideration.

EDIT: Any other options to accomplish the same thing would be welcome suggestions.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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

发布评论

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

评论(8

我一向站在原地 2024-07-26 04:37:04

我想说的是,你是否在每堂课上这样做,还是在全有或全无的基础上进行,取决于你首先为什么选择 pimpl 习惯用法。 在构建库时,我的原因是以下之一:

  • 想要隐藏实现以避免泄露信息(是的,这不是一个 FOSS 项目:)
  • 想要隐藏实现以便减少客户端代码的依赖性。 如果您构建共享库 (DLL),则可以更改 pimpl 类,甚至无需重新编译应用程序。
  • 希望减少使用该库编译类所需的时间。
  • 想要修复命名空间冲突(或类似的)。

这些原因都不会促使我们采取“全有或全无”的方法。 在第一种情况下,您只需简化要隐藏的内容,而在第二种情况下,对于您希望更改的类来说,这样做可能就足够了。 另外,出于第三个和第四个原因,隐藏重要的成员只会带来好处,而这些成员又需要额外的标头(例如,第三方库的标头,甚至是 STL 的标头)。

无论如何,我的观点是,我通常不会发现这样的东西太有用:

class Point {
  public:      
    Point(double x, double y);
    Point(const Point& src);
    ~Point();
    Point& operator= (const Point& rhs);

    void setX(double x);
    void setY(double y);
    double getX() const;
    double getY() const;

  private:
    class PointImpl;
    PointImpl* pimpl;
}

在这种情况下,你会开始权衡,因为需要取消引用指针,并且无法内联方法。 但是,如果您仅对重要的类执行此操作,那么通常可以容忍轻微的开销,不会出现任何问题。

I'd say that whether you do it per-class or on an all-or-nothing basis depends on why you go for the pimpl idiom in the first place. My reasons, when building a library, have been one of the following:

  • Wanted to hide implementation in order to avoid disclosing information (yes, it was not a FOSS project :)
  • Wanted to hide implementation in order to make client code less dependent. If you build a shared library (DLL), you can change your pimpl class without even recompiling the application.
  • Wanted to reduce the time it takes to compile the classes using the library.
  • Wanted to fix a namespace clash (or similar).

None of these reasons prompts for the all-or-nothing approach. In the first one, you only pimplize what you want to hide, whereas in the second case it's probably enough to do so for classes which you expect to change. Also for the third and fourth reason there's only benefit from hiding non-trivial members that in turn require extra headers (e.g., of a third-party library, or even STL).

In any case, my point is that I wouldn't typically find something like this too useful:

class Point {
  public:      
    Point(double x, double y);
    Point(const Point& src);
    ~Point();
    Point& operator= (const Point& rhs);

    void setX(double x);
    void setY(double y);
    double getX() const;
    double getY() const;

  private:
    class PointImpl;
    PointImpl* pimpl;
}

In this kind of a case, the tradeoff starts to hit you because the pointer needs to be dereferenced, and the methods cannot be inlined. However, if you do it only for non-trivial classes then the slight overhead can typically be tolerated without any problems.

迷路的信 2024-07-26 04:37:04

pimpl ideom 的最大用途之一是创建稳定的 C++ ABI。 几乎每个 Qt 类都使用“D”指针,这是一种pimpl。 这允许在不破坏 ABI 的情况下执行更容易的更改。

One of the biggest uses of pimpl ideom is the creation of stable C++ ABI. Almost every Qt class uses "D" pointer that is kind of pimpl. This allows performing much easier changes withot breaking ABI.

以可爱出名 2024-07-26 04:37:04

代码清晰度

代码清晰度是非常主观的,但在我看来,具有单个数据成员的标头比具有许多数据成员的标头更具可读性。 然而,实现文件的噪音较大,因此清晰度会降低。 如果该类是基类,主要由派生类使用而不是维护,那么这可能不是问题。

可维护性

对于 pimpl 类的可维护性,我个人发现每次访问数据成员时额外的取消引用很乏味。 如果数据纯粹是私有的,则访问器无济于事,因为无论如何您都不应该为其公开访问器或修改器,并且您将不得不不断取消引用 pimpl。

对于派生类的可维护性,我发现该习惯用法在所有情况下都是纯粹的胜利,因为头文件列出了更少的不相关细节。 所有客户端编译单元的编译时间也得到了改进。

性能

在许多情况下,性能损失很小,但在少数情况下,性能损失却很严重。 从长远来看,它的数量级与虚拟函数的性能损失相当。 我们讨论的是每个数据成员的每次访问的额外取消引用,加上 pimpl 的动态内存分配,以及销毁时的内存释放。 如果 pimpl 类不经常访问其数据成员,则 pimpl 类对象会经常创建并且是短暂的,那么动态分配可能会超过额外的取消引用。

决定

我认为性能至关重要的类,例如一次额外的取消引用或内存分配会产生显着的差异,无论如何都不应该使用 pimpl。 如果编译时间显着改善,那么性能下降并不显着并且头文件被广泛 #include 的基类可能应该使用 pimpl。 如果编译时间没有减少,则取决于您的代码清晰度品味。

对于所有其他情况,这纯粹是一个品味问题。 在做出决定之前,先尝试一下并测量运行时性能和编译时性能。

Code Clarity

Code clarity is very subjective, but in my opinion a header that has a single data-member is much more readable than a header with many data-members. The implementation file however is noisier, so clarity is reduced there. That might not be an issue if the class is a base class, mostly used by derived classes rather than maintained.

Maintainability

For maintainability of the pimpl'd class I personally find the extra dereference in each access of a data-member tedious. Accessors can't help if the data is purely private because then you shouldn't expose an accessor or mutator for it anyway, and you're stuck with constantly dereferencing the pimpl.

For maintainability of derived classes I find the idiom is a pure win in all cases, because the header file lists fewer irrelevant details. Compile time is also improved for all client compilation units.

Performance

Performance loss is small in many cases and significant in few. In the long-run it is in the order of magnitude of virtual functions' performance loss. We're talking about an extra dereference per access per data-member, plus dynamic memory allocation for the pimpl, plus release of the memory on destruction. If the pimpl'd class doesn't access its data-members often, the pimpl'd class' objects are created often and are short-lived then dynamic allocation can out-weigh the extra-dereferences.

Decision

I think classes in which performance is crucial, such that one extra dereference or memory allocation makes a significant difference, shouldn't use the pimpl no matter what. Base classe in which this reduction in performance is insignificant and of which the header file is widely #include'd probably should use the pimpl if compilation time is improved significantly. If compilation time isn't reduced it's down to your code-clarity taste.

For all other cases it's purely a matter of taste. Try it and measure runtime performance and compile-time performance before you make a decision.

五里雾 2024-07-26 04:37:04

当您使用强异常保证来实现 std::swap 和 operator= 时,pImpl 非常有用。 我倾向于说,如果你的班级支持其中任何一个,并且有多个重要领域,那么它通常不再取决于偏好。

否则,这取决于您希望客户端通过头文件与实现绑定的紧密程度。 如果二进制不兼容的更改不是问题,那么您可能不会在可维护性方面受益匪浅,尽管如果编译速度成为问题,通常会节省一些费用。

性能成本可能更多地与内联损失有关,而不是与间接有关,但这是一个疯狂的猜测。

您可以稍后添加 pImpl,并声明从今天起客户端将不必仅仅因为您添加了私有字段而重新编译。

因此,这一切并不意味着采取全有或全无的方法。 您可以有选择地选择对您有利的课程,而不是对您没有好处的课程,然后再改变主意。 例如,将迭代器实现为 pImpl 听起来像是太多的设计......

pImpl is very useful when you come to implement std::swap and operator= with the strong exception guarantee. I'm inclined to say that if your class supports either of those, and has more than one non-trivial field, then it's usually no longer down to preference.

Otherwise, it's about how tightly you want clients to be bound to the implementation via the header file. If binary-incompatible changes aren't a problem, then you might not benefit much in maintainability, although if compile speed becomes an issue there are usually savings there.

The performance costs probably have more to do with loss of inlining than they do with indirection, but that's a wild guess.

You can always add pImpl later, and declare that from this day forth clients will not have to recompile just because you added a private field.

So none of this suggests an all-or-nothing approach. You can selectively do it for the classes where it gives you benefit, not for the ones it doesn't, and change your mind later. Implementing for example iterators as pImpl sounds like Too Much Design...

白鸥掠海 2024-07-26 04:37:04

这个习惯对于大型项目的编译时间有很大帮助。

外部链接

这也不错

This idiom helps greatly with compile time on large projects.

External link

This is good too

白日梦 2024-07-26 04:37:04

当我想避免头文件污染我的代码库时,我通常使用它。 Windows.h 就是一个完美的例子。 它的行为太恶劣了,我宁愿自杀也不愿让它到处可见。 因此,假设您想要一个基于类的 API,将其隐藏在 pimpl 类后面就可以巧妙地解决问题。 (如果您满足于仅公开单个函数,那么这些函数当然可以前向声明,而无需将它们放入 pimpl 类中)

我不会到处使用 pimpl,部分原因是性能之所以成功,部分原因是因为需要做大量的额外工作才能获得通常很小的好处。 它为您提供的主要功能是实现和接口之间的隔离。 通常,这并不是一个非常高的优先级。

I generally use it when I want to avoid a header file polluting my codebase. Windows.h is the perfect example. It is so badly behaved, I'd rather kill myself than have it visible everywhere. So assuming you want a class-based API, hiding it behind a pimpl class neatly solves the problem. (If you're content to just expose individual function, those can just be forward declared, of course, without putting them into a pimpl class)

I wouldn't use pimpl everywhere, partly because of the performance hit, and partly just because it's a lot of extra work for a usually small benefit. The main thing it gives you is isolation between implementation and interface. Usually, that's just not a very high priority.

梦里人 2024-07-26 04:37:04

我在自己的库中的几个地方使用了这个习惯用法,在这两种情况下都将接口与实现清晰地分开。 例如,我有一个在 .h 文件中完全声明的 XML 阅读器类,该类具有指向 RealXMLReader 类的 PIMPL,该类声明为 & 。 在非公共 .h 和 .cpp 文件中定义。 RealXMlReader 又是我使用的 XML 解析器(当前为 Expat)的便捷包装器。

这种安排允许我将来从 Expat 更改为另一个 XML 解析器,而不必重新编译所有客户端代码(当然我仍然需要重新链接)。

请注意,我这样做并不是出于编译时性能的原因,只是为了方便。 有一些 PIMPL fabnatics 坚持认为,任何包含三个以上文件的项目都将无法编译,除非您自始至终都使用 PIMPL。 值得注意的是,这些人从未拿出任何实际证据,而只是模糊地提到“Latkos”和“指数时间”。

I use the idiom in a couple of places in my own libraries, in both cases to cleanly split the interface from tthe implementation. I have, for example, an XML reader class fully declared in a .h file, which has a PIMPL to a RealXMLReader class which is declared & defined in non-public .h and .cpp files. The RealXMlReader in turn is a convenience wrapper for the XML parser I use (currently Expat).

This arrangement allows me to change from Expat in the future to another XML parser without having to recompile all the client code (I still need to re-link of course).

Note that I don't do this for compile-time performance reasons, only for conveniance. There are a few PIMPL fabnatics who insist that any project containing more than three files will be uncompilable unless you use PIMPLs throughout. It's noticeable that these people never produce any actual evidence, but only make vague references to "Latkos" and "exponential time".

王权女流氓 2024-07-26 04:37:04

当我们有右值语义时,pImpl 将工作得最好。

pImpl 的“替代方案”也将实现隐藏实现细节,即使用抽象基类并将实现放入派生类中。 用户调用某种“工厂”方法来创建实例,并且通常会使用指向抽象类的指针(可能是共享指针)。

pImpl 背后的基本原理可能是:

  • 节省 v 表。 是的,但是您的编译器会内联所有转发吗?您真的会保存任何内容吗?
  • 如果您的模块包含多个彼此详细了解的类,尽管对于外界您是隐藏的。

pImpl 容器类的语义可以是:
- 不可复制,不可分配...因此,您可以在构造时“新建”您的 pImpl,并在销毁时“删除”
- 共享。 因此,您拥有shared_ptr,而不是Impl*

使用shared_ptr,只要类在析构函数处完成,您就可以使用前向声明。 即使是默认的析构函数也应该被定义(它可能会是)。

  • 可交换。 您可以实现“可能为空”并实现“交换”。 用户可以创建一个实例,并向其传递一个非常量引用以使用“交换”来填充它。

  • 两阶段建设。 您构造一个空的,然后对其调用“load()”来填充它。

共享是唯一一个我什至有点喜欢但没有右值语义的东西。 有了它们我们还可以正确实现不可复制不可分配。 我喜欢能够调用一个给我一个函数。

然而,我发现我现在更倾向于使用抽象基类而不是 pImpl,即使只有一种实现也是如此。

pImpl will work best when we have r-value semantics.

The "alternative" to pImpl, that will also achieve hiding the implementation detail, is to use an abstract base class and put the implementation in a derived class. Users call some kind of "factory" method to create the instance and will generally use a pointer (probably a shared one) to the abstract class.

The rationale behind pImpl instead can be:

  • Saving on a v-table. Yes, but will your compiler inline all the forwarding and will you really save anything.
  • If your module contains multiple classes that know about each other in detail although to the outside world you hide that.

Semantics of the container class for the pImpl could be:
- Non-copyable, not assignable... So you "new" your pImpl on construction and "delete" on destruction
- shared. So you have shared_ptr rather than Impl*

With shared_ptr you can use a forward declaration as long as the class is complete at the point of the destructor. Your destructor should be defined even if default (which it probably will be).

  • swappable. You can implement "may be empty" and implements "swap". Users can create an instance of one and pass a non-const reference to it to get it populated, with a "swap".

  • 2-stage construction. You construct an empty one then call "load()" on it to populate it.

shared is the only one I have even a remote liking for without r-value semantics. With them we can also implement non-copyable non-assignable properly. I like to be able to call a function that gives me one.

I have, however, found I tend more now to use abstract base classes more than pImpl, even when there is only one implementation.

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