When implementing certain data structures in C++ one needs to be able to create an array that has uninitialized elements. Because of that, having
buffer = new T[capacity];
is not suitable, as new T[capacity]
initializes the array elements, which is not always possible (if T does not have a default constructor) or desired (as constructing objects might take time). The typical solution is to allocate memory and use placement new.
For that, if we know the number of elements is known (or at least we have an upper bound) and allocate on stack, then, as far as I am aware, one can use an aligned array of bytes or chars, and then use std::launder
to access the members.
alignas(T) std::byte buffer[capacity];
However, it solves the problem only for stack allocations, but it does not solve the problem for heap alloations. For that, I assume one needs to use aligned new, and write something like this:
auto memory = ::operator new(sizeof(T) * capacity, std::align_val_t{alignof(T)});
and then cast it either to std::byte*
or unsigned char*
or T*
.
// not sure what the right type for reinterpret cast should be
buffer = reinterpret_cast(memory);
However, there are several things that are not clear to me.
reinterpret_cast<T*>(ptr)
is defined if ptr points an object that is pointer-interconvertible with T. (See this answer or https://eel.is/c++draft/basic.types#basic.compound-3) for more detail. I assume, that converting it to T*
is not valid, as T is not necessarily pointer-interconvertible with result of new. However, is it well defined for char*
or std::byte
?new
to a valid pointer type (assuming it is not implementation defined), is it treated as a pointer to first element of array, or just a pointer to a single object? While, as far as I know, it rarely (if at all) matters in practice, there is a semantic difference, an expression of type pointer_type + integer
is well defined only if pointed element is an array member, and if the result of arithmetic points to another array element. (see https://eel.is/c++draft/expr.add#4).unsigned char
or std::byte
can provide storage for result of placement new (https://eel.is/c++draft/basic.memobj#intro.object-3), however is it defined for arrays of other types?T::operator new
and T::operator new[]
expressions call ::operator new
or ::operator new[]
behind the scenes. Since the result of builtin new
is void, how conversion to the right type is done? Are these implementation based or we have well defined rules to handle these?::operator delete(static_cast<void*>(buffer), sizeof(T) * capacity, std::align_val_t{alignof(T)});
or there is another way?
PS: I'd probably use the standard library for these purposes in real code, however I try to understand how things work behind the scenes.
Thanks.
pointer-interconvertibility
Regarding pointer-interconvertibility, it doesn't matter if you use T *
or {[unsigned] char|std::byte} *
. You will have to cast it to T *
to use it anyway.
Note that you must call std::launder
(on the result of the cast) to access the pointed T
objects. The only exception is the placement-new call that creates the objects, because they don't exist yet. The manual destructor call is not an exception.
The lack of pointer-interconvertibility would only be a problem if you didn't use std::launder
.
When converting the result of new to a valid pointer type (assuming it is not implementation defined), is it treated as a pointer to first element of array, or just a pointer to a single object?
If you want to be extra safe, store the pointer as {[unsigned] char|std::byte} *
and reinterpret_cast
it after peforming any pointer arithmetic.
an object of type array
unsigned char
orstd::byte
can provide storage for result of placement new
The standard doesn't say anywhere that "providing storage" is required for placement-new to work. I think this term is defined solely to be used in definitions of other terms in the standard.
Consider [basic.life]/example-2
where operator=
uses placement-new to reconstruct an object in place, even though type T
doesn't "provide storage" for the same type T
.
Since the result of builtin new is void, how conversion to the right type is done?
Not sure what the standard has to say about it, but what else can it be other than reinterpret_cast
?
freeing the memory
Your approach looks correct, but I think you don't have to pass the size.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With