【问题标题】:What's the motivation behind having copy and direct initialization behave differently?复制和直接初始化行为不同的动机是什么?
【发布时间】:2012-06-27 09:37:16
【问题描述】:

Why is copy constructor called instead of conversion constructor?有点关系

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

A a(b);
A a = b;

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

【问题讨论】:

  • @Als:也许,我不知道那个标签的用途。不过,这个问题的想法并不是对规范实际上的内容进行区分,这就是我认为的语言律师。如果允许,我更愿意将其标记为语言设计。
  • +1。好问题。甚至我也想知道为什么它们被设计表现得不同。
  • @moooeeeep 不是真的。我知道有区别,我在问为什么。
  • @Luchian:关于该问题的最佳答案详细说明了您何时使用哪个重要。它实际上说直接初始化可以执行比复制初始化更多的转换,因为直接初始化可以将b 转换为A 具有构造函数的任何类型,而复制初始化必须尝试将b 专门转换为AA 的派生类。因此,差异的一个合理动机是复制初始化的存在是为了抑制 A 的任何 explicit 非复制构造函数。
  • 哦,还要抑制从bA 的转换链,其中涉及两个用户定义的转换,其中第二个是A 的构造函数。那里的答案没有解决的是,哪些差异是真正的动机。

标签: c++ initialization history language-design


【解决方案1】:

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

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

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

【讨论】:

  • 好的,但是程序员为什么要想要额外的副本呢?好像没什么用。
  • 我同意。这对我来说没有任何意义,但这是我能想象的唯一可能的解释。我认为只有设计该语言的人才能说出他们的原因。
  • 是的。或者真正想要一份副本并能说出原因的人。
【解决方案2】:

由于它是临时副本,它可以并且可能会被优化

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

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

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


另一个例子,考虑

A a;
B b;

A a1 = a;
A a2 = b;

As 的复制构造函数是私有的时,允许a2 而禁止a1 同样是不一致的。


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

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

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

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

【讨论】:

  • 对不起,我还是不明白怎么做。您能否在答案中更具描述性? (不是反对者)
  • 这回答了这个问题,“为什么即使应用了复制省略,复制构造函数也需要可访问?”。它没有回答这个问题,“为什么定义复制初始化语法来执行可以省略的复制?”
  • 确实,它解决了 的问题,“因此,额外的副本本身不足以引起不同的行为”。潜在的额外副本的原因。为什么决定拥有副本让我难以理解。
  • @RedX - 不,编译器被明确允许省略复制即使如果有副作用。
  • 这正是问题所在 - 为什么要有副本?
【解决方案3】:

举个例子:

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

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

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

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

我现在不得不承认,这个答案与 Suma 的答案非常相似。这个想法是,您可以预期当前规则的行为,而其他任何事情都很难让人们遵循。

【讨论】:

  • foo 按值获取参数,因此很明显会涉及到一个副本。我认为这与问题无关。
  • @Tony:这个例子的重点是表明并不是所有的副本都可以被消除。尽管在某些情况下可以省略它们,但制定更通用的规则然后为每个用例制定单独的规则更容易。
【解决方案4】:

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

int i = 2;

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

int i(2);

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-11-07
    • 2018-11-25
    • 2010-11-06
    • 1970-01-01
    相关资源
    最近更新 更多