NRVO什么时候开始?需要满足什么要求?
When does NRVO kick in? What are the requirements to be satisfied?
我有以下代码。
#include <iostream>
struct Box {
Box() { std::cout << "constructed at " << this << '\n'; }
Box(Box const&) { puts("copy"); }
Box(Box &&) = delete;
~Box() { std::cout << "destructed at " << this << '\n'; }
};
auto f() {
Box v;
return v; // is it eligible for NVRO?
}
int main() {
auto v = f();
}
以上代码产生错误。 call to deleted constructor/function in both gcc and clang
但是如果我将代码更改为 return 纯右值,代码就可以工作。
auto f() {
Box v;
return Box(); // is it because of copy elision?
}
为什么会这样?是因为删除移动构造函数吗?
如果我将复制和移动构造函数都更改为显式,它也会产生错误?
如果标记为已删除,为什么不能简单地使用定义的复制构造函数
编辑:
compiled with -std=c++20 in both gcc and clang, error.
compiled with -std=c++17 gcc, compiles.
compiled with -std=c++17 clang, error.
编辑 2:
clang version: 12.0.0
gcc version: 11.1
此程序中存在两种不同的潜在错误。
auto v = f();
在 C++14 及以下版本中是一个错误,因为该操作在逻辑上是一个移动构造,而不是在 C++17 及更高版本中的错误,因为它是临时而不是的具体化一个移动结构。这是 C++17 保证的复制省略功能,与 NRVO 不同。
return v;
在所有版本的 C++ 中都是一个错误,因为它在逻辑上是一个移动构造,并且构造函数需要存在且可访问。大多数时候 NRVO 会优化构造函数,但 NRVO 不是强制性的,它只是允许的,因此它不能使原本无效的程序有效。但是,gcc 不会捕获 std=c++17
及更低版本的错误。相反,它会回退到复制构造函数。这似乎是一个 gcc 错误。
C++17 不强制要求 NRVO。当操作数是纯右值时,它要求在 return
语句中进行复制省略,因此在这种情况下,不需要存在 copy/move 构造函数。这就是 return Box();
起作用的原因。
显然,C++20 标准发生了变化,影响了 copy/move 省略(Quoting the draft):
Affected subclause: [class.copy.elision]
Change: A function
returning an implicitly movable entity may invoke a constructor taking
an rvalue reference to a type different from that of the returned expression.
Function and catch-clause parameters can be thrown using move
constructors.
给出的例子:
struct base {
base();
base(base const &);
private:
base(base &&);
};
struct derived : base {};
base f(base b) {
throw b; // error: base(base &&) is private
derived d;
return d; // error: base(base &&) is private
}
[class.copy.elision] 的收获(强调我的):
An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference
to a non-volatile object type. In the following copy-initialization
contexts, a move operation is first considered before attempting a
copy operation:
If the expression in a return ([stmt.return])
or co_return ([stmt.return.coroutine]) statement is a (possibly
parenthesized) id-expression that names an implicitly movable entity
declared in the body or parameter-declaration-clause of the innermost
enclosing function or lambda-expression, or
if the operand of
a throw-expression ([expr.throw]) is a (possibly parenthesized)
id-expression that names an implicitly movable entity that belongs to
a scope that does not contain the compound-statement of the innermost
try-block or function-try-block (if any) whose compound-statement or
ctor-initializer contains the throw-expression, overload resolution to
select the constructor for the copy or the return_value overload to
call is first performed as if the expression or operand were an
rvalue. If the first overload resolution fails or was not performed,
overload resolution is performed again, considering the expression or
operand as an lvalue.
[Note 3: This two-stage overload resolution is performed regardless of
whether copy elision will occur. It determines the constructor or the
return_value overload to be called if elision is not performed, and
the selected constructor or return_value overload must be accessible
even if the call is elided. — end note]
我有以下代码。
#include <iostream>
struct Box {
Box() { std::cout << "constructed at " << this << '\n'; }
Box(Box const&) { puts("copy"); }
Box(Box &&) = delete;
~Box() { std::cout << "destructed at " << this << '\n'; }
};
auto f() {
Box v;
return v; // is it eligible for NVRO?
}
int main() {
auto v = f();
}
以上代码产生错误。 call to deleted constructor/function in both gcc and clang
但是如果我将代码更改为 return 纯右值,代码就可以工作。
auto f() {
Box v;
return Box(); // is it because of copy elision?
}
为什么会这样?是因为删除移动构造函数吗? 如果我将复制和移动构造函数都更改为显式,它也会产生错误?
如果标记为已删除,为什么不能简单地使用定义的复制构造函数
编辑:
compiled with -std=c++20 in both gcc and clang, error.
compiled with -std=c++17 gcc, compiles.
compiled with -std=c++17 clang, error.
编辑 2:
clang version: 12.0.0
gcc version: 11.1
此程序中存在两种不同的潜在错误。
auto v = f();
在 C++14 及以下版本中是一个错误,因为该操作在逻辑上是一个移动构造,而不是在 C++17 及更高版本中的错误,因为它是临时而不是的具体化一个移动结构。这是 C++17 保证的复制省略功能,与 NRVO 不同。
return v;
在所有版本的 C++ 中都是一个错误,因为它在逻辑上是一个移动构造,并且构造函数需要存在且可访问。大多数时候 NRVO 会优化构造函数,但 NRVO 不是强制性的,它只是允许的,因此它不能使原本无效的程序有效。但是,gcc 不会捕获 std=c++17
及更低版本的错误。相反,它会回退到复制构造函数。这似乎是一个 gcc 错误。
C++17 不强制要求 NRVO。当操作数是纯右值时,它要求在 return
语句中进行复制省略,因此在这种情况下,不需要存在 copy/move 构造函数。这就是 return Box();
起作用的原因。
显然,C++20 标准发生了变化,影响了 copy/move 省略(Quoting the draft):
Affected subclause: [class.copy.elision]
Change: A function returning an implicitly movable entity may invoke a constructor taking an rvalue reference to a type different from that of the returned expression. Function and catch-clause parameters can be thrown using move constructors.
给出的例子:
struct base {
base();
base(base const &);
private:
base(base &&);
};
struct derived : base {};
base f(base b) {
throw b; // error: base(base &&) is private
derived d;
return d; // error: base(base &&) is private
}
[class.copy.elision] 的收获(强调我的):
An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:
If the expression in a return ([stmt.return]) or co_return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
if the operand of a throw-expression ([expr.throw]) is a (possibly parenthesized) id-expression that names an implicitly movable entity that belongs to a scope that does not contain the compound-statement of the innermost try-block or function-try-block (if any) whose compound-statement or ctor-initializer contains the throw-expression, overload resolution to select the constructor for the copy or the return_value overload to call is first performed as if the expression or operand were an rvalue. If the first overload resolution fails or was not performed, overload resolution is performed again, considering the expression or operand as an lvalue.
[Note 3: This two-stage overload resolution is performed regardless of whether copy elision will occur. It determines the constructor or the return_value overload to be called if elision is not performed, and the selected constructor or return_value overload must be accessible even if the call is elided. — end note]