当前位置: 首页 > 知识库问答 >
问题:

在 return 语句中应该用大括号调用哪个构造函数?

袁奇逸
2023-03-14

请考虑以下代码:

struct NonMovable {
  NonMovable() = default;
  NonMovable(const NonMovable&) = default;
  NonMovable(NonMovable&&) = delete;
};

NonMovable f() {
  NonMovable nonMovable;
  return {nonMovable};
  //return NonMovable(nonMovable);
}

int main() {}

GCC和Clang编译代码时没有错误,也就是说,使用花括号时调用了复制构造函数。但是msvc拒绝了https://godbolt.org/z/49onKj的错误:

error C2280: 'NonMovable::NonMovable(NonMovable &&)': attempting to reference a deleted function

当我指定显式调用复制构造函数(因为nonMoable不是右值),然后mvsc接受代码。

谁是对的?应该调用什么类型的构造函数来返回 {var}; 语句?

共有1个答案

艾弘义
2023-03-14

关于返回值复制省略和临时实体化,存在相当多的实现混乱。首先,让我们看一下OP的示例,稍微修改为在返回语句中不使用括号初始化列表:

// Program (A1)
struct NonMovable {
  NonMovable() = default;
  NonMovable(const NonMovable&) = default;
  NonMovable(NonMovable&&) = delete;  // #1
};

NonMovable f() {
  NonMovable nonMovable;
  return nonMovable;  // #2
                      // GCC, Clang: OK
                      // MSVC: Error (use of deleted function)
}

int main() {}

#1正式为移动构造函数提供了显式删除的定义,这意味着移动构造函数将参与重载解析,并且它缺少未删除的定义不会影响重载解析的结果。

现在,根据[class.copy.elision]/3,#2的复制初始化上下文的重载解析(可能)是两阶段的,开始寻找ctor重载,就好像对象由右值指定一样:

在以下复制初始化上下文中,可以使用移动操作来代替复制操作:

(3.1) 如果 return 语句中的表达式是一个(可能用括号括起来的)id 表达式,该表达式命名的对象具有在最内层封闭函数或 lambda 表达式的正文或参数声明子句中声明的自动存储持续时间,或者 [...]

首先执行重载解析以选择副本的构造函数,就好像对象是由rvalue指定的一样。如果第一个重载解析失败或未执行,或者如果所选构造函数的第一个参数的类型不是对象类型的右值引用(可能是cv限定的),则会再次执行重载解析,将对象视为左值。

这里的关键是,只有当第一阶段过载解决失败(或没有执行)时,才执行第二阶段。在上面的例子中,第一阶段重载决策将找到被删除的move ctor,而第二阶段将不被执行。因此,仅仅基于[class.copy.elision]/3,人们会认为程序(A)是病态的。

另一方面,< code>NonMovable中所有未删除的构造函数都是琐碎的,这意味着我们可以求助于[class.temporary]/3,

当类类型X的对象被传递到函数或从函数返回时,如果X的每个复制构造函数、移动构造函数和析构函数都是平凡的或已删除的,并且X至少有一个未删除的复制或移动构造函数,则允许实现创建一个临时对象来保存函数参数或结果对象。临时对象分别由函数参数或返回值构造,函数的参数或返回对象被初始化,就好像使用未删除的平凡构造函数来复制临时一样(即使该构造函数不可访问或重载解析不会选择该构造函数来执行对象的复制或移动)。

这可以说推翻了[class.copy.elision]/3否则会拒绝该程序的事实。

在进入编译器在这里实际做什么之前,考虑上面程序(A1)的一个稍微修改的版本:

// Program (A2)
struct NonMovable {
  NonMovable() {};
  NonMovable(const NonMovable&) {};
  NonMovable(NonMovable&&) = delete;  // #1
};

// ... as above

其中默认和复制ctors现在已经变得非常重要,因为它们是用户提供的。

然后:

  • 叮当声 11:接受 C 14 到 C 20 的 (A1) 和 (A2)
  • 叮当声 13:拒绝 C 14 到 C 20 的 (A1) 和 (A2)
  • GCC 10:接受 C 14 至 C 20 的 (A1) 和 (A2)
  • GCC 11:接受 C 14 至 C 17 的 (A1) 和 (A2),拒绝 C 20 的 (A1) 和 (A2)
  • MSVC:拒绝 C 14 到 C 20 的 (A1) 和 (A2)

如果我们调整程序(A1)和(A2 ),但是将返回对象放在大括号中,

// ... as above
return {nonMovable};

将相应的程序表示为(B1)和(B2),然后:

    < li>Clang 11:对于C 14到C 20,接受(B1)和(B2) < li>Clang 13:对于C 14到C 20,接受(B1)和(B2) < li>GCC 10:接受C 14到C 20的(B1)和(B2) < li>GCC 11:接受C 14到C 20的(B1)和(B2) < li>MSVC:拒绝C 14到C 20的(B1)和(B2)

谁是对的?应该调用什么类型的构造函数来返回 {var}; 语句?

为了总结并回答 OP 的原始问题,MSVC 拒绝 (B1) 和 (B2) 是错误的,因为 [class.copy.elision]/3,特别是 /3.1,当返回语句中的表达式是支持初始化列表时不适用,即使它包装了具有自动存储持续时间的命名对象也是如此。这种情况只是支持复制初始化,复制构造函数应该是重载解析产生的最佳可行函数。

(A1)和(A2)的实现差异可能与P1825R0有关:P0527R1和P1155R3的合并措辞(更含蓄的移动),这将解释为什么GCC 11和Clang 13现在都拒绝C 20的(A1)与(A2),此外,我们可能会注意到GCC和Clang特别将P1825R标记为分别在版本11和13中实现。

然而,我不明白为什么Clang 13似乎也反向移植了这个,特别是拒绝了C 14和C 17的(A1)和(A2)。我们可能会注意到,Clang(即使在早期版本中)也拒绝了C 14和C 17中的[diff.cpp17.class]/3(作为P1825R0的一部分添加到C 20标准中,突出了C 17的兼容性变化)的例子,这可以说是一个Clang错误。我推测(A1)和(A2)的反向移植拒绝是无意的。

 类似资料:
  • 新的ES6箭头函数表示在某些情况下是隐式的: 表达式也是该函数的隐式返回值。 在什么情况下,我需要将与ES6箭头函数一起使用?

  • 如果将移到构造函数的最后一行,我不明白为什么下面的代码会显示错误。 我已经检查了很多关于StackOverflow的答案,但我仍然不能理解这其中的原因。请帮我用一些简单的例子和解释弄清楚这个错误。

  • 问题内容: 我试图理解下面两个require语句之间的区别。 具体来说,s包装的目的是什么? 它们似乎都分配了电子模块的内容,但是它们的功能显然不同。 谁能给我一些启示? 问题答案: 第二个示例使用解构。 这将调用从所需模块导出的特定变量(包括函数)。 例如(functions.js): 包含在您的文件中: 现在您可以分别给他们打电话了, 相对于: 使用点表示法调用: 希望这可以帮助。 您可以在此

  • return语句用来从一个函数 返回 即跳出函数。我们也可选从函数 返回一个值 。 使用字面意义上的语句 例7.7 使用字面意义上的语句 #!/usr/bin/python # Filename: func_return.py defmaximum(x, y):     ifx > y:         returnx     else:         returny printmaximum(

  • 这个Super()的替代品是什么;我代码中的语句...因为它向我显示了一个名为:构造函数调用必须是构造函数中的第一个语句的错误。

  • 问题内容: 我在JS应用程序中使用异步函数,这些函数包装有接收回调输入的我自己的函数。当我调用回调函数时,是否需要使用“ return”关键字?有关系吗?有什么不同? 例如: PS:我正在使用javascript编写混合移动应用。 问题答案: 如果只有一条路径通过函数,则可以将两种形式互换使用。当然,该函数的返回值将在没有返回的情况下是不确定的,但是您的调用代码可能仍未使用它。 没错 实际上相当于