让复制和直接初始化的行为不同的动机是什么?

What's the motivation behind having copy and direct initialization behave differently?

提问人:Luchian Grigore 提问时间:6/27/2012 最后编辑:CommunityLuchian Grigore 更新时间:6/18/2013 访问量:2260

问:

有点关系 为什么调用复制构造函数而不是转换构造函数?

初始化有两种语法:直接初始化和复制初始化:

A a(b);
A a = b;

我想知道他们有不同的定义行为的动机。对于副本初始化,涉及一个额外的副本,我想不出该副本的任何用途。由于它是临时副本,因此它可以而且可能会被优化出来,因此用户不能依赖它的发生 - 因此,额外的副本本身并不足以成为不同行为的理由。所以。。。为什么?

C++ 初始化 历史 语言设计

评论

8赞 Steve Jessop 6/27/2012
@Als:也许,我不知道那个标签的用途。不过,这个问题的想法并不是要对规范实际的内容进行细细的划分,这就是我所认为的语言律师。如果允许的话,我更愿意给它贴上语言设计的标签。
5赞 Nawaz 6/27/2012
+1.问得好。甚至我想知道为什么它们被设计为行为不同的原因。
4赞 Luchian Grigore 6/27/2012
@moooeeeep不是真的。我知道有区别,我在问为什么。
6赞 Steve Jessop 6/27/2012
@Luchian:不过,这个问题的最高答案详细说明了何时使用哪一个很重要。它实际上表明,直接初始化可以执行比复制初始化更多的转换,因为直接初始化可以转换为具有构造函数的任何类型,而复制初始化必须尝试专门转换为 的派生类或派生类。因此,造成这种差异的一个合理动机是,存在复制初始化是为了抑制 的任何非复制构造函数。bAbAAexplicitA
4赞 Steve Jessop 6/27/2012
哦,还要抑制从 到 的转换链,这涉及两个用户定义的转换,其中第二个是 的构造函数。那边的答案没有解决的是,哪些差异是真正的动机。bAA

答:

4赞 Bo Persson 6/27/2012 #1

由于它是临时的副本,因此可以而且可能会对其进行优化

这里的关键词是可能。该标准允许(但不要求)编译器对副本进行优化。如果某些编译器允许此代码(优化),但其他编译器拒绝它(未优化),这将是非常不一致的。

因此,该标准规定了一种一致的处理方式 - 每个人都必须检查复制构造函数是否可访问,无论他们是否使用它。

这个想法是所有编译器都应该接受或拒绝代码。否则,它将是不可移植的。


再举一个例子,考虑一下

A a;
B b;

A a1 = a;
A a2 = b;

当复制构造函数是私有的时,允许但禁止同样不一致。a2a1A


我们还可以从标准文本中看到,初始化类对象的两种方法本来是不同的 (8.5/16):

如果初始化是直接初始化,或者是复制初始化,其中源类型的 cv 非限定版本与目标类的类相同或派生类,则考虑构造函数。枚举了适用的构造函数 (13.3.1.3),并通过重载解析 (13.3) 选择最佳构造函数。调用如此选择的构造函数来初始化对象,并将初始值设定项表达式或表达式列表作为其参数。如果未应用构造函数,或者重载解析不明确,则初始化格式不正确。

否则(即,对于其余的复制初始化情况),可以按 13.3.1.4 所述枚举可以从源类型转换为目标类型或(当使用转换函数时)转换为其派生类的用户定义的转换序列,并通过重载解析 (13.3) 选择最佳转换序列。如果转换无法完成或不明确,则初始化格式不正确。使用初始值设定项表达式作为其参数调用所选函数;如果函数是构造函数,则调用将初始化目标类型的 CV 非限定版本的临时版本。临时值是 prvalue。然后,根据上述规则,调用的结果(对于构造函数情况是临时的)用于直接初始化作为复制初始化目标的对象。在某些情况下,允许实现通过将中间结果直接构造到正在初始化的对象中来消除此直接初始化中固有的复制;见12.2、12.8。

区别在于,直接初始化直接使用构造类的构造函数。使用复制初始化时,会考虑其他转换函数,这些函数可能会产生必须复制的临时函数。

评论

1赞 Luchian Grigore 6/27/2012
对不起,我仍然不明白如何。你能在答案中描述性更强一点吗?(不是反对票)
4赞 Steve Jessop 6/27/2012
这回答了以下问题:“为什么即使应用了复制省略,复制构造函数也需要可访问?它没有回答以下问题:“为什么将复制初始化语法定义为执行可以省略的复制?
1赞 Bo Persson 6/27/2012
没错,它解决了“因此,额外的副本本身不足以成为不同行为的理由”的观点。潜在的额外副本原因。为什么决定在那里复制我不明白。
2赞 Bo Persson 6/27/2012
@RedX - 不可以,即使会有副作用,编译器也可以明确地省略复制。
1赞 Luchian Grigore 6/27/2012
这正是问题所在——为什么要有副本?
4赞 Suma 6/27/2012 #2

这只是一个猜测,但恐怕如果没有 Bjarne Stroustrup 确认它的真实情况,就很难更确定:

之所以这样设计,是因为程序员假定程序员会期望这种行为,他会期望在使用 = 符号时完成复制,而不是使用直接初始值设定项语法完成。

我认为可能的复制省略只是在标准的更高版本中添加的,但我不确定 - 有人可以通过检查标准历史记录来肯定地知道这一点。

评论

0赞 Luchian Grigore 7/2/2012
好的,但是为什么程序员想要额外的副本呢?似乎它没有任何目的。
0赞 Suma 7/2/2012
我同意。这对我来说没有任何意义,但这是我能想象到的唯一可能的解释。我认为只有那些设计这门语言的人才能说出他们的原因。
1赞 Luchian Grigore 7/2/2012
是的。或者有人真的想要一份副本,并能说出原因。
1赞 Jesse Good 6/29/2012 #3

以以下示例为例:

struct X
{
    X(int);
    X(const X&);
};

int foo(X x){/*Do stuff*/ return 1; }
X x(1);
foo(x);

在我测试的编译器中,即使打开了完全优化,参数 to 也总是被复制。由此,我们可以得出结论,在所有情况下,副本都不会/一定不会被消除。foo

现在让我们从语言设计的角度来思考,想象一下如果你想制定规则,什么时候需要副本,什么时候不需要副本,你必须考虑的所有场景。这将是非常困难的。此外,即使你能够想出规则,它们也会非常复杂,人们几乎不可能理解。但是,与此同时,如果您到处强制复制,那将是非常低效的。这就是为什么规则是这样的,你使规则易于理解,让人们理解,同时如果可以避免的话,仍然不会强迫复制。

我现在不得不承认,这个答案和苏玛的答案非常相似。这个想法是,你可以期待当前规则的行为,而其他任何事情对于人们来说都太难遵循了。

评论

0赞 Tony 6/17/2013
Foo 按值获取其参数,因此很明显会涉及一个副本。我认为这与这个问题没有任何关系。
0赞 Jesse Good 6/17/2013
@Tony:这个例子的重点是表明并非所有副本都可以被消除。尽管在某些情况下可以省略它们,但制定更通用的规则比为每个用例制定单独的规则更容易。
0赞 Tony 6/17/2013 #4

初始化内置类型,例如:

int i = 2;

是非常自然的语法,部分原因是历史原因(记住你的高中数学)。它比以下方面更自然:

int i(2);

即使一些数学家可能会争论这一点。毕竟,调用函数(在本例中为构造函数)并向其传递参数并没有什么不自然的。

对于内置类型,这两种初始化类型是相同的。在前一种情况下,没有额外的副本。 这就是同时进行两种类型的初始化的原因,最初没有具体的意图使它们的行为不同。

但是,存在用户定义的类型,该语言的既定目标之一是允许它们尽可能地表现为内置类型。

因此,复制构造(例如,从某个转换函数获取输入)是第一个语法的自然实现。

您可能有额外的副本,并且它们可能会被省略,这一事实是对用户定义类型的优化。复制省略和显式构造函数都出现在语言中要晚得多。标准允许在使用一段时间后进行优化也就不足为奇了。此外,现在还可以从重载解析候选项中消除显式构造函数。