复制省略 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 的默认构造函数抛出的异常的可能性,你应该 而不是 声明 myfuncnoexcept.

我理解混乱的根源:根据 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 的初始化发生在被调用者上下文中。