与此相关的是 为什么会调用复制构造函数而不是转换构造函数?
有两种初始化语法,分别是直接初始化和拷贝初始化:
A a(b);
A a = b;
我想知道它们有不同定义行为的动机。对于复制初始化,会涉及到额外的复制操作,而我想不到任何目的需要那个复制。既然这是从临时变量复制出来的,那么它很可能会被优化掉,所以用户不能依赖于它发生 - 因此,额外的复制本身并不足以成为不同行为的原因。那么...为什么?
与此相关的是 为什么会调用复制构造函数而不是转换构造函数?
有两种初始化语法,分别是直接初始化和拷贝初始化:
A a(b);
A a = b;
我想知道它们有不同定义行为的动机。对于复制初始化,会涉及到额外的复制操作,而我想不到任何目的需要那个复制。既然这是从临时变量复制出来的,那么它很可能会被优化掉,所以用户不能依赖于它发生 - 因此,额外的复制本身并不足以成为不同行为的原因。那么...为什么?
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。
其中的一个区别是,直接初始化直接使用所构造类的构造函数。使用复制初始化时,会考虑其他转换函数,这些函数可能会产生需要复制的临时对象。
这只是一种猜测,但如果没有Bjarne Stroustrup确认真实情况,恐怕很难更加确定:
设计时采用这种方式是因为程序员预计会期望这种行为,即他将期望在使用=符号时进行复制,而不是使用直接初始化语法进行复制。
我认为可能的复制省略仅在标准的较新版本中添加,但我不确定 - 这是有人可以通过查看标准历史来确定的事情。
以以下示例为例:
struct X
{
X(int);
X(const X&);
};
int foo(X x){/*Do stuff*/ return 1; }
X x(1);
foo(x);
foo
的参数也总是被复制的。由此可见,在所有情况下都不会/不能消除副本。内置类型的初始化,例如:
int i = 2;
这是非常自然的语法,部分原因是由于历史原因(记得你的高中数学)。它比以下语法更自然:
int i(2);
即使一些数学家可能会对这一点提出异议。毕竟,在调用函数(在这种情况下是构造函数)并传递参数时,没有什么不自然的。
对于内置类型,这两种初始化方式是相同的。在前一种情况下没有额外的复制。 这就是拥有这两种初始化方式的原因,最初没有特定的意图使它们行为不同。
然而,存在用户定义的类型,语言的一个声明目标是允许它们尽可能地像内置类型一样运行。
因此,复制构造(例如从某个转换函数中获取输入)是第一种语法的自然实现。
您可能会有额外的副本,并且它们可能被省略,这是针对用户定义类型的优化。复制省略和显式构造函数都是后来才加入到语言中的。标准允许在一定使用期后进行优化并不足为奇。此外,现在可以从重载解析候选项中消除显式构造函数。
b
到A
的转换链,其中涉及两个用户定义的转换,其中第二个是A
的构造函数。那里的答案没有解决的是,这些差异中哪一个才是实际的动机。 - Steve Jessop