复制初始化与直接初始化发生了变化吗?

JDł*_*osz 2 c++ initialization

我很久以前就了解到,复制初始化会创建一个临时对象,然后用于初始化目标,尽管后者的复制构造函数已被优化掉;但编译器仍然假装使用它,检查是否存在并允许访问。

\n\n

我注意到 Herb Sutter 在更新的 GOTW 帖子中提到,auto使用时这不再是正确的。

\n\n

具体来说,Herb(2013 年撰写)仍然阐述了一般熟悉的规则:

\n\n
\n

如果x是其他类型,从概念上讲,编译器首先将 x 隐式转换为临时小部件对象\xe2\x80\xa6 请注意,我在上面多次提到过 \xe2\x80\x9c 概念上\xe2\x80\x9d。\xe2\x80\x99s 因为实际上编译器被允许并且通常会优化来自GOTW #1 的临时 \xe2\x80\x94

\n
\n\n

他后来指出,当auto使用时(在 中auto w = x;),仅调用一个复制构造函数,因为没有x需要首先转换的方法。

\n\n

如果函数返回迭代器(因此 it\xe2\x80\x99 是一个右值):

\n\n
\n

毕竟,正如我们在 GotW #1 中看到的,通常额外=意味着两步 \xe2\x80\x9c 转换为临时的然后复制/移动 \xe2\x80\x9d 的复制初始化 \xe2\x80\x94 但请记住auto像这样使用时,\xe2\x80\x99t 不适用。\xe2\x80\xa6 急!不需要任何转换,我们直接构造i. \xe2\x80\x94 GOTW #2(强调我的)

\n
\n\n

如果从函数返回的类型与通过复制初始化初始化的变量的类型完全相同,则规则是临时变量被优化,但它仍然检查复制构造函数的访问。Herb 表示, 的情况并非如此auto,而是使用了直接初始化。

\n\n

还有其他一些例子,他似乎在说(尽管不是很精确)当使用auto编译器不再假装使用复制构造函数。

\n\n

\xe2\x80\x99s 发生了什么?规则改变了吗?auto这是所有演示都没有提及的附加功能吗?

\n

JDł*_*osz 7

auto我检查了 C++17 标准 (n4659),在初始化部分中找不到任何特别提及的内容,在auto. 因此,我回到基础并详细阅读了初始化规则。男孩,它改变了吗! 在 C++17 中,复制初始化的含义不是您之前学到的。这里关于 SO 的许多答案,以及解释如何需要复制构造函数但随后进行优化的教程都已过时,而且现在是错误的。

\n\n

C++17 改变了临时变量的处理方式,以支持更多的复制省略并提供一种解释强制复制省略的方法。简而言之,纯右值不是可以被优化掉的临时值,但仍然会在逻辑上复制(或移动)到它们的最终位置。相反,纯右值有点悬而未决,没有地址,也没有真正的存在。如果确实需要临时文件,则为 \xe2\x80\x9cmaterialized\xe2\x80\x9d。但这里的想法是纯右值可以折叠起来,并且创建值的代码(例如语句return)可以与最终目标匹配,从而避免创建临时目标。

\n\n

您可以看到,这直接影响了初始化的含义,并且本身就删除了有关复制初始化中的临时的旧规则。

\n\n

所以这里\xe2\x80\x99s 是这样的:

\n\n

新的初始化规则

\n\n

直接初始化与复制初始化

\n\n

此描述涵盖了定义类类型的对象的情况。也就是说,不是引用,不是原始类型等。为了使描述简单,我\xe2\x80\x99m 没有在提到复制构造函数的每个地方都提到移动构造函数。

\n\n
C c1 ( exp() );  //exp returns a value, not a reference.\nC c2 (v);\nC c3 = exp();\nC c4 = v;\n
Run Code Online (Sandbox Code Playgroud)\n\n

我们有直接初始化(c1c2)和复制初始化(c3c4)。列表形式将单独介绍。

\n\n

以下情况按优先顺序排列。每条规则的描述均假设较早的规则尚未匹配。

\n\n

纯右值

\n\n

如果源是纯右值(prvalue)并且已经是正确的类型,我们就会得到创建值的位置和最终目的地之间的匹配。也就是说,在c1and 中, in 语句c3创建的值将直接在变量中创建。在底层机器语言中,调用者确定返回值的去向并将其作为另一个参数或专用寄存器或其他内容传递。在此,为此目的传递(或) 将要去的地址。它总是这样做(在优化的代码中);但从逻辑上讲,调用者设置了一个临时文件,然后将临时文件复制到,但允许优化该副本。现在,暂时没有了。从函数内部的值创建到调用方的声明变量的直接管道是规范的一部分。returnexpexpc1c3c3

\n\n

请注意,这一新现实总体上影响纯右值。纯右值(prvalue)是函数按值返回对象时所拥有的。解释这种情况的段落(\xc2\xa711.6 \xc2\xb617.6.1甚至没有提到复制与直接形式!

\n\n

这意味着不涉及复制构造函数。假设该函数有某种方法来创建对象(不同的构造函数或私有访问),那么当您根本没有可访问的复制构造函数时,您可以创建一个变量。这对于工厂来说非常方便,因为您想要控制对象的创建方式并且类型不可复制且不可移动。

\n\n

直接,有时复制

\n\n

在直接初始化(c2)中,参数用于调用构造函数。考虑直接、显式构造函数。这应该足够熟悉了。

\n\n

在复制初始化中,如果该值与目标类相同甚至是派生类,则该值将用作构造函数的参数。这就是is 也是 class 的c4地方,或者是派生自so it is-a 的类。我们假设这将是\xe2\x80\x99s 选择的复制构造函数,但您可以为派生类型使用特殊的构造函数。作为复制初始化,显式构造函数将被忽略。无论如何,复制构造函数不能是显式的。但你可以明确\xe2\x80\x99s!vCDC CC::C(const D&)

\n\n

复制

\n\n

在复制初始化的其余情况下,发现了一些转换。因为c4假设v是类型E。转换函数是 E 中定义的转换运算符(例如E::operator C())和 C 中的非显式构造函数(例如C::C(const E&))。然后将转换结果用于直接初始化。这听起来很熟悉\xe2\x80\xa6,但这里是棘手的部分:转换函数可能会产生纯右值!普通的转换运算符将按值返回,因此该运算符的返回值直接在 中构造c4。可以编写一个返回引用的转换运算符,因此是一个左值。但转换构造函数始终是纯右值。因此,如果使用构造函数则复制初始化与直接初始化一样直接。但现在它\xe2\x80\x99是一个幻影右值,而不是一个临时值,它消失了。

\n\n

列表初始化

\n\n
C c5 { v1,foo(),7 };\nC c6 = { v1,foo(),7 };\nC c7 = {exp()};\n
Run Code Online (Sandbox Code Playgroud)\n\n

首先,如果C有一个特殊的初始化列表的构造函数,那么就使用它。常规的东西都不适用。

\n\n

否则,它\xe2\x80\x99 与以前几乎相同。有一些差异(\xc2\xa711.6.4 \xc2\xb63.6):

\n\n

由于新语法,可以在复制初始化形式中为构造函数指定多个参数。例如,c5c6都指定相同的构造函数参数。在复制初始化中,不允许使用显式构造函数。请注意,我写了disallowed notignored 在常规复制初始化中,显式构造函数被忽略,重载解析仅使用非显式形式。但在复制列表初始化中,所有构造函数都用于重载解析,但如果选择显式构造函数,则会出现错误(\xc2\xa716.3.1.7)。

\n\n

另一个区别是,使用列表语法时,如果在隐式转换中使用缩小转换 ( \xc2\xa711.6.4 \xc2\xb67 ),则会将其标记为错误。

\n\n

现在有一个大惊喜:临时回来了!在 case 中c7,即使exp()是纯右值,规则也是将其与构造函数参数相匹配。因此,表达式生成了一个类型的值C,但随后该值用于选择构造函数(这将是复制构造函数)。由于复制构造函数不能是显式的,因此直接语法和复制语法之间没有区别。

\n\n

一个普通的程序员需要知道什么?

\n\n

这相当复杂,但普通的日常编码人员只需要了解一些简单的规则。

\n\n
    \n
  • 纯右值直接通过管道传送到新家。所以细节发生在函数返回中,并且这里确实没有\xe2\x80\x99t发生任何事情。因此,复制语法和直接语法之间没有任何有意义的区别。
  • \n
  • 复制初始化与直接初始化会影响explicit构造函数。
  • \n
  • 直接初始化特别需要一个构造函数;复制初始化可以使用任何转换方式。
  • \n
\n\n

对于列表:

\n\n
    \n
  • 列表初始化可以使用特殊的列表构造函数(例如std::vector)。
  • \n
  • 列表初始化提供构造函数参数,但不允许\xe2\x80\x99t缩小转换
  • \n
\n\n

所以,有\xe2\x80\x99s关于列表的新部分。但实际上,它\xe2\x80\x99 是你需要忘记的东西:忘记转换然后(假装)复制业务。

\n