constexpr
placement newDocument #: | P2747R2 |
Date: | 2024-03-19 |
Project: | Programming Language C++ |
Audience: |
EWG |
Reply-to: |
Barry Revzin <barry.revzin@gmail.com> |
R0 [P2747R0] of this paper proposed three related features:
cv void*
to cv T*
during constant evaluationSince then, [P2738R1] was adopted in Varna, which resolves problem #1. Separately, #3 is kind of a separate issue and there are ongoing conversations about how to handle this in order to make inplace_vector
[P0843R9] actually during during constant evaluation for all types. So this paper refocuses to just solve problem #2 and has been renamed accordingly.
Since [P2747R1], included support for arrays (yet another benefit over std::construct_at
).
Consider this implementation of std::uninitialized_copy
, partially adjusted from cppreference:
template <input_iterator I, sentinel_for<I> S, nothrow_forward_iterator I2> constexpr auto uninitialized_copy(I first, S last, I2 d_first) -> I2 { using T = iter_value_t<I2>; I2 current = d_first; try { for (; first != last; ++first, (void)++current) { ::new (std::addressof(*current)) T(*first); } } catch (...) { std::destroy(d_first, current); throw; } }
This fails during constant evaluation today because placement new takes a void*
. But it takes a void*
that points to a T
- we know that by construction. It’s just that we happen to lose that information along the way.
Moreover, that’s not actually how uninitialized_copy
is specified, we actually do this:
where:
Which exists to avoid users having written a global placement new that takes a T*
.
The workaround, introduced by [P0784R7], is a new library function:
This is a magic library function that is specified to do the same voidify
dance, but which the language simply recognizes as an allowed thing to do. std::construct_at
is explicitly allowed in [expr.const]/6:
6 […] Similarly, the evaluation of a call to
std::construct_at
orstd::ranges::construct_at
([specialized.construct]) does not disqualifyE
from being a core constant expression unless the first argument, of typeT*
, does not point to storage allocated withstd::allocator<T>
or to an object whose lifetime began within the evaluation ofE
, or the evaluation of the underlying constructor call disqualifiesE
from being a core constant expression.
It’s good that we actually have a solution - we can make uninitialized_copy
usable during constant evaluation simply by using std::construct_at
. There’s even a paper to do so ([P2283R2]). But that paper also had hinted at a larger problem: std::construct_at
is an extremely limited tool as compared to placement new.
Consider the different kinds of initialization we have in C++:
kind
|
placement new
|
std::construct_at
|
---|---|---|
value initialization | new (p) T(args...) |
std::construct_at(p, args...) |
default initialization | new (p) T |
Not currently possible. [P2283R1] proposed std::default_construct_at |
list initialization | new (p) T{a, b} |
Not currently possible, could be a new function? |
designated initialization | new (p) T{.a=a, .b=b} |
Not possible to even write a function |
That’s already not a great outlook for std::construct_at
, but for use-cases like uninitialized_copy
, we have to also consider the case of guaranteed copy elision:
auto get_object() -> T; void construct_into(T* p) { // this definitely moves a T std::construct_at(p, get_object()); // this definitely does not move a T :::new (p) T(get_object()); // this now also definitely does not move a T, but it isn't practical // and you also have to deal with delightful edge cases - like what if // T is actually constructible from defer? struct defer { constexpr operator T() const { return get_object(); } }; std::construct_at(p, defer{}); }
Placement new is only unsafe because the language allows you to do practically anything - want to placement new a std::string
into a double*
? Sure, why not. But during constant evaluation we already have a way of limiting operations to those that make sense - we can require that the pointer we’re constructing into actually is a T*
. The fact that we have to go through a void*
to get there doesn’t make it unsafe.
Now that we have support for static_cast<T*>(static_cast<void*>(p))
, we can adopt the same rules to make placement new work.
Additionally, std::construct_at
does not support arrays (see also [LWG3436]), but we can make it work with placement without much issue - another reason the language is simply better:
Note that the smaller array case could probably be made to work, but I’m not aware of a strong reason to want it - so it seems like a good start to allow the correct-size array case and reject the wrong-size array case for both directions of wrong size.
Today, we have an exception for std::construct_at
and std::ranges::construct_at
to avoid evaluating the placement new that they do internally. But once we allow placement new, we no longer need an exception for those cases - we simply need to move the lifetime requirement from the exception into the general rule for placement new.
Clarify that implicit object creation does not happen during constant evaluation in 6.7.2 [intro.object]/14:
14
AnExcept during constant evaluation, an operation that begins the lifetime of an array ofunsigned char
orstd::byte
implicitly creates objects within the region of storage occupied by the array.[Note 5: The array object provides storage for these objects. — end note]
AnyExcept during constant evaluation, any implicit or explicit invocation of a function namedoperator new
oroperator new[]
implicitly creates objects in the returned region of storage and returns a pointer to a suitable created object.[Note 6: Some functions in the C++ standard library implicitly create objects ([obj.lifetime], [c.malloc], [mem.res.public], [bit.cast], [cstring.syn]). — end note]
Change 7.6.2.8 [expr.new]/15:
15 During an evaluation of a constant expression, a call to
ana replaceable allocation function is always omitted ([expr.const]).[ Note 1: Only new-expressions that would otherwise result in a call to a replaceable global allocation function can be evaluated in constant expressions ([expr.const]). — end note ]
Change 7.7 [expr.const]/5.18 (paragraph 14 here for context was the [P2738R1] fix to allow converting from void*
to T*
during constant evaluation, as adjusted by [CWG2755]):
- (5.14) a conversion from a prvalue
P
of type “pointer to cvvoid
” to a “cv1
pointer toT
”, whereT
is notcv2 void
, unlessP
points to an object whose type is similar toT
;- (5.15) …
- (5.16) …
- (5.17) …
- (5.18) a new-expression (7.6.2.8 [expr.new]), unless either
- (5.18.1) 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- (5.18.2) the selected allocation function is a non-allocating form ([new.delete.placement]) with an allocated type
T
, where
- (5.18.2.1) the placement argument to the new-expression points to an object that is pointer-interconvertible with an object of type
T
or, ifT
is an array type, with the first element of an object of typeT
, and- (5.18.2.2) the placement argument points to storage whose duration began within the evaluation of
E
;
Remove the special case for construct_at
in 7.7 [expr.const]/6:
- 6 For the purposes of determining whether an expression
E
is a core constant expression, the evaluation of the body of a member function ofstd::allocator<T>
as defined in [allocator.members], where T is a literal type, is ignored.Similarly, the evaluation of the body ofstd::construct_at
orstd::ranges::construct_at
is considered to include only the initialization of theT
object if the first argument (of typeT*
) points to storage allocated withstd::allocator<T>
or to an object whose lifetime began within the evaluation ofE
.
Change 17.6.2 [new.syn] to mark the placement new functions constexpr
:
And likewise in 17.6.3.4 [new.delete.placement]:
2 Returns:
ptr
.…
5 Returns:
ptr
.
Bump the value of __cpp_constexpr
in 15.11 [cpp.predefined]:
And add a new __cpp_lib_constexpr_new
in 17.3.2 [version.syn]:
[CWG2755] Jens Maurer. 2023-06-28. Incorrect wording applied by P2738R1.
https://wg21.link/cwg2755
[LWG3436] Jonathan Wakely. std::construct_at should support arrays.
https://wg21.link/lwg3436
[P0784R7] Daveed Vandevoorde, Peter Dimov,Louis Dionne, Nina Ranns, Richard Smith, Daveed Vandevoorde. 2019-07-22. More constexpr containers.
https://wg21.link/p0784r7
[P0843R9] Gonzalo Brito Gadeschi, Timur Doumler, Nevin Liber, David Sankel. 2023-09-14. inplace_vector.
https://wg21.link/p0843r9
[P2283R1] Michael Schellenberger Costa. 2021-04-19. constexpr for specialized memory algorithms.
https://wg21.link/p2283r1
[P2283R2] Michael Schellenberger Costa. 2021-11-26. constexpr for specialized memory algorithms.
https://wg21.link/p2283r2
[P2738R1] Corentin Jabot, David Ledger. 2023-02-13. constexpr cast from void*: towards constexpr type-erasure.
https://wg21.link/p2738r1
[P2747R0] Barry Revzin. 2022-12-17. Limited support for constexpr void*.
https://wg21.link/p2747r0
[P2747R1] Barry Revzin. 2023-12-10. constexpr placement new.
https://wg21.link/p2747r1