Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to allocatate uninialized array in a way that does not result in UB?

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.

  1. 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 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?
  2. 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 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).
  3. As for lifetimes are concerned, an object of type array 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?
  4. As far as I knowT::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?
  5. 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.

like image 633
Raziel Magius Avatar asked Nov 06 '22 02:11

Raziel Magius


1 Answers

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 or std::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.

like image 67
HolyBlackCat Avatar answered Nov 15 '22 00:11

HolyBlackCat