C++11 委托的 ctors 是否比调用 init 函数的 C++03 ctors 表现更差?

Do C++11 delegated ctors perform worse than C++03 ctors calling init functions?

[这个问题已经过高度编辑;请原谅,我已将编辑内容移至下面的答案中]

来自 C++11 上的 Wikipedia (subarticle included)

This [new delegating constructors feature] comes with a caveat: C++03 considers an object to be constructed when its constructor finishes executing, but C++11 considers an object constructed once any constructor finishes execution. Since multiple constructors will be allowed to execute, this will mean that each delegating constructor will be executing on a fully constructed object of its own type. Derived class constructors will execute after all delegation in their base classes is complete."

这是否意味着委托链为 ctor 委托链中的每个 link 构造一个唯一的临时对象?这种开销只是为了避免简单的 init 函数定义,不值得额外的开销。

免责声明:我问这个问题是因为我是一名学生,但到目前为止的答案都是不正确的,并且表明对所引用的研究缺乏研究 and/or 理解。我对此感到有些沮丧,因此我的编辑和评论都是仓促而草率的,主要是过于聪明 phone。请原谅;我希望我在下面的回答中尽量减少了这一点,并且我了解到我在评论中需要谨慎、完整和清晰。

没有。它们是等价的。委托构造函数的行为就像一个普通的成员函数,作用于由前一个构造函数构造的对象。

我在 proposal for adding delegating constructors 中找不到任何明确支持这一点的信息,但在一般情况下创建副本是不可能的。有些 类 可能没有复制构造函数。

在第 4.3 节 - 对 §15 的更改中,对标准的拟议更改指出:

if the non‐delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked.

这意味着委托构造函数在一个完全构造的对象上工作(取决于您如何定义它)并允许实现让委托 ctors 像成员函数一样工作。

Class 构造函数有两部分,一个成员初始化列表和一个函数体。使用构造函数委托,首先执行委托(目标)构造函数的初始化列表和函数体。之后,委托构造函数的函数体被执行。在某些情况下,当初始化列表和某些构造函数的函数体都被执行时,您可以认为一个对象被完全构造。 这就是为什么 wiki 说 每个委托构造函数将在其自己类型的完全构造的对象上执行 。其实语义可以更准确的描述为:

... 的函数体每个委托构造函数将在其自身类型的完全构造的对象上执行。

但是,委托构造函数只能部分构造对象,并且被设计为仅被其他构造函数调用而不是单独使用。这样的构造函数通常被声明为私有的。因此,在执行委托构造函数之后考虑对象完全构造可能并不总是合适的。

无论如何,因为只执行了一个初始化列表,所以没有你提到的开销。以下引自cppreference:

If the name of the class itself appears as class-or-identifier in the member initializer list, then the list must consist of that one member initializer only; such constructor is known as the delegating constructor, and the constructor selected by the only member of the initializer list is the target constructor

In this case, the target constructor is selected by overload resolution and executed first, then the control returns to the delegating constructor and its body is executed.

Delegating constructors cannot be recursive.

C++11 中的链式委托构造函数确实比 C++03 初始化函数样式产生更多的开销!

参见 C++11 标准草案 N3242,第 15.2 节。委托链中任何 link 的执行块中都可能发生异常,C++11 扩展了现有的异常处理行为以解决该问题。

[文字]和强调我的。

An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects ..., that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s [treated like a subobject as above] destructor will be invoked.

这是在描述委托 ctors 与 C++ 对象堆栈模型的一致性,这必然会引入开销。

我必须熟悉堆栈在硬件级别上的工作方式、堆栈指针是什么、自动对象是什么以及堆栈展开是什么,才能真正理解它是如何工作的。从技术上讲,这些 terms/concepts 是实现定义的细节,因此 N3242 没有定义任何这些术语;但它确实使用了它们。

要点:在堆栈上声明的对象被分配到内存中,可执行文件为您处理寻址和清理。堆栈的实现在 C 中很简单,但在 C++ 中,我们有例外,它们需要扩展 C 的堆栈展开。 a paper by Stroustrup* 的第 5 节讨论了扩展堆栈展开的需要,以及此类功能引入的必要额外开销:

If a local object has a destructor, that destructor must be called as part of the stack unwinding. [A C++ extension of stack unwinding for automatic objects requires] ...an implementation technique that (in addition to the standard overhead of establishing a handler) involves only minimal overhead.

您为委托链中的 每个 link 添加到代码中的正是这种实现技术和开销。 每个范围都有可能出现异常,并且每个构造函数都有自己的作用域,因此链中的每个构造函数都会增加开销(与只引入一个额外作用域的 init 函数相比)。

确实开销很小,而且我确信理智的实现会优化简单的情况以消除该开销。但是,请考虑您拥有 5 class 继承链的情况。假设这些 classes 中的每一个都有 5 个构造函数,并且在每个 class 中,这些构造函数在链中相互调用以减少冗余编码。如果您实例化最派生的实例 class,您将招致上述开销高达 25 次,而 C++03 版本会招致该开销最多 10 次。如果你使这些 classes 虚拟化并多重继承,这种开销将随着这些特性的积累而增加,以及这些特性本身引入额外的开销。这里的寓意是,随着代码的扩展,您会感受到这个新功能的好处。

*Stroustrup 参考资料是很久以前写的,旨在激发对 C++ 异常处理的讨论并定义潜在的(不一定)C++ 语言功能。我选择这个参考而不是一些特定于实现的参考,因为它是人类可读的,并且 'portable.' 我对这篇论文的核心用途是第 5 部分:特别是讨论 C++ 堆栈展开的必要性,以及它的开销发生的必要性。这些概念在论文中被合法化,并且今天对 C++11 有效。

开销是可以衡量的。我使用 Player-class 和 运行 实现了以下 main-函数,它多次使用委托构造函数以及带有 init 函数的构造函数(注释掉).我使用 g++ 7.5.0 和不同的优化级别构建了代码。

构建命令:g++ -Ox main.cpp -s -o file_g++_Ox_(init|delegating).out

我 运行 每个程序五次并在 Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz

上计算平均值

运行时间(毫秒):

选择级别 |委派|初始化

-O0 | 40966 | 26855

-O2 | 21868 | 10965

-O3 | 6475 | 5242

-Ofast | 6272 | 5123

建设五万! objects 可能不是通常的情况,但是委托构造函数有开销,这就是问题所在。

#include <chrono>

class Player
{
private:
    std::string name;
    int health;
    int xp;
public:
    Player();
    Player(std::string name_val, int health_val, int xp_val);
};

Player::Player()
    :Player("None", 0,0){
}

//Player::Player()
//        :name{"None"}, health{0},xp{0}{
//}

Player::Player(std::string name_val, int health_val, int xp_val)
    :name{name_val}, health{health_val},xp{xp_val}{

}

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 50000; i++){
        Player player[i];
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( end - start ).count();

    std::cout << duration;

    return 0;
}