11

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

来自C++11 上的Wikipedia(包括子条目) :

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

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

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

4

4 回答 4

4

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

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

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

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

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

于 2015-10-14T02:54:34.097 回答
3

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 有效。

于 2015-10-16T01:52:15.200 回答
2

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

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

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

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

如果类本身的名称在成员初始值设定项列表中显示为类或标识符,则该列表必须仅包含一个成员初始值设定项;这样的构造函数称为委托构造函数,由初始化列表的唯一成员选择的构造函数是目标构造函数

在这种情况下,通过重载决议选择目标构造函数并首先执行,然后控制返回到委托构造函数并执行其主体。

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

于 2015-10-14T03:14:27.887 回答
1

开销是可衡量的。我使用 -class 实现了以下main-function 并Player使用委托构造函数以及带有 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

建造50,000!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;
}
于 2020-03-22T12:52:02.297 回答