在 return 语句中用大括号调用什么构造函数?
What constructor should be called in return statement with curly braces?
考虑以下代码:
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};
语句中应该调用什么类型的构造函数?
关于 return 值复制省略和临时实现存在相当多的实现混乱。首先,让我们看一下 OP 的示例,稍微修改为不在 return 语句中使用 braced-init-list:
// 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
正式为move构造函数提供了一个explicitly-deleted定义,意味着move构造函数将参与重载决议,它缺少一个非删除定义不会影响重载解析的结果。
现在,根据 [class.copy.elision]/3,#2 的复制初始化上下文的重载决议(可能)是两阶段的,开始寻找 ctor 重载,就好像对象是由右值指定的一样:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
(3.1) If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or [...]
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
这里的关键是第二阶段仅在第一阶段过载解决方案失败(或未执行)时执行。上面的例子中,第一阶段的重载决议会找到被删除的move ctor,第二阶段不会执行。因此,仅基于 [class.copy.elision]/3,有人会争辩说程序 (A) 的格式不正确。
另一方面,NonMovable
中所有未删除的构造函数都是微不足道的,这意味着我们可以转向 [class.temporary]/3、
When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
这可以推翻 [class.copy.elision]/3 否则 拒绝该程序的事实。
在深入了解编译器在这里实际做什么之前,请考虑对上面的程序 (A1) 进行稍微修改的版本:
// Program (A2)
struct NonMovable {
NonMovable() {};
NonMovable(const NonMovable&) {};
NonMovable(NonMovable&&) = delete; // #1
};
// ... as above
默认和复制 ctors 现在已经变得非常重要,因为它们是用户提供的。
然后:
- 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) 但将 return 对象括在大括号中,
// ... 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)
Who's correct? What type of constructor should be called in return {var};
statement there?
总结并回答 OP 的原始问题,MSVC 拒绝 (B1) 和 (B2) 是错误的,因为 [class.copy.elision]/3,尤其是 /3.1,在表达式中不适用return 语句是一个花括号初始化列表,即使它包装了一个具有自动存储持续时间的命名对象。这种情况是简单的大括号复制初始化,复制构造函数应该是重载决议产生的最佳可行函数。
(A1) 和 (A2) 的实施差异可能与 P1825R0: Merged wording for P0527R1 and P1155R3 (more implicit moves), which would explain why both GCC 11 and Clang 13 now rejects both (A1) and (A2) for C++20, and we may moreover note that GCC and Clang 有关,特别是 P1825R0 分别在版本 11 和 13 中实施。
但是,我不明白为什么 Clang 13 似乎也向后移植了这一点,特别是拒绝了 C++14 和 C++17 的 (A1) 和 (A2)。我们可能会注意到 Clang(即使在早期版本中)也拒绝了 C++14 中 [diff.cpp17.class]/3 的示例(作为 P1825R0 的一部分添加到 C++20 标准,突出了 C++17 的兼容性更改)和 C++17,这可以说是一个 Clang 错误。我推测 (A1) 和 (A2) 的反向移植拒绝是无意的。
考虑以下代码:
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};
语句中应该调用什么类型的构造函数?
关于 return 值复制省略和临时实现存在相当多的实现混乱。首先,让我们看一下 OP 的示例,稍微修改为不在 return 语句中使用 braced-init-list:
// 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
正式为move构造函数提供了一个explicitly-deleted定义,意味着move构造函数将参与重载决议,它缺少一个非删除定义不会影响重载解析的结果。
现在,根据 [class.copy.elision]/3,#2 的复制初始化上下文的重载决议(可能)是两阶段的,开始寻找 ctor 重载,就好像对象是由右值指定的一样:
In the following copy-initialization contexts, a move operation might be used instead of a copy operation:
(3.1) If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or [...]
overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.
这里的关键是第二阶段仅在第一阶段过载解决方案失败(或未执行)时执行。上面的例子中,第一阶段的重载决议会找到被删除的move ctor,第二阶段不会执行。因此,仅基于 [class.copy.elision]/3,有人会争辩说程序 (A) 的格式不正确。
另一方面,NonMovable
中所有未删除的构造函数都是微不足道的,这意味着我们可以转向 [class.temporary]/3、
When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
这可以推翻 [class.copy.elision]/3 否则 拒绝该程序的事实。
在深入了解编译器在这里实际做什么之前,请考虑对上面的程序 (A1) 进行稍微修改的版本:
// Program (A2)
struct NonMovable {
NonMovable() {};
NonMovable(const NonMovable&) {};
NonMovable(NonMovable&&) = delete; // #1
};
// ... as above
默认和复制 ctors 现在已经变得非常重要,因为它们是用户提供的。
然后:
- 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) 但将 return 对象括在大括号中,
// ... 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)
Who's correct? What type of constructor should be called in
return {var};
statement there?
总结并回答 OP 的原始问题,MSVC 拒绝 (B1) 和 (B2) 是错误的,因为 [class.copy.elision]/3,尤其是 /3.1,在表达式中不适用return 语句是一个花括号初始化列表,即使它包装了一个具有自动存储持续时间的命名对象。这种情况是简单的大括号复制初始化,复制构造函数应该是重载决议产生的最佳可行函数。
(A1) 和 (A2) 的实施差异可能与 P1825R0: Merged wording for P0527R1 and P1155R3 (more implicit moves), which would explain why both GCC 11 and Clang 13 now rejects both (A1) and (A2) for C++20, and we may moreover note that GCC and Clang 有关,特别是 P1825R0 分别在版本 11 和 13 中实施。
但是,我不明白为什么 Clang 13 似乎也向后移植了这一点,特别是拒绝了 C++14 和 C++17 的 (A1) 和 (A2)。我们可能会注意到 Clang(即使在早期版本中)也拒绝了 C++14 中 [diff.cpp17.class]/3 的示例(作为 P1825R0 的一部分添加到 C++20 标准,突出了 C++17 的兼容性更改)和 C++17,这可以说是一个 Clang 错误。我推测 (A1) 和 (A2) 的反向移植拒绝是无意的。