【问题标题】:What constructor should be called in return statement with curly braces?应该在带有花括号的 return 语句中调用什么构造函数?
【发布时间】:2021-06-16 20:14:23
【问题描述】:

考虑以下代码:

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

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

谁是正确的?那里的return {var};语句应该调用什么类型的构造函数?

【问题讨论】:

  • 我很确定 msvc 错了。
  • 我假设问题的上下文与 Sy 的 article 有关。
  • 不管编译器有什么错误,不要像这样显式删除移动构造函数。由于复制是移动的有效实现,通用代码中的T(std::move(t)) 应该回退到复制,而不是彻底失败。

标签: c++ c++17 language-lawyer


【解决方案1】:

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

// 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 的 copy-init 上下文的重载解析(可能)是两阶段的,开始寻找 ctor 重载,就好像对象是由右值指定的:

在以下复制初始化上下文中,可能会使用移动操作而不是复制操作:

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

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

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

另一方面,NonMovable 中所有未删除的构造函数都是微不足道的,这意味着我们可以转向 [class.temporary]/3

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

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

在了解编译器在此处实际执行的操作之前,请考虑对上述程序 (A1) 稍作修改的版本:

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

// ... as above

默认和复制 ctor 现在已变得不平凡,因为它们是用户提供的。

然后:

  • Clang 11:接受 C++14 到 C++20 的 (A1) 和 (A2)
  • Clang 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
  • MSVC:拒绝 C++14 到 C++20 的 (A1) 和 (A2)

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

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

将对应的程序分别记为(B1)和(B2),则:

  • Clang 11:接受 C++14 到 C++20 的 (B1) 和 (B2)
  • Clang 13:接受 C++14 到 C++20 的 (B1) 和 (B2)
  • GCC 10:接受 C++14 到 C++20 的 (B1) 和 (B2)
  • GCC 11:接受 C++14 到 C++20 的 (B1) 和 (B2)
  • MSVC:拒绝 C++14 到 C++20 的 (B1) 和 (B2)

谁是正确的?那里的return {var};语句应该调用什么类型的构造函数?

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

(A1) 和 (A2) 的实现差异可能与 P1825R0: Merged wording for P0527R1 and P1155R3 (more implicit moves) 有关,这可以解释为什么 GCC 11 和 Clang 13 现在都拒绝 C++20 的 (A1) 和 (A2),我们可能此外请注意,GCCClang 特别将 P1825R0 标记为分别在版本 11 和 13 中实现。

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

【讨论】:

  • return 语句中的表达式是一个花括号初始化列表 你的意思是操作数吗?因为braced-init-list 不是表达式。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-05-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多