Is falling of the end of a boost::asio::awaitable co routine without a co_return undefined behavior (UB)? I could not find anything to this in the boost documentation. Cppreference says, its UB when there is no Promise::return_void()
, in contrast to promise.return_void()
which it says will also be called, for all cases including co_return
and falling of the end of the co-routine.
However I found different behavior, one suggesting UB: Code on Compiler Explorer
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
boost::asio::awaitable<void> coroutineA() {
std::cout << "coroutineA" << std::endl;
}
boost::asio::awaitable<void> coroutineB() {
std::cout << "coroutineB" << std::endl;
co_return;
}
int main() {
boost::asio::io_context context;
auto waitA= boost::asio::co_spawn(context,
coroutineA(), boost::asio::use_future); //Seems to cause UB
auto waitB = boost::asio::co_spawn(context,
coroutineB(), boost::asio::use_future);
std::thread t([&context] () {
context.run();
});
waitA.get();
waitB.get();
t.join();
}
Execution of coroutineA
leads to a crash with this simple code, in other demo cases it leads to a non 0 program code but non blocking execution.
The only clue I could find was in the boost code: Boost 1.82 asio/impl/awaitable.hpp
defines void return_void()
for both template <typename Executor> class awaitable_frame<void, Executor>
and template <typename Executor> class awaitable_frame<awaitable_thread_entry_point, Executor>
. The former one should, to my understanding, the promise type we have for both your coroutines.
Cppreference states, both cases co_return
and falling of the end will call promise.return_void()
, i.e. awaitable_frame::return_void()
, which I would argue is defined.
So again, is falling of the end of a boost::asio::awaitable co routine without a co_return undefined behavior (UB)? If yes, why? Could someone point me to the documentation, or better show me the flaw in my reasoning.
After reading the C++20 draft and further experimentation I feel confident enough to say:
Is falling of the end of a boost::asio::awaitable co routine without a co_return undefined behavior (UB)?
It depends. If there is any other coroutine keyword in the function body like co_yield
, co_return
(like in an if branch) or co_await
, then it's not UB. If there is not at least one co routine keyword in the function, it is UB. This is true for all C++20 coroutines where the used promise defines return_void()
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
boost::asio::awaitable<void> coroutineA(boost::asio::io_context& context) {
boost::asio::steady_timer t(context);
t.expires_after(std::chrono::seconds(1));
co_await t.async_wait(boost::asio::use_awaitable);//now this is a coroutine, falling of ok
std::cout << "coroutineA" << std::endl;
}//only ok thanks to 2 lines above
boost::asio::awaitable<void> coroutineB() {
if (false) {
co_return; //Now this is a coroutine, falling of ok
}
std::cout << "coroutineB" << std::endl;
}//only ok thanks to 3 lines above
int main() {
boost::asio::io_context context;
auto waitA= boost::asio::co_spawn(context,
coroutineA(context), boost::asio::use_future);
auto waitB = boost::asio::co_spawn(context,
coroutineB(), boost::asio::use_future);
std::thread t([&context] () {
context.run();
});
waitA.get();
waitB.get();
t.join();
}
The C++20 Standard and later (I uses Draft N4861 and the latest) states in 9.5.4 Coroutine definitions [dcl.fct.def.coroutine] in 1:
A function is a coroutine if its function-body encloses a coroutine-return-statement (8.7.4), an await-expression (7.6.2.3), or a yield-expression (7.6.17).
The unqualified-ids return_void and return_value are looked up in the scope of the promise type. If both are found, the program is ill-formed. [Note: If the unqualified-id return_void is found, flowing off the end of a coroutine is equivalent to a co_return with no operand. Otherwise, flowing off the end of a coroutine results in undefined behavior (8.7.4). — end note]
So under the assumption that the used promise defines return_void()
correctly, which e.g. boost::asio::awaitable<void>
does i.e. in this case, falling of the end of a coroutine is fine.
BUT it is only a coroutine if, and only if (iff), its function body contains i.e. "encloses" one of the current 3 coroutine keywords: co_return
, co_yield
, co_await
. It does not matter if they are actually executed. Otherwise we simply have a function, not a coroutine, and falling of the end of a non void function is undefined behavior (UB) i.e. really really bad (crashes, works sometime and so on).
IMHO it seems the C++ committee refrained for some reason to introduce an async
keyword for function declarations, like JS and Python does. If C++ would have one, this behavior would be not that surprising for C++ programmers I guess.