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

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

本文关键字:动机 是什么 复制 初始化      更新时间:2023-10-16

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

初始化

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

A a(b);
A a = b;

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

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

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

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

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

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

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

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

<小时 />

另一个例子,考虑

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 非限定版本的临时版本。临时是原则。然后,调用的结果(构造函数情况的临时结果)用于根据上述规则直接初始化作为复制初始化目标的对象。在某些情况下,允许实现通过将中间结果直接构造到正在初始化的对象中来消除此直接初始化中固有的复制;参见 12.2、12.8。

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

举个例子:

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);

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

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

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

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

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