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

时间:2015-10-14 02:50:58

标签: c++ performance c++11 constructor delegating-constructor

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

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

  

这个[新的委托构造函数]有一个警告:C ++ 03认为在构造函数完成执行时要构造一个对象,但 C ++ 11认为构造的对象一旦任何构造函数完成执行。由于允许多个构造函数执行,这意味着每个委托构造函数将在其自己类型的完全构造的对象上执行。派生类构造函数将在其基类中的所有委托完成后执行。&# 34;

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

免责声明:我问了这个问题,因为我是一名学生,但迄今为止的答案都是不正确的,并证明缺乏对所引用研究的研究和/或理解。我对此感到有些沮丧,因此我的编辑和评论一直是匆匆而且很糟糕,主要是通过智能手机。请原谅这个;我希望我在下面的答案中将其最小化,并且我已经了解到我需要在评论中小心,完整和明确。

4 个答案:

答案 0 :(得分:5)

没有。它们是等价的。委托构造函数的行为类似于作用于由前一个构造函数构造的Object的普通成员函数。

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

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

  

如果对象的非委托构造函数已完成执行,并且该对象的委托构造函数以异常退出,则将调用该对象的析构函数。

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

答案 1 :(得分:3)

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

... 每个委托构造函数的函数体将在一个完全构造的对象上执行。

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

无论如何,由于只执行了一个初始化列表,因此没有您提到的这种开销。以下引自cppreference

  

如果类的名称本身在类中显示为class-or-identifier   成员初始化列表,然后列表必须包含该成员   仅初始化程序;这样的构造函数称为委托   构造函数,以及由唯一成员选择的构造函数   初始化列表是目标构造函数

     

在这种情况下,目标构造函数由重载选择   解析并先执行,然后控件返回到   委托构造函数及其正文被执行。

     

委托构造函数不能递归。

答案 2 :(得分:3)

C ++ 11中的链式委托构造函数会产生比C ++ 03 init函数样式更多的开销!

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

[text]和强调我的。

  

任何存储持续时间的对象,其初始化或销毁由异常终止,将为其所有完全构造的子对象执行析构函数...,即对于主构造函数(12.6.2)已完成的子对象执行和析构函数尚未开始执行。 类似地,如果对象的非委托构造函数已完成执行并且该对象的委托构造函数以异常退出,则将调用该对象[如上所述的子对象]析构函数。

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

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

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

  

如果本地对象具有析构函数,则必须将该析构函数作为堆栈展开的一部分进行调用。 [自动对象的堆栈展开的C ++扩展需要] ......一种实现技术(除了建立处理程序的标准开销之外)只涉及最小的开销。

这是您在代码链中为每个链接添加到代码中的非常实现的技术和开销。每个范围都有可能发生异常,并且每个构造函数都有自己的范围,所以每个链中的构造函数会增加开销(与仅引入一个额外范围的init函数相比)。

确实,开销很小,而且我确信理智的实现可以优化简单的情况以消除这种开销。但是,考虑一下你有一个5类继承链的情况。假设这些类中的每一个都有5个构造函数,并且在每个类中,这些构造函数在链中相互调用以减少冗余编码。如果您实例化最派生类的实例,则会产生上述开销,直到 25 次,而C ++ 03版本会产生高达 10 <的开销。 / strong>次。如果将这些类设置为虚拟并且多次继承,则这些开销将增加与这些功能的累积相关,以及这些功能本身会带来额外的开销。这里的道德观点是,随着你的代码的扩展,你会感受到这个新特征的叮咬。

* Stroustrup引用是很久以前编写的,用于激发对C ++异常处理的讨论,并定义了潜在的(不一定是)C ++语言特性。我选择了这个引用而不是某些特定于实现的引用,因为它是人类可读的,并且是“可移植的本文的核心用途是第5节:特别讨论了对C ++堆栈展开的需求,以及它开销的必要性。这些概念在本文中是合法的,并且今天对C ++ 11有效。

答案 3 :(得分:1)

开销是可测量的。我用main类实现了以下Player函数,并使用委托的构造函数以及带有init函数的构造函数(注释了)将其运行了好几次。我使用g ++ 7.5.0和不同的优化级别构建了代码。

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

我每个程序运行了五次,并在2.50GHz的Intel®CoreTM i5-7200U CPU上计算了平均值

运行时间(以毫秒为单位):

优化级别|委托|初始化

-O0 | 40966 | 26855

-O2 | 21868 | 10965

-O3 | 6475 | 5242

-Ofast | 6272 | 5123

要建造50,000个!对象可能不是通常的情况,但是委托构造函数会产生开销,这就是问题所在。

#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;
}