Document Number: | P0070R00 |
---|---|
Date: | 2015-09-12 |
Project: | Programming Language C++, Evolution |
Revises: | none |
Reply to: | gorn@microsoft.com |
One of the concerns raised in Lenexa was that having a tag on the coroutine
definition would be useful to help the compiler process the body of the function
since coroutines require special handling of the return statements, but it is
not known in advance whether a function is a coroutine or not until we encounter
await
or yield
expressions. The vote taken (but not
recorded on the wiki) was against having a tag. However, one of the suggestions
made during the session was to introduce a new keyword, co_return
that must be used in place of return
in coroutines and coroutines
only. This paper explores this suggestion and recommends against it.
The updated wording is provided in a separate paper P0057R00.
After Lenexa, we implemented "return before await" in MSVC compiler by
deferring semantic analysis of the return statements until the end of the
function. It did not require heroic effort as we already do several rewriting of
the expressions within the body of the function after we've seen its body prior
to emitting low-level representation to the back-end. If the function declared
return type is auto
or decltype(auto)
, we deduce the
return type on the first return statement seen as implied by
N4527/[dcl.spec.auto]/11.
In GCC, according to a person familiar with it, a similar processing was done to handle named return value optimization, namely, GCC had to first observe all the return statements in the function before deciding on how to handle them, which meant that it had to parse until the end of the function before finalizing processing of the return statements.
This new information lessens the technical difficulty motivation to
introducing new statement / keyword co_return
.
Another argument raised was that without a different kind of return statement, coroutines would be confusing to the reader. For example, consider the following coroutine:
future<int> deep_thought() {
await 7'500'000'000h;
return 42;
}
One may find it confusing that in a function returning
future<int>
we are allowing a return of an integer value.
Intuitive reasoning behind this syntax is that coroutine when suspended must
return back to the caller, and since the eventual result of the computation
reported via return
is not available yet, the caller receives a
placeholder object, such as future<int>
that can be used to
obtain eventual value once available. Thus, in a coroutine, return
statement indicates that the function is terminated, control needs to be
returned to the caller and the result of the computation of the function to be
provided to the interested party. This is similar to a normal function, with the
exception that in a coroutine, return
statement provides an
eventual return value, as opposed to immediate return value
and the interested party is not necessarily the function to which we return, but
the one consuming the result from the future<int>
.
Indeed, all of the programming languages that adopted an await construct end up making the same determination with respect to the return statement.
// Python // PHP/HACK
async def deep_thought(n): async function DeepThought(): Awaitable<int> {
await delay(7500000000); await Awaitable.Delay(7500000000);
return 42 return 42;
}
// Dart // C#
Future<int> DeepThought() async { async Task<int> DeepThought() {
await Future.Delay(7500000000); await Task.Delay(7500000000);
return 42; return 42;
} }
Requiring a programmer to use a different kind of return statement in coroutines, seems unnecessary, given the practical experience of using similar constructs in other languages.
Should we keep coroutines using await
as proposed, but require
to use a co_return
statement only in generators?
First, unlike coroutines in other languages, in C++, coroutines are
generalized functions. Library author defining coroutine_traits
decides whether the function to which the trait applies will have the semantics
of a generator, a task, an asynchronous generator, or even a regular function.
Having a different kind of return statement breaks this property.
Second, comparing with existing practice in other language one finds that 3 in 4 chose not to mangle the return statement in generators.
// Python // PHP/HACK
def gen(n): function gen() {
yield 5 yield 5;
return return;
}
// Dart // C#
Iterable gen() sync* { IEnumerable<int> gen() {
yield 5; yield return 5;
return; yield break;
} }
Even in the last case, C# design team preference was to use yield
expr
as a yield statement, but, because C# 1.0 was out for more than 5
years, they did not want to break existing customers, they end up with
yield return
and that led to the decision to use yield
break
.
Using return statement in coroutines is existing practice in other languages. There is no need reason to believe that C++ developers are more easily confused than developers in other languages and given that implementation experience showed that this is technically feasible, we recommend to stay with the return statement in the coroutines.
Python: PEP 0492 --
Coroutines with async and await syntax (https://www.python.org/dev/peps/pep-0492/)
Hack: Hack Language
Reference (http://docs.hhvm.com/manual/en/hack.async.php)
[C#]:
C# 5.0 Language Specification (https://msdn.microsoft.com/en-us/library/ms228593(v=vs.110).aspx)
Dart: Spicing Up Dart
with Side Effects (http://queue.acm.org/detail.cfm?id=2747873)
N4527:
Working Draft, Standard for Programming Language C++ (http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4527.pdf)
N4402: Resumable Functions
(revision 4) (https://isocpp.org/files/papers/N4402.pdf)
P0057r00: Wording for Coroutines, Revision
3 (http://wg21.link/P0057R00)