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.
- The result
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 toT*
is not valid, as T is not necessarily pointer-interconvertible with result of new. However, is it well defined forchar*
orstd::byte
? - 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? While, as far as I know, it rarely (if at all) matters in practice, there is a semantic difference, an expression of typepointer_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). - As for lifetimes are concerned, an object of type array
unsigned char
orstd::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? - As far as I know
T::operator new
andT::operator new[]
expressions call::operator new
or::operator new[]
behind the scenes. Since the result of builtinnew
is void, how conversion to the right type is done? Are these implementation based or we have well defined rules to handle these? - When freeing the memory, should one use
::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.
new T[]
initializes the array elements" No, it doesn't.new T[]()
would, but notnew T[]
. I mean, it will default initialize them, so if a default constructor exists, it will be called. But ifT
is a trivial type, it will be left uninitialized. So what exactly do you mean by "uninitialized" here? Do you mean that there are no actualT
s, or do you wantT
s to exist but have uninitialized values? – Exospherenew T
statement. – Kakaaba