Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it UB to resume a member function coroutine of an object whose lifetime has ended?

This question stems from this comment: Lambda lifetime explanation for C++20 coroutines

regarding this example:

auto foo() -> folly::coro::Task<int> {
    auto task = []() -> folly::coro::Task<int> {
        co_return 1;
    }();
    return task;
}

So the question is whether executing the coroutine returned by foo would result in UB.

"Calling" a member function (after the object's lifetime ended) is UB: http://eel.is/c++draft/basic.life#6.2

...any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...] The program has undefined behavior if:

[...]

-- the pointer is used to access a non-static data member or call a non-static member function of the object, or

However, in this example:

  • the () operator of the lambda is called while the lifetime of the lambda is still valid
  • It is then suspended,
  • then the lambda is destroyed,
  • and then the member function (operator ()) is resumed at some point afterwards.

Is this resumption considered undefined behavior?

like image 716
Mike Lui Avatar asked Mar 11 '20 13:03

Mike Lui


1 Answers

[dcl.fct.def.coroutine]p3:

The promise type of a coroutine is std::coroutine_traits<R, P1, ..., Pn>::promise_type, where R is the return type of the function, and P1 ... Pn are the sequence of types of the function parameters, preceded by the type of the implicit object parameter (12.4.1) if the coroutine is a non-static member function.

The implicit object parameter is in your example a const reference, and hence that reference will be dangling when execution is resumed after the closure object has been destroyed.

However, on the note of objects being destroyed during execution of a member function, this is indeed fine per se, and no other than the standard itself implies this in [basic]:

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. [...]

void B::mutate() {
  new (this) D2;    // reuses storage --- ends the lifetime of *this
  f();              // undefined behavior
  ... = this;       // OK, this points to valid memory
}

(NB: the above UB is because the implicit this is not laundered and still refers to the implicit object parameter.)

So your example appears to be well-defined, conditional on the idea that resumption of execution does not fall under the same rules as an original invocation. Note that the reference to the closure object might be dangling, but it's not accessed in any way between suspension and resumption.

like image 128
Columbo Avatar answered Nov 23 '22 23:11

Columbo