C++11 中的链式委托构造函数确实比 C++03 的 init 函数样式产生更多的开销!
请参阅 C++11 标准草案N3242,第 15.2 节。委托链中任何环节的执行块都可能发生异常,C++11 扩展了现有的异常处理行为来解决这个问题。
[文本]并强调我的。
任何存储持续时间的对象,其初始化或销毁被异常终止,都将为其所有完全构造的子对象执行析构函数...,即,对于主体构造函数 (12.6.2) 已完成执行的子对象,并且析构函数还没有开始执行。类似地,如果对象的非委托构造函数已完成执行,并且该对象的委托构造函数因异常退出,则将调用该对象的[像上面的子对象一样处理]的析构函数。
这是描述委托 ctor 与 C++ 对象堆栈模型的一致性,这必然会引入开销。
我必须熟悉诸如堆栈在硬件级别上的工作原理、堆栈指针是什么、自动对象是什么以及堆栈展开是什么,才能真正理解它是如何工作的。从技术上讲,这些术语/概念是实现定义的细节,因此 N3242 没有定义这些术语中的任何一个;但它确实使用它们。
它的要点:在堆栈上声明的对象被分配到内存中,并且可执行文件为您处理寻址和清理。堆栈的实现在 C 中很简单,但在 C++ 中,我们有异常,它们需要扩展 C 的堆栈展开。Stroustrup * 的一篇论文的第 5 节讨论了扩展堆栈展开的必要性,以及此类功能引入的必要额外开销:
如果本地对象具有析构函数,则必须调用该析构函数作为堆栈展开的一部分。[用于自动对象的堆栈展开的 C++ 扩展需要] ...一种实现技术(除了建立处理程序的标准开销之外)仅涉及最小开销。
您为委托链中的每个链接添加到代码中的正是这种实现技术和开销。每个作用域都有可能发生异常,每个构造函数都有自己的作用域,因此链中的每个构造函数都会增加开销(与只引入一个额外作用域的 init 函数相比)。
确实,开销是最小的,我确信理智的实现会优化简单的案例来消除开销。但是,考虑一个有 5 个类继承链的情况。假设这些类中的每一个都有 5 个构造函数,并且在每个类中,这些构造函数在链中相互调用以减少冗余编码。如果您实例化最派生类的实例,您将产生高达25次上述开销,而 C++03 版本将产生高达10次的开销次。如果您将这些类设为虚拟并进行多重继承,那么与这些特性的累积相关的开销将会增加,并且这些特性本身也会引入额外的开销。这里的寓意是,随着您的代码扩展,您将感受到这个新功能的影响。
* Stroustrup 参考是很久以前写的,目的是激发关于 C++ 异常处理的讨论并定义潜在的(不一定)C++ 语言特性。我选择了这个参考而不是一些特定于实现的参考,因为它是人类可读的,并且是“便携的”。我对本文的核心用途是第 5 节:专门讨论 C++ 堆栈展开的必要性,以及产生开销的必要性。这些概念在论文中是合法的,并且在今天对 C++11 有效。