C++ coroutines and const reference parameters

161 Views Asked by At

From Note 3 in https://eel.is/c++draft/dcl.fct.def.coroutine#13

[Note 3: If a coroutine has a parameter passed by reference, resuming the coroutine after the lifetime of the entity referred to by that parameter has ended is likely to result in undefined behavior. — end note]

It is very clear that taking a parameter by reference in a coroutine may result in undefined behavior if a careless caller doesn't make sure the provided argument outlives the coroutine object.

But what about a const reference parameter initialized with a temporary? It would make sense to me if the standard guaranteed that the lifetime of the parameter is extended by the local reference but I can't find anything about it.

Is this defined behavior (assuming coro() uses obj after the first suspension) and if so what is it?

task<void> coro(const Obj& obj);

...

auto tsk = coro(Obj{});
co_await tsk;

It seems to me that if this is not permitted, const references can simply not be used as coroutine parameters for the risk of someone passing a temporary and assuming ordinary function behavior.

2

There are 2 best solutions below

5
Nicol Bolas On

It is very clear that taking a parameter by reference in a coroutine may result in undefined behavior if a careless caller doesn't make sure the provided argument outlives the coroutine object.

But what about a const reference parameter initialized with a temporary?

That's the definition of "a careless caller" who "doesn't make sure the provided argument outlives the coroutine object". There are no special provisions in this case. The lifetime of a temporary used as a function argument is extended to that of the full expression containing the function call. That function call being a coroutine doesn't change this.

Remember: being a coroutine is an implementation detail of the function. The caller (the one with the temporary) does not know if a function is a coroutine or not. So not only are there no special provisions, there cannot be special provisions.

It seems to me that if this is not permitted, const references can simply not be used as coroutine parameters for the risk of someone passing a temporary and assuming ordinary function behavior.

The general way C++ is designed assumes that the user knows what they're doing. The language frequently refrains from stopping you from doing something dangerous so long as it is possible to use that thing in a non-dangerous way.

That having been said, the coroutine function itself can simply internally copy any such parameters to its stack if they want. The coroutine can be set to not initially suspend, which guarantees that it will execute up to the first explicit suspension point. At which point, they can do whatever they want to make this usage safe. Maybe the function never uses the parameter after the first suspension point. Maybe something else.

So your example may have UB or may not, depending on what happens inside the coroutine.

0
PiotrNycz On

It is hard to add anything to @NicolBolas answer.

However, a short advice how to prevent from such bugs like referring to dangling reference inside coroutine.

Use std::reference_wrapper<const T> to prevent accidental use of references to temporary if you need to have const reference - or just pass by value if you need only value.

#include <functional>

task<> foo(const int& a)
{
   co_return;
}

task<> foo_no_temp(std::reference_wrapper<const int> a)
{
   co_return;
}

int main() {
    auto c1 = foo(1);
    // auto c2 = foo_no_temp(1); // does not compile
    const int a = 1;
    auto c3 = foo_no_temp(a);
}