返回值的复制省略和noexcept

5

我有一个类似于这样的函数模板:

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

因为复制省略,这个函数模板是否保证是noexcept的?如果在构造函数内部抛出异常,这会发生在函数内部还是外部?

3个回答

6
所有的复制省略只是消除了实际的复制或移动。一切都发生在“好像”没有发生复制省略的情况下(当然,除了复制本身)。
构造函数发生在函数内部。复制省略不会改变这一点。它所做的只是消除实际的复制/移动,因为函数的返回值被推回到其调用者时(我是否重复了自己?)。
因此,如果类的默认构造函数抛出异常,则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"; }
};

这段代码可以顺利执行,没有出现异常。


0

返回值的初始化发生在被调用者的上下文中(包含return语句的函数)。也就是说,如果您想保留处理由T的默认构造函数抛出的异常的可能性,您不应该使用noexcept声明myfunc

我理解混淆的来源:根据C++17及更高版本中的值类别分类法,prvalue是构造对象的配方,而不是对象本身。请考虑以下代码:

T foo() {
    return {};
}
T t = foo();

在C++14中,return语句和t的初始化是两个独立的步骤,尽管可以通过省略作为优化。在第一步中,返回对象(也称为"foo()")从{}进行复制初始化。在第二步中,t从该返回对象进行复制初始化。显然,第一步发生在调用者上下文中,第二步发生在调用者上下文中。
因此,在C++17中,您可能认为会发生类似的两步过程,只不过使用了修订后的prvalue概念:即,由于foo()是一个prvalue,您可能认为return语句仅创建一个配方(可以概念上表示为[](void* p) { new (p) T{}; }),并且该配方在调用者上下文中创建,而执行该配方以创建t将在调用者上下文中发生。如果是这种情况,则对T的默认构造函数的实际调用将发生在调用者的上下文中,因此任何由它引发的异常都不会遇到调用者的外部大括号。
但是,标准明确否认了这种解释。
返回语句通过从操作数进行复制初始化[...]来初始化(显式或隐式)函数调用的glvalue结果或prvalue结果对象。也就是说,t的初始化是由return语句本身完成的。这意味着在调用者的最外层块实际上离开之前,t已经完全初始化。例如,如果调用方中有任何需要销毁的局部变量,则在t已经初始化之后才会发生(因此,此行为与C++14的行为可能不同)。正如清楚地表明这样的局部变量的销毁发生在调用方上下文中(因此,如果抛出异常,则搜索处理程序的位置将遇到foo的最外层块),t的初始化也发生在调用方上下文中。

-2

像这样做:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接