【问题标题】:Do C++11 delegated ctors perform worse than C++03 ctors calling init functions?C++11 委托 ctor 的性能是否比 C++03 调用 init 函数的 ctor 差?
【发布时间】:2020-03-27 23:11:37
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • “委托链真的为 ctor 委托链中的每个链接构造一个唯一的临时对象吗?” 我不太明白你是如何从维基百科文章中推断出来的。
  • 我在编辑我的问题时解决了这个问题,并在此处删除了我多余的(措辞不佳的)cmets。一探究竟。 +1 指出需要清晰!
  • 我是否可以断定这条评论"Clarifying the rest: the implementation may just write on the same stack address (no ctors required, that was a blunder)" 从本质上改变了问题?如果我理解正确的话,它似乎消除了“委托链为 ctor 委托链中的每个链接构造一个唯一的临时对象”的有争议的说法。
  • 我是个聪明人。老实说,我不知道任何特定的实现会做什么或不做什么来优化某些情况。我所知道的是,该标准在成员对象如何在破坏对象中被破坏以及由构造函数创建的对象如何在委托构造函数调用中被破坏之间进行了类比。也就是说,后面的对象被当作成员对象对待。我说的是来自 N3242 的 15.2。我在下面引用了它,找到那个方法会快一点。

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


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 你能提供一个来源吗?编辑:不是维基百科来源它的说法,但我想确定,谢谢你的回答
  • 找不到源 - 也许它是实现定义的,但没有理由以这种方式实现它。我做了一些修改,可能涵盖了您的查询
  • 什么?不,这并不意味着。它明确指出相反的情况。它表明一个对象已经完成构造并且它的析构函数被调用。你真的认为如果是这样的话,他们不会只是写它被视为一个成员函数......是的,你可能没有一个复制 ctor,但你不需要一个。堆栈指针在整个 ctor 调用中都不会改变。对象是默认移动构造的,覆盖堆栈。大概..
  • 其实这也可能不是真的。 ctor 作用于自己类型的对象,因此如果存在任何继承,则不能使用相同的堆栈块,因为基数和派生数可能大小不同。
  • 移动构造函数也可以被删除或不实现。对面什么?它似乎将实现留给编译器。 “堆栈指针没有改变”意味着它是同一个对象 - 没有副本
【解决方案2】:

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

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

[文本] 和 强调 我的。

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

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

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

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

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

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

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

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

【讨论】:

  • 感谢您指出这一点。我改写了那段以表达我的意图:在我研究这些东西之前,我肯定不知道这些东西,我需要掌握它们才能理解标准在说什么。我会假设那里还有其他一些像我一样的想象中的人:P
  • 谢谢。我不确定我能想出一个例子来说明“你会感受到这个新功能的影响”。你能想出一个吗(我打赌编译器可以轻松地使用隐式 noexcept-ness 和空构造函数体。事实上,构造函数内联并且可能有针对嵌套异常处理范围的特定优化。我对“咬”持怀疑态度,除非我们假设Hell++)
  • 异常处理的现代实现通常使用基于表的方法,在非异常代码路径上具有零(直接)运行时开销。此外,我同意 sehe 可以应用内联和函数间优化;通常,一个类的所有构造函数无论如何都定义在同一个文件中。如果您声称存在实际开销而不仅仅是开销的可能性,请提供示例和测量值。
【解决方案3】:

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

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

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

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

如果类本身的名称在 成员初始值设定项列表,则该列表必须由该成员组成 仅初始化器;这样的构造函数被称为委托 构造函数,以及由唯一成员选择的构造函数 初始化列表是目标构造函数

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

委托构造函数不能递归。

【讨论】:

  • 我认为“完全构建”的部分具有误导性。在最后一个构造函数完成之前,总对象不会完全构造。每个委托的构造函数都在构造整个对象的一部分。委托构造函数在委托构造函数首先完成构造它的部分之后构造它的对象部分。那么当委托的构造函数被执行时,什么是“完全构造的”呢?也许引用实际上意味着“完全分配等待被构造”?
  • @RemyLebeau 同意。委托构造函数只能部分构造对象,并且被设计为仅由其他构造函数调用而不是单独使用。这样的构造函数可以声明为私有的。会更新答案。感谢 cmets。
  • 如果您阅读我引用的原始提案,您会发现实际发生的是每个委托的 ctor 创建一个完整的对象,因此 ctor 执行块中的异常将破坏最后创建的对象。
  • 标准中的生命周期、销毁和初始化之间存在差异。我可以在 C++11 IS 中找到的唯一类似于“完全构造”的定义是 [except.ctor]p2 "[...] 将为其所有完全构造的子对象执行析构函数 [. ..],也就是说,对于主要构造函数已完成执行而析构函数尚未开始执行的子对象。” OTOH,同一段落指出当委托 ctor 通过异常退出时调用 dtor。抄送@RemyLebeau
  • @dyp。这意味着如果构造函数抛出,当且仅当至少一个构造函数完成时,才会调用析构函数。
【解决方案4】:

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

【讨论】:

  • 你能不能同时记下你的 CPU,完整的编译命令,以及你在不同的优化级别下得到了什么?
  • 我不确定这个答案是否适合这个问题。这个答案适用于特定的编译器。问题似乎集中在语言是否需要额外开销,而不是某些实现是否导致它。某些编译器没有优化委托构造函数以及另一个与特性本身不同导致开销。此外,如果您要比较委托构造函数,则需要将其与等效情况进行比较。也就是说,当你的构造函数显式调用某个函数来填充成员时。
  • 在委托构造函数的情况下,您构造 std::string 两次,这会增加您观察到的开销。
猜你喜欢
  • 2016-07-25
  • 2012-07-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-29
  • 2010-09-17
相关资源
最近更新 更多