constexpr coroutines
This paper is proposing making coroutines functional during constant evaluation. Even when most of use-cases for coroutines are based on I/O and event based, coroutines are still useful for compile-time based computation, eg. std::generator
.
Why would you want to do this?
Well, you just told me coroutines are the best way to solve some problems, so wouldn't I also want to use the Best Way at compile time? (quote from Jason Turner, co-author of "constexpr all the things" talk)
Simple example
When this paper is merged into the standard, users will be able to use std::generator
-like coroutines to generate or calculate data.
template <typename T> constexpr auto fib() -> std::generator<T> {
T a = 0;
T b = 1;
co_yield a;
do {
co_yield b;
auto tmp = b;
b += a;
a = tmp;
} while (true);
}
template <typename T, size_t N> constexpr auto calculate_fibonnaci() {
auto res = std::array<T, N>{};
std::ranges::copy(fib<T>() | std::views::take(N), res.begin());
return res;
}
constexpr auto cached_fibonnaci = calculate_fibonnaci<unsigned, 20>();
Implementation experience
Partially implemented in clang available on my github, implementation should be ready for its presentation at Wroclaw meeting, and also will be soon available on compiler explorer (thanks Matt!).
Most of functionality needed was already present in Clang, it was mostly about removing checks in the parser.
Another part was implementing the functionality in Clang's interpreter and there I needed to add fibers (stackfull coroutines) as the interpreter recursive walks over AST. Ability to save interpreter's stack content did minimize impact of the change to only resuming, suspending, and variable storage and life-time management.
At the end of evaluation the interpret needs to check objects holding fibers if there is still any coroutine not released, if there is it report similar error as when there is an unreleased memory allocation.
Hardest problem was implementing local "stack", as createLocal
function was designed around idea of having only one branch of evaluation. This I solved by providing context of currently evaluated coroutine in EvalInfo
and switching it on every suspension / resume of a coroutine.
Impact on existing code
None, this is a pure extension, it allows code to be constexpr which wasn't case before.
Intention for wording changes
Remove all obstacles blocking coroutines from being constant evaluatable. Make sure all coroutines are destroyed at end of constant evaluation.
Proposed changes to wording
7.7 Constant expressions [expr.const]
- either it has an initializer or its default-initialization results in some initialization being performed, and
- the full-expression of its initialization is a constant expression when interpreted as a constant-expression, except that if o is an object, that full-expression may also invoke constexpr constructors for o and its subobjects even if those objects are of non-literal class types.
- V is constexpr,
- V is not initialized to a TU-local value, or
- P is in the same translation unit as D.
- a variable that is usable in constant expressions, or
- a template parameter object, or
- a string literal object, or
- a temporary object of non-volatile const-qualified literal type whose lifetime is extended ([class.temporary]) to that of a variable that is usable in constant expressions, or
- a non-mutable subobject or reference member of any of the above.
- this ([expr.prim.this]), except
- in a constexpr function ([dcl.constexpr]) that is being evaluated as part of E or
- when appearing as the postfix-expression of an implicit or explicit class member access expression ([expr.ref]);
- a control flow that passes through
a declaration of a block variable ([basic.scope.block]) with
static ([basic.stc.static]) or
thread ([basic.stc.thread]) storage duration,
unless that variable is usable in constant expressions;
[Example 1: constexpr char test() { static const int x = 5; static constexpr char c[] = "Hello World"; return *(c + x); } static_assert(' ' == test()); — end example]
- an invocation of a non-constexpr function;68
- an invocation of an undefined constexpr function;
- an invocation of an instantiated constexpr function that is not constexpr-suitable;
- an invocation of a virtual function ([class.virtual]) for an object whose dynamic type is constexpr-unknown;
- an expression that would exceed the implementation-defined limits (see [implimits]);
- an operation that would have undefined or erroneous behavior as specified in [intro] through [cpp], excluding [dcl.attr.assume] and [dcl.attr.noreturn];69
- an lvalue-to-rvalue conversion unless
it is applied to
- a non-volatile glvalue that refers to an object that is usable in constant expressions, or
- a non-volatile glvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
- an lvalue-to-rvalue conversion that is applied to a glvalue that refers to a non-active member of a union or a subobject thereof;
- an lvalue-to-rvalue conversion that is applied to an object with an indeterminate value;
- an invocation of an implicitly-defined copy/move constructor or copy/move assignment operator for a union whose active member (if any) is mutable, unless the lifetime of the union object began within the evaluation of E;
- in a lambda-expression,
a reference to this or to a variable with
automatic storage duration defined outside that
lambda-expression, where
the reference would be an odr-use ([basic.def.odr], [expr.prim.lambda]);
[Example 2: void g() { const int n = 0; [=] { constexpr int i = n; // OK, n is not odr-used here constexpr int j = *&n; // error: &n would be an odr-use of n }; } — end example][Note 3:If the odr-use occurs in an invocation of a function call operator of a closure type, it no longer refers to this or to an enclosing automatic variable due to the transformation ([expr.prim.lambda.capture]) of the id-expression into an access of the corresponding data member.— end note][Example 3: auto monad = [](auto v) { return [=] { return v; }; }; auto bind = [](auto m) { return [=](auto fvm) { return fvm(m()); }; }; // OK to capture objects with automatic storage duration created during constant expression evaluation. static_assert(bind(monad(2))(monad)() == monad(2)()); — end example]
- a conversion from a prvalue P of type “pointer to cv void” to a type “cv1 pointer to T”, where T is not cv2 void, unless P is a null pointer value or points to an object whose type is similar to T;
- a reinterpret_cast ([expr.reinterpret.cast]);
- a modification of an object ([expr.ass], [expr.post.incr], [expr.pre.incr]) unless it is applied to a non-volatile lvalue of literal type that refers to a non-volatile object whose lifetime began within the evaluation of E;
- an invocation of a destructor ([class.dtor]) or a function call whose postfix-expression names a pseudo-destructor ([expr.call]), in either case for an object whose lifetime did not begin within the evaluation of E;
- a new-expression ([expr.new]),
unless either
- the selected allocation function is a replaceable global allocation function ([new.delete.single], [new.delete.array]) and the allocated storage is deallocated within the evaluation of E, or
- the selected allocation function is
a non-allocating form ([new.delete.placement])
with an allocated type T, where
- the placement argument to the new-expression points to an object that is pointer-interconvertible with an object of type T or, if T is an array type, with the first element of an object of type T, and
- the placement argument points to storage whose duration began within the evaluation of E;
- a delete-expression ([expr.delete]), unless it deallocates a region of storage allocated within the evaluation of E;
- a call to an instance of std::allocator<T>::allocate ([allocator.members]), unless the allocated storage is deallocated within the evaluation of E;
- a call to an instance of std::allocator<T>::deallocate ([allocator.members]), unless it deallocates a region of storage allocated within the evaluation of E;
- an await-expression ([expr.await]);
- a yield-expression ([expr.yield]);
- a three-way comparison ([expr.spaceship]), relational ([expr.rel]), or equality ([expr.eq]) operator where the result is unspecified;
- a throw-expression ([expr.throw]);
- a dynamic_cast ([expr.dynamic.cast]) or typeid ([expr.typeid]) expression on a glvalue that refers to an object whose dynamic type is constexpr-unknown or that would throw an exception;
- an asm-declaration ([dcl.asm]);
- an invocation of the va_arg macro ([cstdarg.syn]);
- a non-constant library call ([defns.nonconst.libcall]); or
- a goto statement ([stmt.goto]). [Note 4: — end note]
- an operation that has undefined behavior as specified in [library] through [exec],
- an invocation of the va_start macro ([cstdarg.syn]),
- a call to a function that was previously declared with the noreturn attribute ([dcl.attr.noreturn]) and that call returns to its caller, or
- a statement with an assumption ([dcl.attr.assume])
whose converted conditional-expression,
if evaluated where the assumption appears,
would not disqualify E from being a core constant expression and
would not evaluate to true. [Note 5:E is not disqualified from being a core constant expression if the hypothetical evaluation of the converted conditional-expression would disqualify E from being a core constant expression.— end note]
- it is not of class type nor (possibly multidimensional) array thereof, or
- it is of class type or (possibly multidimensional) array thereof, that class type has a constexpr destructor, and for a hypothetical expression E whose only effect is to destroy a, E would be a core constant expression if the lifetime of a and its non-mutable subobjects (but not its mutable subobjects) were considered to start within E.
- user-defined conversions,
- lvalue-to-rvalue conversions ([conv.lval]),
- array-to-pointer conversions ([conv.array]),
- function-to-pointer conversions ([conv.func]),
- qualification conversions ([conv.qual]),
- integral promotions ([conv.prom]),
- integral conversions ([conv.integral]) other than narrowing conversions ([dcl.init.list]),
- floating-point promotions ([conv.fpprom]),
- floating-point conversions ([conv.double]) where the source value can be represented exactly in the destination type,
- null pointer conversions ([conv.ptr]) from std::nullptr_t,
- null member pointer conversions ([conv.mem]) from std::nullptr_t, and
- function pointer conversions ([conv.fctptr]),
- if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
- if the value is an object of scalar type, it does not have an indeterminate or erroneous value ([basic.indet]),
- if the value is of pointer type, it contains the address of an object with static storage duration, the address past the end of such an object ([expr.add]), the address of a non-immediate function, or a null pointer value,
- if the value is of pointer-to-member-function type, it does not designate an immediate function, and
- if the value is an object of class or array type, each subobject satisfies these constraints for the value.
- its innermost enclosing non-block scope is a function parameter scope of an immediate function,
- it is a subexpression of a manifestly constant-evaluated expression or conversion, or
- its enclosing statement is enclosed ([stmt.pre]) by the compound-statement of a consteval if statement ([stmt.if]).
- a potentially-evaluated id-expression that denotes an immediate function that is not a subexpression of an immediate invocation, or
- an immediate invocation that is not a constant expression and is not a subexpression of an immediate invocation.
- the call operator of a lambda that is not declared with the consteval specifier,
- a defaulted special member function that is not declared with the consteval specifier, or
- a function that results from the instantiation of a templated entity defined with the constexpr specifier.
- declared with the consteval specifier, or
- an immediate-escalating function F
whose function body contains an immediate-escalating expression E
such that E's innermost enclosing non-block scope
is F's function parameter scope. [Note 11:Default member initializers used to initialize a base or member subobject ([class.base.init]) are considered to be part of the function body ([dcl.fct.def.general]).— end note]
- a constant-expression, or
- the condition of a constexpr if statement ([stmt.if]), or
- an immediate invocation, or
- the result of substitution into an atomic constraint expression to determine whether it is satisfied ([temp.constr.atomic]), or
- the initializer of a variable
that is usable in constant expressions or
has constant initialization ([basic.start.static]).70 [Example 10: template<bool> struct X {}; X<std::is_constant_evaluated()> x; // type X<true> int y; const int a = std::is_constant_evaluated() ? y : 1; // dynamic initialization to 1 double z[a]; // error: a is not usable // in constant expressions const int b = std::is_constant_evaluated() ? 2 : y; // static initialization to 2 int c = y + (std::is_constant_evaluated() ? 2 : y); // dynamic initialization to y+y constexpr int f() { const int n = std::is_constant_evaluated() ? 13 : 17; // n is 13 int m = std::is_constant_evaluated() ? 13 : 17; // m can be 13 or 17 (see below) char arr[n] = {}; // char[13] return m + sizeof(arr); } int p = f(); // m is 13; initialized to 26 int q = p + f(); // m is 17 for this call; initialized to 56 — end example]
- a manifestly constant-evaluated expression,
- a potentially-evaluated expression,
- an immediate subexpression of a braced-init-list,71
- an expression of the form & cast-expression that occurs within a templated entity,72 or
- a potentially-evaluated subexpression ([intro.execution]) of one of the above.
- a constexpr function that is named by an expression that is potentially constant evaluated, or
- a potentially-constant variable named by a potentially constant evaluated expression.
9.2.6 The constexpr and consteval specifiers [dcl.constexpr]
- it is not a coroutine ([dcl.fct.def.coroutine]), and
- if the function is a constructor or destructor, and its class does not have has any virtual base classes.
- an invocation of a constexpr function can appear in a constant expression ([expr.const]) and
- copy elision is not performed in a constant expression ([class.copy.elision]).
17.12 Coroutines [support.coroutine]
17.12.1 General [support.coroutine.general]
17.12.2 Header <coroutine> synopsis [coroutine.syn]
17.12.3 Coroutine traits [coroutine.traits]
17.12.3.1 General [coroutine.traits.general]
17.12.3.2 Class template coroutine_traits [coroutine.traits.primary]
17.12.4 Class template coroutine_handle [coroutine.handle]
17.12.4.1 General [coroutine.handle.general]
17.12.4.2 Construct/reset [coroutine.handle.con]
constexpr coroutine_handle() noexcept;
constexpr coroutine_handle(nullptr_t) noexcept;
static constexpr coroutine_handle from_promise(Promise& p);
constexpr coroutine_handle& operator=(nullptr_t) noexcept;
17.12.4.3 Conversion [coroutine.handle.conv]
constexpr operator coroutine_handle<>() const noexcept;
17.12.4.4 Export/import [coroutine.handle.export.import]
constexpr void* address() const noexcept;
static constexpr coroutine_handle<> coroutine_handle<>::from_address(void* addr);
static constexpr coroutine_handle<Promise> coroutine_handle<Promise>::from_address(void* addr);
17.12.4.5 Observers [coroutine.handle.observers]
constexpr explicit operator bool() const noexcept;
constexpr bool done() const;
17.12.4.6 Resumption [coroutine.handle.resumption]
constexpr void operator()() const;
constexpr void resume() const;
constexpr void destroy() const;
17.12.4.7 Promise access [coroutine.handle.promise]
constexpr Promise& promise() const;
17.12.4.8 Comparison operators [coroutine.handle.compare]
constexpr bool operator==(coroutine_handle<> x, coroutine_handle<> y) noexcept;
constexpr strong_ordering operator<=>(coroutine_handle<> x, coroutine_handle<> y) noexcept;
17.12.4.9 Hash support [coroutine.handle.hash]
template<class P> struct hash<coroutine_handle<P>>;
17.12.5 No-op coroutines [coroutine.noop]
17.12.5.1 Class noop_coroutine_promise [coroutine.promise.noop]
struct noop_coroutine_promise {};
17.12.5.2 Class coroutine_handle<noop_coroutine_promise> [coroutine.handle.noop]
17.12.5.2.1 General [coroutine.handle.noop.general]
17.12.5.2.2 Conversion [coroutine.handle.noop.conv]
constexpr operator coroutine_handle<>() const noexcept;
17.12.5.2.3 Observers [coroutine.handle.noop.observers]
constexpr explicit operator bool() const noexcept;
constexpr bool done() const noexcept;
17.12.5.2.4 Resumption [coroutine.handle.noop.resumption]
constexpr void operator()() const noexcept;
constexpr void resume() const noexcept;
constexpr void destroy() const noexcept;
17.12.5.2.5 Promise access [coroutine.handle.noop.promise]
constexpr noop_coroutine_promise& promise() const noexcept;
17.12.5.2.6 Address [coroutine.handle.noop.address]
constexpr void* address() const noexcept;
17.12.5.3 Function noop_coroutine [coroutine.noop.coroutine]
constexpr noop_coroutine_handle noop_coroutine() noexcept;
17.12.6 Trivial awaitables [coroutine.trivial.awaitables]
Feature test macros
15.11 Predefined macro names [cpp.predefined]
__cpp_constexpr_coroutines 2024??L
17.3.2 Header <version> synopsis [version.syn]
#define __cpp_lib_constexpr_coroutines 2024??L