我有一个这样的功能模板:
template <typename T>
constexpr auto myfunc() noexcept
{
return T{};
}
由于复制省略,此功能模板是否保证为noexcept?如果构造函数内部抛出异常,这是发生在函数内部还是外部?
答案 0 :(得分:6)
复制省略所做的就是消除实际的复制或移动。一切都在发生&#34; as-if&#34;事情发生时没有发生复制过程(当然除了复制本身)。
构造发生在函数内部。复制省略不会改变这一点。它所做的就是消除实际的复制/移动(我是否重复自己?),因为函数的返回值被推回到其调用者中。
因此,如果班级的 默认 构造函数引发异常,则noexcept
会从高轨道中取出整个内容。
如果复制/移动构造函数抛出异常,由于复制/移动没有发生,所以一切都继续发生。
使用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"; }
};
这没有例外。
答案 1 :(得分:0)
返回值的初始化发生在被调用者(包含 return
语句的函数)的上下文中。也就是说,如果您想保留处理由 T
的默认构造函数抛出的异常的可能性,您应该不要用 {{1} 声明 myfunc
}.
我理解混淆的根源:根据 C++17 及更高版本中的值类别分类法,纯右值是构造对象的方法,而不是对象本身。考虑以下代码:
noexcept
在 C++14 中,T foo() {
return {};
}
T t = foo();
语句和 return
的初始化是两个独立的步骤,尽管允许省略作为优化。在第一步中,返回对象(又名“t
”)从 foo()
复制初始化。在第二步中,{}
是从该返回对象复制初始化的。显然,第一步发生在被调用者上下文中,第二步发生在调用者上下文中。
因此在 C++17 中,您可能会认为发生了类似的两步过程,只是修改了纯右值概念:即,由于 t
是纯右值,您可能会认为 {{1} } 语句简单地创建一个配方(概念上可以表示为 foo()
)并且所述配方是在被调用者上下文中创建的,而该配方的执行以创建 return
将发生在调用者上下文中。如果是这种情况,那么对 [](void* p) { new (p) T{}; }
的默认构造函数的实际调用将发生在调用者的上下文中,因此它抛出的任何异常都不会遇到被调用者的外大括号。
然而,该标准有明确的语言否认这种解释:
<块引用>return 语句通过从操作数复制初始化 [...] 来初始化(显式或隐式)函数调用的左值结果或右值结果对象。
即t
的初始化是由T
语句本身完成的。这意味着 t
在被调用者的最外层块实际离开之前已完全初始化。例如,如果被调用者中有任何需要销毁的局部变量,这实际上发生在 return
已经初始化之后(因此这种行为可能与 C++14 的行为不同)。正如很明显,此类局部变量的销毁发生在被调用者上下文中(因此,如果因此抛出异常,则对处理程序的搜索将遇到最外层的 t
块),同样清楚的是t
的初始化发生在被调用者上下文中。
答案 2 :(得分:-2)
这样做:
template <typename T> constexpr
auto myfunc() noexcept(std::is_nothrow_default_constructible_v<T>)
{
return T{};
}