4

This question is a dual of "Constructor with by-value parameter & noexcept". That question showed that lifetime management of a by-value function argument is handled by the calling function; therefore the caller handles any exceptions that happen and the called-function can mark itself noexcept. I'm wondering how the output end is handled with 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
}

Let's say that the return value is successfully created within MyFunction. And let's say that the appropriate special member functions (copy/move-assignment/construction) of MyType may not be noexcept.

  1. Do the RVO/NRVO/Whatever-from-C++11 rules concerning the transfer of return values from the called function to the caller mean that the transfer always succeeds no-throw no matter the noexcept status of the appropriate special member function?
  2. If the answer to the previous question is "no," then if the return value transfer throws, does the exception count against the called function or the caller?
  3. If the answer to the previous question is "the called function," then the plain noexcept marker on MyFunction will cause a call to std::terminate. What should MyFunction's noexcept profile be changed to? When I asked about this on the Usenet, a respondent thought it should be std::is_nothrow_move_assignable<MyType>::value. (Note that MyCaller used several methods of using a return value, but MyFunction won't know which one is in use! The answer has to cover all cases.) Does it make a difference if MyType is changed to be copyable but non-movable?

So if the worst-cases of the second and third questions are accurate, then any function that returns by value can't have a plain noexcept if the return type has a throw-able move! Now types with throw-able moves should be rare, but template code still has to "dirty" itself with is_nothrow_move_assignable every time return-by-value is used.

I think making the called-function responsible is broken:

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?
    }
}

This problem, to me at least, seems fixable at the caller's end (just wrap the move-assignment with a try/catch) but unfixable from the called-function's end. I think the caller has to handle this, even if we need to change the rules of C++ to do so. Or at least some sort of defect report is needed.

Community
  • 1
  • 1
CTMacUser
  • 1,996
  • 1
  • 16
  • 27
  • Correct me if I'm wrong, but RVO/NRVO/Move optimizations do not affect who constructs the object, only memory it is constructed at. Your object will be constructed directly at memory location supplied by caller (usually, caller's stack), but constructor call itself will be within stack frame of callee. So, if constructor throws, it'll be callee who "throws" it. – lapk Feb 14 '12 at 06:22
  • Sorry, I meant "within code segment of callee", not "within stack frame of callee". – lapk Feb 14 '12 at 07:31

2 Answers2

3

To answer part of your question, you can ask whether a certain type is nothrow constructible:

#include <type_traits>

MyType  MyFunction( SomeType const &x )
    noexcept(std::is_nothrow_move_constructible<MyType>::value)
{
  // ....
}
Xeo
  • 129,499
  • 52
  • 291
  • 397
  • 1
    But doesn't C++11 return-value handling always use move semantics, conceptually? Copy-semantics are used only if the type is non-movable, right? Further, don't the `is_*move_*` classes use/call the `*copy*` versions for non-movable types; in other words, you don't need to explicitly take non-movability into account and the `*move*` tests are all you would need? – CTMacUser Feb 14 '12 at 05:56
  • On another note, the transfer is always considered a move-construction, and not move-assignment (as I said someone told me the last time I asked this)? I hope the answer isn't "it depends," since `MyFunction`'s programmer has no idea whether the result will go inside a constructor/function parameter, on the right side of an assignment, or to oblivion at the time s/he is writing it. – CTMacUser Feb 15 '12 at 04:27
  • @CTMacUser: No, if you call `X x; x = foo();`, then it will be assignment. However, that part is on the callers side. The construction *into the return value* is a move-construction. – Xeo Feb 15 '12 at 07:44
1

I think your question is confused, in that you're talking about a "transfer" from callee to caller, and that's not a term that we use in C++. The simplest way to think about function return values is that the callee communicates with the caller via a "return slot" (a temporary object constructed by the callee and destroyed by the caller). The callee is responsible for constructing the return value into the "return slot", and the caller is responsible for getting the value out of the "return slot" (if desired) and then destroying whatever remains in the "return slot".

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
}

First: The statement return SOME_EXPRESSION; causes the result of SOME_EXPRESSION to get moved into the "return slot" of MyFunction. This move may be elided. If the move is not elided, then MyType's move-constructor will get called. If that move-constructor throws an exception, you can catch the exception via a try-block around the return itself, or via a function try block.

Case A: There's the move-ctor inside MyFunction (which may be elided), and then the move-ctor into test1 (which may be elided).

Case B: Same as case A.

Case C: There's the move-ctor inside MyFunction (which may be elided), and then the move-assignment into test1.

Case D: Same as case C. The call to std::move doesn't provide any benefit, and it's bad style to write it.

Case E: There's the move-ctor inside MyFunction (which may be elided), and that's it.

If exceptions are thrown during the move-ctor or move-assignment into test1, you can catch those by wrapping the code dealing with test1 in a try-block. The code inside MyFunction is completely irrelevant at that point; MyFunction doesn't know or care what the caller is going to do with the returned object. Only the caller knows, and only the caller is able to catch exceptions generated by the caller.

Quuxplusone
  • 23,928
  • 8
  • 94
  • 159