4

这个问题是"Constructor with by-value parameter & noexcept" 的对偶。该问题表明,按值函数参数的生命周期管理由调用函数处理;因此调用者处理发生的任何异常,并且被调用函数可以标记自己noexcept。我想知道如何处理输出端noexcept

MyType  MyFunction( SomeType const &x ) noexcept;

//...

void  MyCaller()
{
    MyType  test1 = MyFunction( RandomSomeType() );
    MyType  test2{ MyFunction( RandomSomeType() ) };
    //...
    test1 = MyFunction( RandomSomeType() );
    test2 = std::move( MyFunction(RandomSomeType()) );
    MyFunction( RandomSomeType() );  // return value goes to oblivion
}

假设返回值在MyFunction. 假设适当的特殊成员函数(复制/移动分配/构造)MyType 可能不是noexcept.

  1. 关于将返回值从被调用函数传输到调用者的 RVO/NRVO/Whatever-from-C++11 规则是否意味着无论noexcept相应的特殊成员函数的状态如何,传输总是成功不抛出?
  2. 如果上一个问题的答案是“否”,那么如果返回值传输抛出异常,该异常是否计入被调用函数或调用者?
  3. 如果上一个问题的答案是“被调用函数”,那么简单的noexcept标记 onMyFunction将导致调用std::terminate. MyFunctionnoexcept个人资料应该改成什么?当我在 Usenet 上询问这个问题时,一位受访者认为应该是std::is_nothrow_move_assignable<MyType>::value. (请注意,MyCaller使用了几种使用返回值的方法,但MyFunction不知道正在使用哪一种!答案必须涵盖所有情况。)如果MyType更改为可复制但不可移动,是否会有所不同?

noexcept因此,如果第二个和第三个问题的最坏情况是准确的,那么如果返回类型具有可抛出的移动,则任何按值返回的函数都不能有一个平原!现在具有可抛出动作的类型应该很少见,但模板代码仍然必须在is_nothrow_move_assignable每次使用按值返回时“弄脏”自己。

我认为让被调用函数负责是坏的:

MyType  MyFunction( SomeType const &x ) noexcept( ??? )
{
    //...
    try {
        return SOME_EXPRESSION;

        // What happens if the creation of SOME_EXPRESSION succeeds, but the
        // move-assignment (or whatever) transferring the result fails?  Is
        // this try/catch triggered?  Or is there no place lexically this
        // function can block a throwing move!?
    } catch (...) {
        return MyType();

        // Note that even if default-construction doesn't throw, the
        // move-assignment may throw (again)!  Now what?
    }
}

至少对我来说,这个问题在调用者端似乎是可以解决的(只需用try/包装移动分配catch),但从被调用函数端无法解决。我认为调用者必须处理这个问题,即使我们需要更改 C++ 的规则来这样做。或者至少需要某种缺陷报告。

4

2 回答 2

3

要回答您的部分问题,您可以询问某种类型是否不可构造:

#include <type_traits>

MyType  MyFunction( SomeType const &x )
    noexcept(std::is_nothrow_move_constructible<MyType>::value)
{
  // ....
}
于 2012-02-13T23:20:23.350 回答
1

我认为您的问题很困惑,因为您正在谈论从被调用者到调用者的“转移”,而这不是我们在 C++ 中使用的术语。考虑函数返回值的最简单方法是被调用者通过“返回槽”(被调用者构造并由调用者销毁的临时对象)与调用者通信。被调用者负责将返回值构造到“返回槽”中,调用者负责从“返回槽”中取出值(如果需要),然后销毁“返回槽”中剩余的任何内容。

MyType MyFunction(SomeType const &x) noexcept
{
    return SOME_EXPRESSION;
}

void MyCaller()
{
    MyType  test1 = MyFunction( RandomSomeType() );  // A
    MyType  test2{ MyFunction( RandomSomeType() ) };  // B
    //...
    test1 = MyFunction( RandomSomeType() );  // C
    test2 = std::move( MyFunction(RandomSomeType()) );  // D
    MyFunction( RandomSomeType() );  // E
}

首先:该语句return SOME_EXPRESSION;导致 的结果SOME_EXPRESSION被移动到 的“返回槽”中MyFunction。这一举动可能会被省略。如果移动没有被省略,那么MyType's move-constructor 将被调用。如果该移动构造函数抛出异常,您可以通过return自身周围的 try-block 或函数 try block来捕获异常。

案例A:里面有move-ctor MyFunction(可以省略),然后是move-ctor into test1(可以省略)。

案例B:同案例A

案例C:里面有 move-ctor MyFunction(可能被省略),然后是 move-assignment into test1

案例D:同案例C。调用 tostd::move并没有提供任何好处,而且编写它的风格很糟糕。

案例E:里面有 move-ctor MyFunction(可能会被省略),就是这样。

如果在 move-ctor 或 move-assignment into 期间抛出异常test1,您可以通过将处理的代码包装test1在 try-block 中来捕获这些异常。那时里面的代码MyFunction是完全不相关的;MyFunction不知道也不关心调用者将如何处理返回的对象。只有调用者知道,也只有调用者能够捕捉到调用者产生的异常。

于 2015-05-19T22:43:15.740 回答