Search code examples
c++boostc++20boost-asioc++-coroutine

Is falling of the end of a boost::asio::awaitable<void> co routine without a co_return undefined behavior?


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.


Solution

  • 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)?

    TL;DR

    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()

    Example

    #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();
    }
    

    Reasoning

    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).

    and in 6

    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.