This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 116c. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2025-03-08


2995. Meaning of flowing off the end of a function

Section: 8.7.4  [stmt.return]     Status: open     Submitter: Brian Bi     Date: 2025-02-16

Consider:

  int foo() {
    struct S {
      ~S() noexcept(false) { throw 1; }
    } s;
  }

  int main() {
    try {
      foo();
    } catch (int x) {
      return x;
    }
  }

Does this program exit with code 1, or does it have undefined behavior per 8.7.4 [stmt.return] paragraph 4? Implementations differ.

The crucial question is whether the undefined behaviour caused by "flowing off the end" is before or after the closing brace of the function body, which is where the destruction of local variables occurs. Current consensus appears to solidify around "after".

This approach is consistent with the observation that the following two functions, given identical (valid) function bodies, should have the same semantics:

  void f() {{ ... }}
  void g() { ... }

Suggested resolution (option 1):

  1. Change in 6.9.3.1 [basic.start.main] paragraph 5 as follows:

    A return statement (8.7.4 [stmt.return]) in main has the effect of leaving the main function (destroying any objects with automatic storage duration) and calling std::exit with the return value as the argument. If control flows off the end of the compound-statement of main, the effect is equivalent to a return with operand 0 (see also 14.4 [except.handle]).
  2. Add a new paragraph at the end of 8.4 [stmt.block]:

    [Note 1: A compound statement defines a block scope (6.4 [basic.scope]). A declaration is a statement (8.8 [stmt.dcl]). —end note]

    Control is said to reach the end of a compound-statement

    • when its last statement completes normally and is not a jump-statement (8.7 [stmt.jump]) and the destruction of block variables with automatic storage duration, if any, completes normally (8.8 [stmt.dcl]), or
    • when the compound-statement is executed and contains no statements.

  3. Change in 8.7.4 [stmt.return] paragraph 4 as follows:

    Control is said to flow off the end of a function when control reaches the end of its compound-statement or that of a handler of its function-try-block (8.4 [stmt.block]). Flowing off the end of a constructor, a destructor, or a non-coroutine function with a cv void return type is equivalent to a return with no operand returns control to the caller, except as specified in 14.4 [except.handle]. Otherwise, flowing off the end of a function that is neither main (6.9.3.1 [basic.start.main]) nor a coroutine (9.5.4 [dcl.fct.def.coroutine]) results in undefined behavior.
  4. Change in 8.7.5 [stmt.return.coroutine] paragraph 3 as follows:

    If a search for the name return_void in the scope of the promise type finds any declarations, flowing off reaching the end of a coroutine's original function-body (8.4 [stmt.block]) is equivalent to a co_return with no operand; otherwise flowing off reaching the end of a coroutine's original function-body results in undefined behavior.
  5. Change in 9.5.4 [dcl.fct.def.coroutine] paragraph 6 as follows:

    [Note 1: If return_void is found, flowing off reaching the end of a coroutine coroutine's original function-body (8.4 [stmt.block]) is equivalent to a co_return with no operand. Otherwise, flowing off reaching the end of a coroutine coroutine's original function-body results in undefined behavior (8.7.5 [stmt.return.coroutine]). —end note]
  6. Change in 9.5.4 [dcl.fct.def.coroutine] paragraph 11 as follows:

    The coroutine state is destroyed when control flows off the end of the coroutine (8.7.4 [stmt.return]) or the destroy member function (17.12.4.6 [coroutine.handle.resumption]) of a coroutine handle (17.12.4 [coroutine.handle]) that refers to the coroutine is invoked. In the latter case, control in the coroutine is considered to be transferred out of the function (8.8 [stmt.dcl]). The storage for the coroutine state is released by calling a non-array deallocation function (6.7.6.5.3 [basic.stc.dynamic.deallocation]). If destroy is called for a coroutine that is not suspended, the program has undefined behavior.
  7. Change in 14.4 [except.handle] paragraph 14 as follows:

    The currently handled exception is rethrown if control reaches the end (8.4 [stmt.block]) of a handler of the function-try-block of a constructor or destructor. Otherwise, flowing off the end of the compound-statement of a handler of a function-try-block is equivalent to flowing off the end of the compound-statement of that function (see 8.7.4 [stmt.return]).

Possible resolution (option 2):

  1. Change in 6.9.3.1 [basic.start.main] paragraph 5 as follows:

    A return statement (8.7.4 [stmt.return]) in main has the effect of leaving the main function (destroying any objects with automatic storage duration) and calling std::exit with the return value as the argument. If control flows off the end of the compound-statement of main, the effect is equivalent to a return with operand 0 (see also 14.4 [except.handle]). The statement return 0; is appended to the outermost blocks of main (8.4 [stmt.block]).
  2. Add a new paragraph at the end of 8.4 [stmt.block]:

    [Note 1: A compound statement defines a block scope (6.4 [basic.scope]). A declaration is a statement (8.8 [stmt.dcl]). —end note]

    The outermost blocks of a function are

    • the compound-statement of its function-body or function-try-block (if any) and
    • the compound-statements of the handlers of its function-try-block (if any).
    [Note: A defaulted or deleted function has no outermost blocks. -- end note]

    A statement S is appended to a compound-statement by inserting S immediately prior to the closing }.

    The statement throw; is appended to the compound-statements of the handlers of a function-try-block of a constructor or destructor. The statement return; is appended to any other outermost block of a non-coroutine function with a cv void return type or with no return type.

    If control flows off the end of an outermost block of a function and the destruction of block variables with automatic storage duration, if any, completes normally (8.8 [stmt.dcl]), the behavior is undefined. [Note: Control never flows off the end of main (6.9.3.1 [basic.start.main]), a constructor, a destructor, or a void-returning function. -- end note]

  3. Change in 8.7.4 [stmt.return] paragraph 4 as follows:

    Flowing off the end of a constructor, a destructor, or a non-coroutine function with a cv void return type is equivalent to a return with no operand. Otherwise, flowing off the end of a function that is neither main (6.9.3.1 [basic.start.main]) nor a coroutine (9.5.4 [dcl.fct.def.coroutine]) results in undefined behavior.
  4. Change in 8.7.5 [stmt.return.coroutine] paragraph 3 as follows:

    If a search for the name return_void in the scope of the promise type finds any declarations, flowing off the end of a coroutine's function-body is equivalent to a co_return with no operand; otherwise flowing offthe end of a coroutine's function-body results in undefined behavior.
  5. Change in 9.5.4 [dcl.fct.def.coroutine] paragraph 6 as follows:

    If searches for the names return_void and return_value in the scope of the promise type each find any declarations, the program is ill-formed. Otherwise, if the search for the name return_void in the scope of the promise type finds any declarations, the statement co_return; is appended to the coroutine's original function-body (8.4 [stmt.block], 8.7.5 [stmt.return.coroutine]). [Note 1: If return_void is found, flowing off the end of a coroutine is equivalent to a co_return with no operand. Otherwise, flowing Flowing off the end of a coroutine coroutine's original function-body results in undefined behavior (8.7.5 [stmt.return.coroutine]). end note]
  6. Change in 9.5.4 [dcl.fct.def.coroutine] paragraph 11 as follows:

    The coroutine state is destroyed when control flows off the end of the coroutine at the end of its replacement function-body (8.4 [stmt.block]) or when the destroy member function (17.12.4.6 [coroutine.handle.resumption]) of a coroutine handle (17.12.4 [coroutine.handle]) that refers to the coroutine is invoked. In the latter case, control in the coroutine is considered to be transferred out of the function (8.8 [stmt.dcl]). The storage for the coroutine state is released by calling a non-array deallocation function (6.7.6.5.3 [basic.stc.dynamic.deallocation]). If destroy is called for a coroutine that is not suspended, the program has undefined behavior.
  7. Change in 14.4 [except.handle] paragraph 14 as follows:

    The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor. Otherwise, flowing off the end of the compound-statement of a handler of a function-try-block is equivalent to flowing off the end of the compound-statement of that function (see 8.7.4 [stmt.return]).