C++ Logo


Advanced search

Make constexpr dynamic allocation less magic

From: v.S. F. <de34_at_[hidden]>
Date: Tue, 23 Mar 2021 12:51:54 +0000
There are some issues in constexpr dynamic allocation in C++20 IMO.

Part 1. Destructor calls in constant evaluation

A plain (pseudo-)destructor call (note that a pseudo-destructor is made destroying the object via P0593R6) is not restricted in constant evaluation, while a std::(ranges::)destroy_at is restricted. Because destroying an object is not needed to modify any scalar (sub)object, so the current specification in [expr.const] apparently allow evaluation of a constant expression to destroy an object that whose lifetime didnít begin within it, e.g. a global constexpr object.

Such restriction should exist for plain (pseudo-)destructor calls, not for std::(ranges::)destroy_at only. This part should be addressed as a CWG issue.

Part 2. Program-defined specializations for std::allocator

Since C++20, std::allocator<T>::allocate/deallocate can perform constexpr dynamic allocation/deallocation, but it is unspecified how are such operations done. On the other hand, users are still allowed to provide program-defined specializations for std::allocator, while the allocate/deallocator member functions of these specializations must be manually implemented. As a result, thereís no portable way these specializations constexpr dynamic allocation/deallocation, or even no way if the compiler uses some non-extensible magic, so itís doubtful that whether these specializations can achieve the requirement in [namespace.std]/2.

Move the magic into free function templates (such as std::allocate_n/deallocate_n), and make std::allocator<T>::allocate/deallocate call them.

Part 3. Direct list-initialization in constant evaluation

std::(ranges::)construct_at effectively allows us to perform direct (non-list) initialization on a given location in constant evaluation, but default initialization and direct list-initialization are not allowed yet. Although the utility performing default initialization, default_construct_at, is proposed in P2283<https://wg21.link/p2283>, there is no general way simulate a placement-new that performs direct list-initialization by function templates.

[expr.const] currently doesnít directly allow a plain non-allocating placement-new expression, which looks an oversight.

Introduce the item "type-corresponding placement new-expression". A placement new-expression is type-corresponding if

- it would select a non-allocating allocation function, and

- its type-id or new-type-id doesnít denotes an array of non-constant length, and

- the expression in its new-placement, after removing any explicit cast operator to cv void*, is a such expression that becomes a T* prvalue before converted to cv void*, where T is same as the type denoted by type-id/new-type-id ignoring top-level cv-qualification.
The "corresponding pointer value" of a type-corresponding placement-new expression is the aforementioned T* prvalue.
Allow a type-corresponding placement new-expression whose corresponding pointer value satisfies the current requirements for std::(ranges::)construct_at in constant evaluation.

As a result, we can make std::(ranges::)construct_at non-magic, and implement default_construct_at directly.

Part 4. Storage reuse in constant evaluation

Currently storage reuse is permitted in constant evaluation, however, it may be not clear whether skipping a non-trivial destructor call result in UB. [basic.life] says ďand any program that depends on the side effects produced by the destructor has undefined behaviorĒ, and I donít know whether the requirement is diagnosable in constant evaluation.

Unclear. Maybe we can require that in constant evaluation, a skipped destructor call shall must be no-op, like constant destruction? This part should be addressed as a CWG issue. Related to EDIT 2342<https://github.com/cplusplus/draft/pull/2342>.

Other questions and concerns

Should we clarify that no allocation/deallocation function is called by new-/delete-expressions in constant evaluation? Note that these functions are not constexpr as for now.

Can we remove non-allocating allocation functions (along with their corresponding deallocation functions) and make their signatures built-in candidates? IMO it is cleaner that ::new ((void*)buf) int{} no longer requires <new> and no longer call any function semantically.

Jiang An

Received on 2021-03-23 07:52:07