复制省略 return 值和 noexcept
copy elision of return values and noexcept
我有一个这样的函数模板:
template <typename T>
constexpr auto myfunc() noexcept
{
return T{};
}
由于复制省略,这个函数模板是否保证是noexcept?如果在构造函数内部抛出异常,这是发生在函数内部还是外部?
复制省略所做的就是消除实际的复制或移动。一切都会发生 "as-if" 事情会在没有发生复制省略的情况下发生(当然除了复制本身)。
构造发生在函数内部。复制省略不会改变这一点。它所做的只是消除实际的 copy/move 发生(我在重复自己吗?)作为函数的 return 值被推回其调用者的结果。
因此,如果 class 的 default 构造函数抛出异常,则 noexcept
会破坏整个事情都来自高轨道。
如果 copy/move 构造函数抛出异常,由于 copy/move 没有发生,一切都会继续进行。
使用 gcc 7.3.1,使用 -std=c++17 编译:
template <typename T>
constexpr auto myfunc() noexcept
{
return T{};
}
class xx {
public:
xx() { throw "Foo"; }
};
int main()
{
try {
myfunc<xx>();
} catch (...) {
}
}
结果:
terminate called after throwing an instance of 'char const*'
现在,让我们混合起来,在复制和移动构造函数中抛出异常:
class xx {
public:
xx() { }
xx(xx &&) { throw "Foo"; }
xx(const xx &) { throw "Baz"; }
};
这运行没有异常。
这样做:
template <typename T> constexpr
auto myfunc() noexcept(std::is_nothrow_default_constructible_v<T>)
{
return T{};
}
return 值的初始化发生在被调用者(包含 return
语句的函数)的上下文中。也就是说,如果你想保留处理由 T
的默认构造函数抛出的异常的可能性,你应该 而不是 声明 myfunc
与 noexcept
.
我理解混乱的根源:根据 C++17 及更高版本中的值类别分类法,纯右值是构造对象的方法,而不是对象本身。考虑以下代码:
T foo() {
return {};
}
T t = foo();
在 C++14 中,return
语句和 t
的初始化是两个独立的步骤,尽管允许省略作为优化。在第一步中,return 对象(a.k.a. "foo()
")是从 {}
复制初始化的。在第二步中,t
是从 return 对象复制初始化的。显然,第一步发生在被调用者上下文中,第二步发生在调用者上下文中。
所以在 C++17 中,您可能会认为会发生类似的两步过程,只是使用修改后的纯右值概念:即,由于 foo()
是纯右值,您可能会认为 return
语句只是创建一个配方(在概念上可以表示为 [](void* p) { new (p) T{}; }
)并且所述配方是在被调用者上下文中创建的,而执行该配方以创建 t
将发生在调用者上下文中.如果是这种情况,那么对 T
的默认构造函数的实际调用将发生在调用者的上下文中,因此它抛出的任何异常都不会遇到被调用者的外括号。
但是,该标准有明确的语言否认这种解释:
the return statement initializes the glvalue result or prvalue result object of the (explicit or implicit) function call by copy-initialization [...] from the operand.
即t
的初始化是由return
语句自己完成的。这意味着 t
在实际离开被调用者的最外层块之前已完全初始化。例如,如果被调用者中有任何局部变量需要销毁,那实际上发生在 t
已经初始化之后(因此这种行为可能与 C++14 的行为不同)。正如很明显,此类局部变量的销毁发生在被调用者上下文中(因此,如果由此抛出异常,则搜索处理程序将遇到 foo
的最外层块),也很明显t
的初始化发生在被调用者上下文中。
我有一个这样的函数模板:
template <typename T>
constexpr auto myfunc() noexcept
{
return T{};
}
由于复制省略,这个函数模板是否保证是noexcept?如果在构造函数内部抛出异常,这是发生在函数内部还是外部?
复制省略所做的就是消除实际的复制或移动。一切都会发生 "as-if" 事情会在没有发生复制省略的情况下发生(当然除了复制本身)。
构造发生在函数内部。复制省略不会改变这一点。它所做的只是消除实际的 copy/move 发生(我在重复自己吗?)作为函数的 return 值被推回其调用者的结果。
因此,如果 class 的 default 构造函数抛出异常,则 noexcept
会破坏整个事情都来自高轨道。
如果 copy/move 构造函数抛出异常,由于 copy/move 没有发生,一切都会继续进行。
使用 gcc 7.3.1,使用 -std=c++17 编译:
template <typename T>
constexpr auto myfunc() noexcept
{
return T{};
}
class xx {
public:
xx() { throw "Foo"; }
};
int main()
{
try {
myfunc<xx>();
} catch (...) {
}
}
结果:
terminate called after throwing an instance of 'char const*'
现在,让我们混合起来,在复制和移动构造函数中抛出异常:
class xx {
public:
xx() { }
xx(xx &&) { throw "Foo"; }
xx(const xx &) { throw "Baz"; }
};
这运行没有异常。
这样做:
template <typename T> constexpr
auto myfunc() noexcept(std::is_nothrow_default_constructible_v<T>)
{
return T{};
}
return 值的初始化发生在被调用者(包含 return
语句的函数)的上下文中。也就是说,如果你想保留处理由 T
的默认构造函数抛出的异常的可能性,你应该 而不是 声明 myfunc
与 noexcept
.
我理解混乱的根源:根据 C++17 及更高版本中的值类别分类法,纯右值是构造对象的方法,而不是对象本身。考虑以下代码:
T foo() {
return {};
}
T t = foo();
在 C++14 中,return
语句和 t
的初始化是两个独立的步骤,尽管允许省略作为优化。在第一步中,return 对象(a.k.a. "foo()
")是从 {}
复制初始化的。在第二步中,t
是从 return 对象复制初始化的。显然,第一步发生在被调用者上下文中,第二步发生在调用者上下文中。
所以在 C++17 中,您可能会认为会发生类似的两步过程,只是使用修改后的纯右值概念:即,由于 foo()
是纯右值,您可能会认为 return
语句只是创建一个配方(在概念上可以表示为 [](void* p) { new (p) T{}; }
)并且所述配方是在被调用者上下文中创建的,而执行该配方以创建 t
将发生在调用者上下文中.如果是这种情况,那么对 T
的默认构造函数的实际调用将发生在调用者的上下文中,因此它抛出的任何异常都不会遇到被调用者的外括号。
但是,该标准有明确的语言否认这种解释:
the return statement initializes the glvalue result or prvalue result object of the (explicit or implicit) function call by copy-initialization [...] from the operand.
即t
的初始化是由return
语句自己完成的。这意味着 t
在实际离开被调用者的最外层块之前已完全初始化。例如,如果被调用者中有任何局部变量需要销毁,那实际上发生在 t
已经初始化之后(因此这种行为可能与 C++14 的行为不同)。正如很明显,此类局部变量的销毁发生在被调用者上下文中(因此,如果由此抛出异常,则搜索处理程序将遇到 foo
的最外层块),也很明显t
的初始化发生在被调用者上下文中。