Document #: | P3032R0 |
Date: | 2024-02-13 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Barry Revzin <barry.revzin@gmail.com> |
C++20 introduced constexpr allocation, but in a limited form: any allocation must be deallocated during that constant evaluation.
The intent of the rule is that no constexpr allocation persists to runtime. For more on why we currently need to avoid that, see Jeff Snyder’s [P1974R0] and also [P2670R1].
But the rule cited above does slightly more than prevent constexpr allocation to persist until runtime. The goal of this paper is to allow more examples of allocations that do not persist until runtime, that nevertheless are still rejected by the C++23 rules.
For the purposes of this paper, we’ll consider the example of wanting to get the number of enumerators of a given enumeration. While the specific example is using reflection ([P2996R1]), there isn’t anything particularly reflection-specific about the example - it just makes for a good example. All you need to know about reflection to understand the example is that ^E
gives you an object of type std::meta::info
and that this function exists:
With that, let’s go through several attempts at trying to get the number of enumerators of a given enumeration E
as a constant:
Attempt | Result | |
---|---|---|
1 |
✅. This one is valid - because r1 is a constexpr variable, it’s initializer starts a constant evaluation - which includes the entire expression. The temporary vector is destroyed at the end of that expression, so it doesn’t persist outside of any constant evaluation.
|
|
2 |
❌. This one is invalid. The same idea about initializing enumerators_of(^E) persists outside of its constant expression in order to invoke .size() on it, which is not allowed.
|
|
3 |
✅. Both this row and the next row are subtle refinements of the second row that make it valid. The only difference between this and the previous row is thatf2 was constexpr but f3 is consteval . This distinction matters, because now enumerators_of(^E) is no longer an immediate invocation - it is now in an immediate function context. As a result, the only thing that matters is that the entire expression enumerators_of(^E).size() is constant - and the temporary vector<info> does not persist past that.
|
|
4 |
✅. Here f4 is a constexpr function template, whereas f2 was a regular constexpr function. This matters because of [P2564R3] - the fact that enumerators_of(^E).size() isn’t a constant expression now causes f4 to become a consteval function template - and thus we’re in the same state that we were in f3 : it’s not enumerators_of(^E) that needs to be a core constant expression but rather all of enumerators_of(^E).size() .
|
|
5 |
❌. Even though f5 is consteval , we are still explicitly starting a new constant evaluation within f5 by declaring es to be constexpr . That allocation persists past that declaration - even though it does not persist past f5 , which by being consteval means that it does not persist until runtime.
|
Three of these rows are valid C++23 programs (modulo the fact that they’re using reflection), but 2
and 5
are invalid - albeit for different reasons:
enumerators_of(^E)
as a constant expression all by itself - even if enumerators_of(^E).size()
is definitely a constant expression.es
as a non-transient constexpr allocation - even though it definitely does not persist until runtime, and thus does not actually cause any of the problems that non-transient constexpr allocation has to address.The wording in [P2564R3] introduced the term immediate-escalating expression in 7.7 [expr.const]:
17 An expression or conversion is immediate-escalating if it is not initially in an immediate function context and it is either
In the second example:
The expression enumerators_of(^E)
is immediate-escalating - it is an immediate invocation (enumerators_of
is a consteval
function) that is not a constant expression (because the temporary vector persists outside of this expression). This is what causes f4
to become a consteval
function template.
But enumerators_of(^E).size()
is not an immediate invocation (it simply has a subexpression that is an immediate invocation). However, if we were to define it as an immediate invocation - then it would not be an immediate-escalating expression anymore because it is actually a constant expression. And that would be enough to fix this example (as well as f4
which would then itself not escalate to consteval
since it wouldn’t need to).
Put differently: instead of escalating enumerators_of(^E)
up to the nearest function, which we then try to make consteval
(and fail in the case of f2
because constexpr
functions are not immediate-escalating), we only need to escalate up to the nearest enclosing expression that could be a constant expression.
The wording in 7.7 [expr.const] for rejecting non-transient allocations rejects an expression E
as being a core constant expressions if E
evaluates:
- (5.18) a new-expression ([expr.new]), unless 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
;- (5.20) a call to an instance of
std::allocator<T>::allocate
([allocator.members]), unless the allocated storage is deallocated within the evaluation ofE
;
That is - an allocation within E
has to be transient to E
. However, the rule we really want is that a constant allocation is transient to constant evaluation. In the fifth example:
The allocation in enumerators_of(^E)
isn’t transient to that expression, but it is definitely destroyed within f5
, which is consteval
. That’s important: if f5
were constexpr
, we’d have access to that allocation at runtime.
We can loosen the restriction such that an allocation within E
must be deallocated within E
or, if E
is in an immediate function context, the end of that context. This would be the end of the if consteval { }
block or the end of the consteval
function. Such a loosening would allow f5
above, but not if it’s constexpr
, and not if es
were also declared static
.
There are two separate potential changes here, that would each make one of the attempts above well-formed:
enumerators_of(^E).size()
becomes a constant expression, orThe second of these is straightforward to word and provides a lot of value - since now particularly in the context of reflection you can declare a constexpr vector<info>
inside a consteval
function and use those contents as a constant expression. The first of these is complicated to word and does not provide as much value, as it is a limitation that is fairly easy to work around: either declare a local constexpr
variable, or change the function to be consteval
or a template.
As such, this paper only proposes extending the notion of transience.
Change 7.7 [expr.const]/5:
5 An expression E is a core constant expression unless the evaluation of E, following the rules of the abstract machine ([intro.execution]), would evaluate one of the following:
- (5.1) […]
- (5.18) a new-expression ([expr.new]), unless the selected allocation function is a replaceable global allocation function ([new.delete.single], [new.delete.array]) and the allocated storage is deallocated either within the evaluation of
E
or, ifE
is in an immediate function content, within that context;- (5.19) a delete-expression ([expr.delete]), unless it deallocates a region of storage allocated either within the evaluation of
E
or, ifE
is in an immediate function content, within that context;- (5.20) a call to an instance of
std::allocator<T>::allocate
([allocator.members]), unless the allocated storage is deallocated either within the evaluation ofE
or, ifE
is in an immediate function content, within that context;- (5.21) a call to an instance of
std::allocator<T>::deallocate
([allocator.members]), unless it deallocates a region of storage allocated either within the evaluation ofE
or, ifE
is in an immediate function content, within that context;- (5.22) […]
Thank you to Peter Dimov for being Peter Dimov and coming up with all of these examples.
[P1974R0] Jeff Snyder, Louis Dionne, Daveed Vandevoorde. 2020-05-15. Non-transient constexpr allocation using propconst.
https://wg21.link/p1974r0
[P2564R3] Barry Revzin. 2022-11-11. consteval needs to propagate up.
https://wg21.link/p2564r3
[P2670R1] Barry Revzin. 2023-02-03. Non-transient constexpr allocation.
https://wg21.link/p2670r1
[P2996R1] Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde. 2023-12-18. Reflection for C++26.
https://wg21.link/p2996r1