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

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

本文关键字:函数 c++ init 性能 调用 是否      更新时间:2023-10-16

[这个问题经过高度编辑;对不起,我已经把编辑移到了下面的答案中]

来自维基百科(包含子条目)关于c++ 11:

这个[新的委托构造函数特性]附带了一个警告:c++ 03认为对象在其构造函数完成执行时被构造,但是 c++ 11认为对象在任何构造函数完成执行时被构造。由于允许执行多个构造函数,这意味着每个委托构造函数将在其自己类型的完全构造对象上执行。派生类构造函数将在其基类中的所有委托完成后执行。"

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

免责声明:我问这个问题,因为我是一个学生,但到目前为止的答案都是不正确的,并且表明缺乏研究和/或对参考研究的理解。对此我感到有些沮丧,因此我的编辑和评论都是仓促而拙劣的,而且大多是通过智能手机进行的。请原谅;我希望我在下面的回答中已经尽量减少了这种情况,并且我已经学会了在我的评论中要小心、完整和清晰。

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

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

在Section 4.3 -§15的修改中,对标准的修改建议如下:

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

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

c++ 11中的链式委托构造函数确实比c++ 03的init函数风格产生更多的开销!

参见c++ 11标准草案N3242, section 15.2。异常可能发生在委托链中的任何链接的执行块中,c++ 11扩展了现有的异常处理行为来解决这个问题。

[文本]

强调

任何存储期限的对象,如果其初始化或销毁被异常终止,则将为其所有完整构造的子对象执行析构函数…也就是说,对于主构造函数(12.6.2)已经完成执行而析构函数还没有开始执行的子对象。类似地,如果对象的非委托构造函数已经完成执行,并且该对象的委托构造函数以异常退出,则该对象的析构函数将被调用。

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

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

它的要点是:在堆栈上声明的对象被分配到内存中,可执行程序为您处理寻址和清理。栈的实现在C中很简单,但是在c++中,我们有异常,它们需要扩展C的栈展开。Stroustrup*的一篇论文的第5节讨论了扩展堆栈展开的必要性,以及这种特性引入的必要的额外开销:

如果局部对象具有析构函数,则必须在堆栈展开时调用该析构函数。[自动对象的堆栈展开的c++扩展需要]…一种实现技术(除了建立处理程序的标准开销之外)只涉及最小的开销。

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

的确,开销是最小的,而且我确信合理的实现优化了简单的情况来消除开销。但是,考虑一个有5个类继承链的情况。假设每个类都有5个构造函数,在每个类中,这些构造函数在一个链中相互调用以减少冗余编码。如果您实例化了最派生类的实例,那么您将导致高达25次的上述开销,而c++ 03版本将导致高达10次的开销。如果您将这些类设置为虚类并进行多重继承,则该开销将随着这些特性的积累以及这些特性本身引入的额外开销而增加。这里的寓意是,随着代码的扩展,您将感受到这个新特性的影响。

* Stroustrup参考是很久以前编写的,目的是激发对c++异常处理的讨论,并定义了潜在的(不一定)c++语言特性。我选择这个参考而不是一些特定于实现的参考,因为它是人类可读的,并且是"可移植的"。本文的核心用途是第5节:具体讨论了c++堆栈展开的必要性,以及它的开销发生的必要性。

这些概念在本文中是合法的,并且在c++ 11中仍然有效。

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

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

然而,委托构造函数只能部分构造对象,并且只能被其他构造函数调用,而不是单独使用。这样的构造函数通常声明为private。因此,在委托构造函数执行后,认为对象已完全构造可能并不总是合适的。

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

如果类本身的名称在成员初始化项列表,则该列表必须包含该成员初始化;这样的构造函数称为委托类的唯一成员选择的构造函数初始化列表是目标构造函数

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

委托构造函数不能是递归的。

开销是可测量的。我用Player类实现了下面的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上的平均值

msec:

Opt-Level | delegation | init

- 0 | 40966 | 26855

-O2 | 21868 | 10965

- 3 | 6475 | 5242

-Ofast | 6272 | 5123

建设50000 !对象可能不是常见的情况,但是委托构造函数有一个开销,这就是问题所在。

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