复制初始化和直接初始化的背后动机是什么?

43

与此相关的是 为什么会调用复制构造函数而不是转换构造函数?

有两种初始化语法,分别是直接初始化和拷贝初始化:

A a(b);
A a = b;

我想知道它们有不同定义行为的动机。对于复制初始化,会涉及到额外的复制操作,而我想不到任何目的需要那个复制。既然这是从临时变量复制出来的,那么它很可能会被优化掉,所以用户不能依赖于它发生 - 因此,额外的复制本身并不足以成为不同行为的原因。那么...为什么?


8
@Als: 或许我并不清楚该标签的目的。然而,问题的意图并不是为了在规范中挑剔每一个微小的细节,那就有些像是语言律师式的行为。如果可以的话,我更愿意将其标记为“语言设计”。 - Steve Jessop
5
好问题。即使是我也想知道它们为什么被“设计”成行为不同。 - Nawaz
4
不完全正确。我知道有区别,我在问为什么。 - Luchian Grigore
6
@Luchian:那个问题中排名第一的答案详细说明了使用哪种初始化方式何时很重要。它实际上表示,直接初始化可以执行比复制初始化更多的转换,因为直接初始化可以将“b”转换为任何“A”具有构造函数的类型,而复制初始化必须尝试将“b”特别转换为“A”或“A”的派生类。因此,差异的一个合理动机是,复制初始化存在是为了抑制A的任何显式非复制构造函数。 - Steve Jessop
4
同时,还需要抑制从bA的转换链,其中涉及两个用户定义的转换,其中第二个是A的构造函数。那里的答案没有解决的是,这些差异中哪一个才是实际的动机。 - Steve Jessop
显示剩余4条评论
4个回答

4
由于这是从临时变量复制而来,因此它可以并且可能会被优化掉。
关键字在于可能。标准允许编译器对复制进行优化,但不要求。如果一些编译器允许这段代码(优化),而其他编译器拒绝它(非优化),那么这将非常不一致。
因此,标准规定了一种一致的处理方式-每个人都必须检查复制构造函数是否可访问,无论他们是否使用它。
思路是所有编译器应该接受或拒绝这段代码。否则它将是不可移植的。
另一个例子,请考虑
A a;
B b;

A a1 = a;
A a2 = b;

如果 A 的拷贝构造函数是私有的,那么允许 a2 却禁止 a1 是同样不一致的。


从标准文本中我们也可以看到,初始化类对象的两种方法是有区别的 (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
4
这回答了一个问题:“即使应用了复制省略,为什么复制构造函数仍然需要可访问?” 它并未回答这个问题:“为什么复制初始化语法被定义为可以被省略的复制?” - Steve Jessop
1
真实的情况是,它解决了“因此额外的复制本身不足以成为不同行为的原因”的问题。潜在的额外复制本身就是原因。我不明白为什么决定在那里有这个副本。 - Bo Persson
2
@RedX - 不,即使存在副作用,编译器也明确允许省略复制。 - Bo Persson
1
这正是问题所在 - 为什么要复制? - Luchian Grigore
显示剩余3条评论

4

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

设计时采用这种方式是因为程序员预计会期望这种行为,即他将期望在使用=符号时进行复制,而不是使用直接初始化语法进行复制。

我认为可能的复制省略仅在标准的较新版本中添加,但我不确定 - 这是有人可以通过查看标准历史来确定的事情。


好的,但是程序员为什么会想要额外的副本呢?看起来它似乎没有任何作用。 - Luchian Grigore
我同意。然而,这对我来说毫无意义,但这是我能想象到的唯一可能的解释。我认为只有那些设计语言的人才能说明他们的原因。 - Suma
1
是的。或者有人真的想要一份副本并能告诉我们原因。 - Luchian Grigore

1

以以下示例为例:

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

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

在我测试的编译器中,即使开启了完全优化,foo 的参数也总是被复制的。由此可见,在所有情况下都不会/不能消除副本。
现在从语言设计的角度来考虑,如果你想制定何时需要复制和何时不需要复制的规则,那么你必须考虑所有可能的情况,这将非常困难。而且,即使你能够制定规则,它们也会非常复杂,几乎无法让人理解。然而,同时,如果你强制复制每个地方,那将非常低效。这就是为什么规则是这样的,你可以让规则易于理解,同时如果可以避免复制,就不要强制进行复制。
我现在必须承认,这个答案与 Suma 的答案非常相似。这个想法是,你可以期望当前规则的行为,任何其他行为都会对人们来说太难以理解。

foo按值传递其参数,因此很明显会涉及到复制。我认为这与问题无关。 - Tony
@Tony:这个例子的整个意义在于展示并非所有的副本都可以被消除。虽然在某些情况下它们可以省略,但制定一个更普遍的规则比为每个用例制定单独的规则更容易。 - Jesse Good

0

内置类型的初始化,例如:

int i = 2;

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

int i(2);

即使一些数学家可能会对这一点提出异议。毕竟,在调用函数(在这种情况下是构造函数)并传递参数时,没有什么不自然的。

对于内置类型,这两种初始化方式是相同的。在前一种情况下没有额外的复制。 这就是拥有这两种初始化方式的原因,最初没有特定的意图使它们行为不同。

然而,存在用户定义的类型,语言的一个声明目标是允许它们尽可能地像内置类型一样运行。

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

您可能会有额外的副本,并且它们可能被省略,这是针对用户定义类型的优化。复制省略和显式构造函数都是后来才加入到语言中的。标准允许在一定使用期后进行优化并不足为奇。此外,现在可以从重载解析候选项中消除显式构造函数。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接