1. Introduction
[P0593] introduced the ability for objects to be implicitly created under
certain circumstances. In doing so, it added the requirement to allocators
that, when
is called, they create
a
, without additionally creating any array
elements. [N4910], in [allocator.requirements.general], provides the
following as an example of a way to implement this:
.
This example is incorrect.
After the lifetime of the
is begun, objects can be
implicitly created ([intro.object]/13). There are two possible cases here,
both of which result in UB.
First, if
is not an implicit-lifetime type, then only the array
object can be created (since all array types are implicit-lifetime, even if the
element type is not), and it must be created in order to satisfy the allocator
requirements. This is the goal of the allocator, but in an attempt to obtain a
pointer to the first element, it passes the pointer from
to
.
has the following preconditions:
represents the address A of a byte in memory. An object X that is within its lifetime and whose type is similar to
p is located at the address A. All bytes of storage that would be reachable through the result are reachable through
T (see below).
p
The result of
does not point to a
, and it
cannot have created
, since it is not an implicit-lifetime type.
The example therefore violates the preconditions of
and has undefined behavior.
If
is an implicit-lifetime type, then implicit object creation
must still create the array object in order to satisfy the allocator
requirements, but it could also create a
object at the first
byte in order to satisfy the preconditions of
.
However, this would violate the allocator requirements (since it creates an
array element object, violating the requirements on
),
yielding UB when used as a standard allocator per [res.on.functions]/2.3. No
set of implicitly created objects would give the program defined behavior, so
the program has undefined behavior ([intro.object]/10).
2. Problem
The fact that an example is incorrect is not a problem, since it can be fixed.
The problem is that there is no proper way to do what the example hopes to
accomplish. To the author’s knowledge, there is no reasonable standard way to
allocate a
for a runtime value of
, then get a
pointer to storage for the first element, which is what is required to
implement
.
This problem applies to any allocator which returns memory from an allocated
,
, or
.
Allocators based on
,
,
,
, and
can be
implemented in such a way as to have defined behavior. All of those functions
return a pointer to a suitable created object ([intro.object]/13 and
[c.malloc]/4), so a conforming allocate can be implemented as
T * allocate ( size_type n ) { return * reinterpret_cast < T ( * )[] > ( :: operator new ( sizeof ( T ) * n )); }
In this implementation,
returns a pointer to a
suitable created
, then a pointer to storage for its first
element is achieved by dereferencing the
and relying on
array-to-pointer decay to produce the final pointer.
There is no equivalent for user-defined functions that create a
for allocated objects. The only operations that return
pointers to suitable created objects are invoking functions named
or
and invoking the
standard functions named above (and
, but that’s not
helpful here).
In addition to suitable created objects being insufficient, there is no other
(reasonable) way to obtain a pointer to a
implicitly created
in a
.
looks promising, but
it cannot be used to form a pointer to a
because of
[res.on.functions]/2.5:
In particular, the effects are undefined in the following cases:
- [...]
- If an incomplete type is used as a template argument when instantiating a template component or evaluating a concept, unless specifically allowed for that component.
No specific type
can work for
because
and
are not similar if
.
However, a non-constant template argument to
can work:
template < typename T , std :: size_t Min , std :: size_t Max > auto do_launder_array ( void * p , std :: size_t n ) -> T ( * )[] { if constexpr ( Min == Max ) { std :: abort (); } else { if ( n == Min ) { return std :: launder ( reinterpret_cast < T ( * )[ Min ] > ( p )); } else { return :: do_launder_array < T , Min + 1 , Max > ( p , n ); } } } inline constexpr std :: size_t max_launder_array_size = 899 ; template < typename T > auto launder_array ( void * p , std :: size_t n ) -> T ( * )[] { return :: do_launder_array < T , 1 , max_launder_array_size > ( p , n ); } // In some allocator: T * allocate ( size_type n ) { return *:: launder_array < T > ( somehow_allocate_byte_array ( n ), n ); }
This works by determining, at runtime, the type
that the
allocated pointer should be laundered to. There are many issues with this
solution, however. First, it can only operate for
that are
less than some compile-time constant. Second, this requires deeply-nested
recursive template instantiations. Finally, this code is unreasonably
complex for a simple operation: "get a pointer to a
that
I know exists", even if the fact that it is known to exist is due to
implicit object creation.
3. Proposed Solution
The solution proposed here is to allow laundering pointers to arrays of unknown bound. This allows acquiring a pointer to an implicitly created array, without requiring that any elements have been created.
Since
and
are similar, no further
changes are needed to be able to satisfy the preconditions of
.
With this change, the following would be a conforming implementation of
for an allocator that places objects into
s:
T * allocate ( size_type n ) { return * std :: launder ( reinterpret_cast < T ( * )[] > ( somehow_allocate_byte_array ( n ))); }
4. Proposed Wording
4.1. 17.6.5 Pointer optimization barrier [ptr.launder]
template < class T > [[ nodiscard ]] constexpr T * launder ( T * p ) noexcept ;
Mandates:is true.
! is_function_v < T > && ! is_void_v < T > Preconditions:
represents the address A of a byte in memory. An object X that is within its lifetime and whose type is similar to
p is located at the address A. All bytes of storage that would be reachable through the result are reachable through
T (see below).
p Returns: A value of type
that points to X.
T * Remarks:
may be a type that is an array of unknown bound. An invocation of this function may be used in a core constant expression if and only if the (converted) value of its argument may be used in place of the function invocation. A byte of storage b is reachable through a pointer value that points to an object Y if there is an object Z, pointer-interconvertible with Y, such that b is within the storage occupied by Z, or the immediately-enclosing array object if Z is an array element.
T