43

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

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

A a(b);
A a = b;

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

4

4 回答 4

4

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

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

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

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


另一个例子,考虑

A a;
B b;

A a1 = a;
A a2 = b;

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


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

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

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

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

于 2012-06-27T09:59:14.440 回答
4

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

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

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

于 2012-06-27T10:27:09.853 回答
1

Take the following example:

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

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

In the compilers I tested, the argument to foo was always copied even with full optimization turned on. From this, we can gather that copies will not/must not be eliminated in all situations.

Now lets think from a language design perspective, imagine all the scenarios you would have to think about if you wanted to make rules for when a copy is needed and when it isn't. This would be very difficult. Also, even if you were able to come up with rules, they would be very complex and almost impossible for people to comprehend. However, at the same time, if you forced copies everywhere, that would be very inefficient. This is why the rules are the way they are, you make the rules comprehensible for people to understand while still not forcing copies to be made if they can be avoided.

I have to admit now, this answer is very similar to Suma's answer. The idea is that you can expect the behavior with the current rules, and anything else would be too hard for people to follow.

于 2012-06-29T09:40:11.040 回答
0

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

int i = 2;

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

int i(2);

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

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

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

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

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

于 2013-06-17T03:51:46.237 回答