Resolving confusion over how to return from a C++ coroutine

Raymond

A customer was having trouble writing a coroutine using C++/WinRT. This function compiled successfully:

winrt::IAsyncOperation<bool> HelperFunction()
{
    /* no other co_return statements */

    co_return true;
}

But once they added a condition, it stopped compiling successfully:

winrt::IAsyncOperation<bool> MainFunction()
{
    ...
    if (condition) {
        ...
        co_return HelperFunction(); // Fails to compile
    }

    co_return false;
}

The error message is

error C2664: 'void std::experimental::coroutine_traits<winrt::Windows::Foundation::IAsyncOperation<bool>>::promise_type::return_value(TResult &&) noexcept': cannot convert argument 1 from 'winrt::Windows::Foundation::IAsyncOperation<bool>' to 'TResult &&'
with
[
    TResult=bool
]
message : Reason: cannot convert from 'winrt::Windows::Foundation::IAsyncOperation<bool>' to 'TResult'
with
[
    TResult=bool
]
message : No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called

What’s going on here?

The co_return statement takes the thing being co-returned and passes it to the promise’s return_value method (or if you co_return nothing, calls the promise’s return_void method with no parameters). Although the language imposes no semantics upon this action, the intent is that this is how you produce the asynchronous result of the coroutine: The asynchronous result of the coroutine is the thing that the caller gets when they co_await the coroutine.

Declaration IAsyncAction f() IAsyncOperation<T> f() fire_and_forget f()
Return type IAsyncAction IAsyncOperation<T> fire_and_forget
Using return T return IAsyncAction(...); return IAsyncOperation<T>(...); return {};
Result type void T void
Using co_return T co_return; co_return T(...); co_return;

If you use the return keyword, then you must return the coroutine type. This follows the rules of the C++ language that you’re familiar with: If your function says that it returns something, then the thing you return needs to be that something (or something convertible to it).

What’s new for coroutines is the co_return keyword. If you use the co_return keyword, then the thing you co_return needs to be the coroutine result (or something convertible to it).

You have to pick a side: Either return everywhere in your function or co_return everywhere in your function. You can’t mix-and-match. That would result in Main­Function() being part-coroutine and part not-coroutine, which the language doesn’t support. You’re either a coroutine or you’re not.

Writing co_return HelperFunction(); is trying to return an IAsyncOperation<bool> as the result of the coroutine. But the coroutine result isn’t a IAsyncOperation<bool>. It’s just a bool.

And that’s what the compiler error message is trying to say, with compiler-colored glasses: “Cannot convert IAsyncOperation<bool> to bool.” You co_returned an IAsyncOperation<bool>, but the only thing that the IAsyncOperation<bool> knows how to co_return is a bool, and the compiler is unable to perform the conversion.

What you need to do is co_return a bool somehow.

The customer discovered on their own that adding a co_await fixed the problem:

winrt::IAsyncOperation<bool> MainFunction()
{
    ...
    if (condition) {
        co_return co_await HelperFunction(); // added co_await
    }

    co_return false;
}

But the customer was unsure of themselves. “Why is co_await needed? Are there any unintended consequences?”

The co_await keyword instructs the compiler to generate code to suspend the current coroutine Main­Function() and resume execution when Helper­Function() produces a result. Since Helper­Function() is itself a IAsyncOperation<bool>, that result will also be a bool. You can then co_return that bool, which makes it the result of the Main­Function() coroutine.

Bonus chatter: The customer also found, in their experimentation, that this version also compiled successfully:

winrt::IAsyncOperation<bool> MainFunction()
{
    if (condition) co_return true;
    return false;
}

How does this work? It seems to be breaking the rules above, because we are using return with the result type, and we’re mixing return and co_return within the same function body.

Yes, this code should not compile.

What you’re seeing is a backward compatibility behavior of the Visual C++ compiler: When coroutines were being developed, the original idea was to overload the return. If you returned something that matched the declared return type, then it was treated as producing the return value of the function. But if you returned something that matched the result type, then the function transformed into a coroutine, and you were producing the result of the coroutine.

My guess is that this syntax was chosen to align with the C# and JavaScript languages, both of which overload the return statement in this way.

Ultimately, however, the ambiguity was too much,¹ and the coroutine specification that was ratified created new keywords to make explicit whether the function body was a classic function or a coroutine. The Visual C++ compiler retains the old syntax for backward compatibility with existing code that was written to the pre-ratified standard.

It appears that an artifact of this backward compatibility is that the compiler accepts the reverse error:

winrt::IAsyncOperation<bool> MainFunction()
{
    co_return HelperFunction();
}

This uses co_return with the return type instead of the result type. Somehow, the compiler accepts it even though it’s not required by backward compatibility. (My guess is that there’s some compatibility code that merges return and co_return, and while that takes care of the compatibility issue, it also makes the compiler accept other things inadvertently.

It also seems that the /permissive- flag doesn’t turn off this compatibility behavior.

¹ Consider a class that is designed to be the return type of a coroutine.

template<typename T>
class task
{
    /* stuff required to be a coroutine return type */
};

task<int> calculate()
{
    /* do some calculations */
    co_return value;
}

This hypothetical task type supports being used as the return type of a coroutine, and our sketch of a calculate() function calculates a value and co_returns it.

But suppose we added a new constructor:

template<typename T>
class task
{
public:
    /* create a task that has already completed with a value */
    task(T const& resolved);

    /* existing stuff required to be a coroutine return type */
};

This new constructor provides a way to create an already-completed task by passing the result directly to the constructor.

Given this new constructor, the following code would become ambiguous under the pre-standardized version that used return for both normal return and coroutine return:

task<int> calculate()
{
    /* do some calculations */
    return value;
}

Is this a plain non-coroutine function that returns a task with the resolved constructor? Or is this a coroutine function that produces a task from the coroutine promise via return_value()? Both interpretations would be valid here.

Changing the keyword to co_return for coroutines removes this ambiguity.

5 comments

Leave a comment

  • GL

    My guess is that this syntax was chosen to align with the C# and JavaScript languages, both of which overload the return statement in this way.

    I thought C# and JavaScript had the “async” marker on the method so the compiler knew which kind of “return” was being used in the body. On the other hand, can you do something like the following in C++?

    IAsyncOperation<bool> HelperFunction();
    IAsyncOperation<bool> MainFunction()
    {
        if (condition) return HelperFunction();
        co_return false;
    }
    • Raymond ChenMicrosoft employee

      See paragraph that begins “You have to pick a side”. C# and JavaScript uee the “async” attribute on the function to indicate whether it is a coroutine. C++ uses the “co_” keywords inside the function body to indicate whether it is a coroutine. I suspect C++ went the way it did because “async” is not part of the function signature.

      • Nathan Williams

        The confusion is understandable. While C# does overload the return keyword, it is not in the same way as was originally intended for C++, as the paragraph suggests. In the original C++ coroutine spec, the behavior of the return keyword was determined by the type of its argument, which is overloading in the same sense as function overloading. In C#, the behavior of the return keyword is determined by the modifiers on the method.

    • 紅樓鍮

      As an unrelated side note, if the branch to HelperFunction() is before any co_await then you could factor the co_awaiting part out into a lambda:

      // do not use code in italics in production
      IAsyncOperation<bool> MainFunction() {
        if (condition)
          return HelperFunction();
        return [=]() -> IAsyncOperation<bool> {
          co_await something();
          co_return false;
        }();
      }

      However, you’d have to be wary about its bad implications (blog.stephencleary. com/2016/12/eliding-async-await.html):

      1. Object lifetimes are now a mess (oldnewthing/20190116-00/?p=100715):
        // do not use code in italics in production
        IAsyncOperation<bool> MainFunction() {
          http_client cl = make_http_client();
          if (cl.some_codition())
            return HelperFunction();
          return [cl = std::move(cl)]() mutable -> IAsyncOperation<bool> {
            co_await cl.download();
            return false;
          }(); // cl destructs as part of the lambda
        }
      2. error handling: any exceptions originating from before the async lambda will now be thrown directly out of MainFunction(), not stored in the returned IAsyncOperation. If you need those exceptions to be stored in the returned IAsyncOperation, as an entirely async MainFunction() would do, you’ll have to catch them.

      Fixing the above problems would require code like this:

      template <typename T>
      T rethrow_coroutine(std::exception_ptr e = std::current_exception()) {
        std::rethrow_exception(std::move(e));
        co_await std::suspend_never();
      }
      
      IAsyncOperation<bool> MainFunction() try {
        http_client cl = make_http_client();
        if (cl.some_codition())
          return HelperFunction();
        return [](http_client cl) -> IAsyncOperation<bool> {
          co_await cl.download();
          return false;
        }(std::move(cl));
      } catch (...) {
        return rethrow_coroutine<IAsyncOperation<bool>>();
      }

      In other words, you’d be vastly better off to just co_return co_await. (Coding like this buys you little if the compiler can allocate HelperFunction()‘s coroutine frame inline in MainFunction()‘s frame, although I doubt IAsyncAction et al. are designed with that optimization in mind, which could encourage you to actually write code like above.)

  • Ian Yates

    If you had them split the return from the calculation, by assigning to a temporary variable, I suspect they may have seen it more obviously too.