Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Arrays and implicit-lifetime object creation

Some types are defined by standard as implicit-lifetime types, and arrays are among them. Some functions implicitly create objects with implicit-lifetime (malloc etc are among them), with a list of operations that implicitly create objects with implicit lifetime, found here. https://en.cppreference.com/w/cpp/language/object (I hope it is correct, but for the rest of the question let's assume that new works as malloc in this case for implicit object creation purposes).

What does it mean to implicitly create an array if it doesn't create its elements? Does it mean that

T* implicit_array = reinterpret_cast<T*>(::operator new(sizeof(T) * count, std::align_val_t{alignof(T)}) );

produces implicit_array object that is suitable for pointer arithmetic in, namely provides valid storage for elements of type T to be constructed using placement new later?
Does it mean that new (implicit_array + i) T{...} is a well defined operation, even though, by the standard, implicit_array + i is not necessarily defined? https://eel.is/c++draft/expr.unary#op-3.2.

Or it means that

std::byte* memory = 
    reinterpret_cast<std::byte*>(::operator new(sizeof(T) * capacity, std::align_val_t{alignof(T)}));
new (memory) T{args1 ...}
// build more objects
new  (memory + (k-1)*sizeof(T) ) T{args_k ...} 
T* implicit_array = std::launder(reinterpret_cast<T*>(memory) ); // does it produce array of k elements?

treats implicit_array as an array with k elements?

Thanks.

like image 207
Raziel Magius Avatar asked Mar 16 '21 23:03

Raziel Magius


1 Answers

It is possible to allocate the storage for the array, first, then construct the elements later. A closely related note in expr.new/15 specifically mentions "the common idiom of allocating character arrays into which objects of other types will later be placed".

The posted code mostly follows the proper allocate-then-construct sequence, except the reinterpret_cast is not safe and is, in fact, not necessary. The conversion from the void * returned in the allocation step to the typed T * is done by the placement new operator at construction time, instead.

void *alloc_array(size_t cnt)
{
    return ::operator new(sizeof(T) * cnt, std::align_val_t{alignof(T)});
}

T *construct_array(void *buf, size_t cnt)
{
    T *arr = new(buf) T {...}, *p = arr;
    for(int i = 1; i < cnt; i++)
        p = new(p + 1) T {...};
    return arr;
}

void destruct_array(T *arr, size_t cnt)
{
    for(int i = cnt; i--; )
        arr[i].~T();
}

void free_array(void *buf, size_t cnt)
{
    ::operator delete(buf, sizeof(T) * cnt, std::align_val_t{alignof(T)});
}

Sample usage:

    void *buf = alloc_array(cnt);
    T *arr = construct_array(buf, cnt);

    for(int i = 0; i < cnt; i++)
        T &ob = arr[i]; /* ... */

    destruct_array(arr, cnt);
    free_array(buf, cnt);
like image 151
dxiv Avatar answered Nov 09 '22 15:11

dxiv