7

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.

1 Answers1

4

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);
dxiv
  • 16,984
  • 2
  • 27
  • 49
  • `p = new(p + 1) T {...}` Is this line valid (by the standard or is it UB)? Isn't p a pointer to single element of type T and not array element? `T *arr = new(buf) T {...}` does not create an array, as far as I am aware. – Myrddin Krustowski Mar 18 '21 at 11:23
  • Also, what makes `reinterpret_cast` not safe? As far as I am aware, since arrays are defined to be implicit lifetime objects by the last standard, `(T*)(::operator new (...) )` just like (T*) malloc (... )` should implicitly construct an array, provided that subsequent actions give it defined behavior. Are not (T*) and reinterpret_cast equivalent? – Myrddin Krustowski Mar 18 '21 at 11:47
  • @RazielMagius That `arr` is a pointer to the first element of the array, and `arr == &arr[0]` is guaranteed by the standard. – dxiv Mar 18 '21 at 17:40
  • @RazielMagius The cast is not safe because it produces a `T *` to something that is not a `T` object (yet) at that point, and attempts to use that pointer as a `T *` may result in UB. It is, indeeed, a legal cast by the language rules, and it will result in the same value of the pointer in the end, though the formal argument is more convoluted. Basically, `reinterpret_cast` translates to `static_cast` ([expr.reinterpret_cast/7](https://eel.is/c++draft/expr.reinterpret.cast#7)) which ends up copying the pointer value ([expr.static.cast/13](https://eel.is/c++draft/expr.static.cast#13))... – dxiv Mar 18 '21 at 17:41
  • ...which matches the unchanged pointer that placement new returns ([new.delete.placement/5](https://eel.is/c++draft/new.delete.placement#5)). But casting it upfront is too early, and is not necessary. Both the standard example ([new.delete.placement/4](https://eel.is/c++draft/new.delete.placement#4)) and the c++faq ones ([1](https://isocpp.org/wiki/faq/dtors#placement-new), [2](https://isocpp.org/wiki/faq/dtors#memory-pools)) use the two step allocation+construction, without any explicit casts. – dxiv Mar 18 '21 at 17:41
  • I am not sure, however according to this , I think creating an array using `T* implicit_array = reinterpret_cast(::operator new(sizeof(T) * count, std::align_val_t{alignof(T)}) )` should be fine. As far as I am aware, `malloc` and `new` both construct objects with implicit lifetime. – Myrddin Krustowski Mar 20 '21 at 17:52
  • Both examples do no create arrays, but single objects. And while `&p[0]` and `p` are equal, if we use `p[1]` and `q = new (p+1) T{...}` do not need to be equal in general, even if `p` is an array, which I believe is not the case as `new (p) T{...}` does not create an array, but single object. And even if `p` was an array, in general, they do not have to be equal - otherwise `std::launder` would not be required. – Myrddin Krustowski Mar 20 '21 at 18:00
  • @RazielMagius 1) Neither `malloc` nor this `new` overload create "*objects*". They only allocate *storage* where objects will be created later. This distinction is specifically made in [basic.life/6](https://eel.is/c++draft/basic.life#6): "*before the lifetime of an object has started but after the storage which the object will occupy has been allocated ...*". 2) `p == &p[0]` and `p + 1 == &p[1]` by definition. 3) `std::launder` is not needed here, and it would apply to the raw pointer anyway, see e.g. "*case 3*" in the bottom example [here](https://en.cppreference.com/w/cpp/utility/launder). – dxiv Mar 20 '21 at 18:36
  • Please see the example in https://eel.is/c++draft/intro.object#12 and the comments to it. _"The call to std​::​malloc implicitly creates an object of type X and its subobjects a and b, and returns a pointer to that X object"_. Also, please see http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html which I believe is a part of standard now (at least parts of it). – Myrddin Krustowski Mar 20 '21 at 18:50
  • @RazielMagius One paragraph up has this important caveat: "***if*** *that value would result in the program having defined behavior. If no such pointer value would give the program defined behavior, the behavior of the program is undefined*". Add a constructor to that `struct`, or a virtual destructor, and the behavior is no longer defined, so the example is about a very special case. Anyway, this is drifting into questions about allocation, lifetime, launder etc, not really related to my answer or the original question as posed. – dxiv Mar 20 '21 at 19:00
  • My question was what is the meaning of "implicit-lifetime" for arrays. Please see https://en.cppreference.com/w/cpp/language/lifetime and implicit-lifetime. Arrays are clearly one of those. Also "_Note that if a subobject of an implicitly created object is not of an implicit-lifetime type, its lifetime does not begin implicitly._" – Myrddin Krustowski Mar 20 '21 at 19:04
  • PS: Any question about implicit-lifetime is implicitly a question about allocation, lifetime, (sometimes) launder, and (sometimes) memmove (or memcpy) as it is not relevant in any other contexts. – Myrddin Krustowski Mar 20 '21 at 20:31
  • As for launder part - I did not say that you need to launder in your example. What I said, that in general, the results of the expressions `q = new (p+1) T{...}` and `p+1` do not need to be equal in general. – Myrddin Krustowski Mar 20 '21 at 21:18
  • @RazielMagius 1) As stated in the linked page (and [basic.life](https://eel.is/c++draft/basic.life)), an array is implicitly created iff its element type is i.e. it has *vacuous initialization*. But again, my answer (and your original question) is about the general case of an arbitrary type `T` without additional assumptions. 2) The return value of `new (p) T{...}` *is* equal to `p` when using the global or default operator ([new.delete.placement](https://eel.is/c++draft/new.delete.placement) "*Returns: ptr*"), otherwise if you provide a class override then it's your choice what it returns. – dxiv Mar 20 '21 at 23:07
  • Arrays are implicit-lifetime types, by the standard, without reference to their type. Please see https://eel.is/c++draft/basic.types#general-9. _Scalar types, implicit-lifetime class types ([class.prop]), array types, and cv-qualified versions of these types are collectively called implicit-lifetime types._ – Myrddin Krustowski Mar 20 '21 at 23:45
  • I am not sure, but I think that this one should be valid code: `(T(*)[])::operator new(sizeof(T) * count, std::align_val_t{ alignof(T) })` as it returns a pointer to an array (which is implicitly constructed type), while `(T*)::operator new(... )` returns a pointer to first element of the array, which is not implicitly constructed itself. Does it make more sense now? – Myrddin Krustowski Mar 20 '21 at 23:56
  • @RazielMagius From the previously linked [basic.life/1](https://eel.is/c++draft/basic.life#1): "*A variable is said to have vacuous initialization if it is default-initialized and*, ***if it is*** *of class type or a (possibly multi-dimensional)* ***array thereof***, *that class type has a trivial default constructor. The lifetime of an object of type T begins when: (1.1) ... and (1.2) its initialization (if any) is complete (* ***including vacuous*** *initialization)*". That said, all this may be better suited for another question than random comments so I think I'll drop out now. – dxiv Mar 20 '21 at 23:59