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.
Suggestion:
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.
Suggestion:
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.
Suggestion:
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.
Suggestion:
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
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.
Suggestion:
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.
Suggestion:
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.
Suggestion:
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.
Suggestion:
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