具有按值返回的函数 &noexcept

Function with by-value return & noexcept

本文关键字:函数 noexcept 返回      更新时间:2023-10-16

这个问题是"带有按值参数的构造函数&noexcept"的对偶问题。这个问题表明,按值函数参数的生存期管理是由调用函数处理的;因此调用方处理发生的任何异常,并且被调用函数可以将自己标记为CCD_ 1。我想知道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. 如果上一个问题的答案是"被调用的函数",那么MyFunction上的普通noexcept标记将导致对std::terminate的调用。noexcept0的noexcept配置文件应该更改为什么?当我在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++的规则。或者至少需要某种缺陷报告。

要回答部分问题,您可以询问某个类型是否是另一个可构造类型:

#include <type_traits>
MyType  MyFunction( SomeType const &x )
    noexcept(std::is_nothrow_move_constructible<MyType>::value)
{
  // ....
}

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

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的"返回槽"中。这一举措可能会被忽略。如果move没有被消除,那么MyType的move构造函数将被调用。如果move构造函数抛出异常,则可以通过return本身周围的try块或函数try块捕获异常。

案例A:在MyFunction中有一个移动的ctor(可能被忽略),然后将ctor移动到test1中(可能被消除)。

病例B:与病例A相同。

案例C:在MyFunction中有一个move ctor(可以省略),然后将赋值移动到test1中。

病例D:与病例C相同。对std::move的调用没有提供任何好处,而且编写它的风格也不好

案例EMyFunction内部有一个移动ctor(可能被忽略),就这样。

如果在将ctor或移动赋值到test1的过程中抛出异常,则可以通过将处理test1的代码封装在try块中来捕获这些异常。MyFunction内部的代码在这一点上是完全无关的;MyFunction不知道或不关心调用者将如何处理返回的对象。只有调用者知道,并且只有调用者能够捕获调用者生成的异常。