复制返回值和noexcept的省略

时间:2018-04-21 15:27:55

标签: c++ exception optimization noexcept

我有一个这样的功能模板:

template <typename T>
constexpr auto myfunc() noexcept
{
    return T{};
}

由于复制省略,此功能模板是否保证为noexcept?如果构造函数内部抛出异常,这是发生在函数内部还是外部?

3 个答案:

答案 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{};
}