很久以前,我了解到,复制初始化会创建一个临时文件,然后用于初始化目标,尽管后一个复制构造函数已经过优化;但编译器仍假装使用它,检查是否存在并允许访问。
我注意到Herb Sutter在updated GOTW posts中提到,当使用auto
时,这已不再适用。
具体而言,赫伯(2013年撰写)仍然陈述了一般熟悉的规则:
如果
x
属于其他类型,概念上编译器会先隐式地将x转换为临时小部件对象...请注意,我在概念上说过几次。这是因为实际上编译器被允许并且通常会优化掉临时 - 来自GOTW #1
他后来指出,当使用auto
时(在auto w = x;
中)只调用一个复制构造函数,因为无法首先转换x
。
在函数返回迭代器的情况下(所以它是一个右值):
毕竟,正如我们在GotW#1中所看到的那样,通常额外的
=
意味着复制初始化的两步“转换为临时然后复制/移动”,但回想一下在使用auto
这样的时适用。 ... presto!不需要转换,我们直接构建i
。 - GOTW #2(强调我的)
如果从函数返回的类型与使用复制初始化初始化的变量完全相同,则规则是临时优化,但它仍然检查复制构造函数上的访问。 Herb说auto
不是这种情况,而是使用直接初始化。
还有其他一些例子,他似乎在说(虽然没有严格的精确度),当使用auto
时,你不再让编译器假装使用复制构造函数。
发生了什么事?规则有变化吗?这是auto
的一些额外功能,所有的演示文稿都没有提及吗?
答案 0 :(得分:1)
我检查了C ++ 17标准(n4659),在初始化部分找不到auto
的特别提及,也没有在auto
部分有关初始化的任何内容。所以,我回到基础并详细阅读初始化规则。而男孩,它改变了!在C ++ 17中,复制初始化的含义不是你之前学到的。这里有很多关于SO的答案,以及解释如何复制构造函数然后进行优化的教程都已经过时了,现在已经错了。
C ++ 17改变了临时处理的方式,以支持更多的复制省略并启用一种方法来解释强制复制省略。简而言之,prvalues不是可能被优化的临时值,但仍然在逻辑上被复制(或移动)到他们的最终家园。相反,prvalues有点不明确,没有地址,也没有真正的存在。如果实际需要临时性,则一个是“物化的”。但这里的想法是prvalue可以折叠掉,创建值的代码(例如return
语句)可以与最终目标匹配,从而避免创建临时值。
您可以看到这直接影响初始化的含义,并且本身会删除有关临时复制初始化的旧规则。
所以这是交易:
此描述涵盖了定义类类型对象的情况。也就是说,不是引用,不是原始类型等。为了使描述简单,我没有提到每个提到复制构造函数的移动构造函数。
C c1 ( exp() ); //exp returns a value, not a reference.
C c2 (v);
C c3 = exp();
C c4 = v;
我们有直接初始化(c1
和c2
)和复制初始化(c3
和c4
)。列表表格将单独列出。
以下情况按优先顺序列出。假设先前的规则已经不匹配,将描述每个规则。
如果源是prvalue(纯rvalue)并且已经是正确的类型,我们将获得创建值的位置和最终目标之间的匹配。也就是说,在c1
和c3
中,return
中exp
语句创建的值将直接在变量中创建。在底层机器语言中,调用者确定返回值的去向,并将其作为另一个参数或专用寄存器或其他内容传递给exp
。在此,c1
(或c3
)的地址将用于此目的。它始终如此(在优化的代码中);但逻辑调用者设置临时,然后将临时复制到c3
,但允许优化副本。现在,没有暂时的。从函数内部的值创建到调用者中声明的变量的直接管道是规范的一部分。
请注意,这个新现实通常会影响prvalues。 prvalue(纯rvalue)是函数按值返回对象时的值。解释此案例(§11.6 ¶17.6.1)的段落甚至没有提到复制与直接表格!
这意味着不涉及复制构造函数。假设函数有某种方式来创建对象(不同的构造函数或私有访问),则可以在没有可访问的复制构造函数时创建变量。这对于工厂来说非常方便,您可以在这些工厂中控制对象的创建方式,并且类型不可复制且不可移动。
在直接初始化(c2
)中,参数用于调用构造函数。直接,明确的构造函数被考虑。这应该是足够熟悉的。
在复制初始化中,如果该值与目标或甚至派生类的类相同,则该值将用作构造函数的参数。那是c4
其中v
也是类C
,或类D
派生自C
所以是-a { {1}}。我们假设这将是所选择的复制构造函数,但是您可以为派生类型使用特殊的构造函数。作为复制初始化,忽略显式构造函数。无论如何,复制构造函数不能是显式的。但你可以C
明确!
在剩余的复制初始化情况下,会发现一些转换。对于C::C(const D&)
,假设c4
的类型为v
。转换函数是E中定义的转换运算符(例如E
)和C中的非显式构造函数(例如E::operator C()
)。然后将转换结果与直接初始化一起使用。这听起来很熟悉......但这里有一个棘手的部分:转换函数可能产生一个prvalue!普通转换运算符将按值返回,因此该运算符的返回值直接在C::C(const E&)
中构造。可以编写一个返回引用的转换运算符,因此是一个左值。但转换构造函数始终是prvalues。因此,出现如果使用构造函数,那么复制初始化就像直接初始化一样直接。但现在它是一个幻影的prvalue,而不是一个临时的,消失了。
c4
首先,如果C c5 { v1,foo(),7 };
C c6 = { v1,foo(),7 };
C c7 = {exp()};
有一个特殊的初始化列表构造函数,那么就使用它。没有常规的东西适用。
否则,它和以前几乎一样。有一些差异(§11.6.4 ¶3.6):
由于新的语法,可以在复制初始化表单中为构造函数指定多个参数。例如,C
和c5
都指定相同的构造函数参数。在复制初始化中,不允许显式构造函数。请注意,我写了不允许而不是忽略:在常规复制初始化中,显式构造函数被忽略并且重载解析仅使用非显式表单。但是在copy-list-initialization中,所有构造函数都用于重载解析,但是如果选择了显式构造函数,则会出现错误(§16.3.1.7)。
另一个区别是,在使用列表语法时,缩小转换(§11.6.4 ¶7)会被标记为错误(如果它们将用于隐式转换)。
现在有一个大惊喜:临时回来了!在c6
的情况下,即使c7
是prvalue,规则也是将其与构造函数参数匹配。因此,表达式生成了exp()
类型的值,但然后该值用于选择构造函数(它将是复制构造函数)。由于复制构造函数不能显式,因此直接和复制语法之间没有区别。
这一切都相当复杂,但正常的日常编码员只需要了解一些简单的规则。
C
构造函数。列表:
std::vector
)。所以,有关于列表的新部分。但实际上,你需要忘掉:忘记转换 - 然后 - (假装)复制业务。