1

Suppose I am writing a fixed-size array class of runtime size, somewhat equivalent to Rust's Box<[T]> in order to save the space of tracking capacity when I know the array isn't going to change size after initialization.

In order to support types which do not have a default constructor, I want to be able to allow the user to supply a generator function that takes the index of the element and produces a T. In order to do this and decouple allocation and initialization, I follow the advice in CJ Johnson's CppCon 2019 talk "How to Hold a T" and initially create the array in terms of a single-member union:

template<typename T>
union MaybeUninit
{
    MaybeUninit() {}
    ~MaybeUninit() {}
    T val;
};

// ...

m_array = new MaybeUninit<T>[size];

// initialize all elements by setting val for each item

T* items = reinterpret_cast<T*>(m_array); // is this OK and dereferenceable?

My question is, once the generator is done and all the elements of m_array are initialized, am I allowed (according to the standard, regardless of whether a given compiler implementation permits it) to use reinterpret_cast<T*>(m_array) to treat the result as an array of the actual objects (the line marked "is this OK")? If not, is there any way to get from MaybeUninit<T>* to T* without copying?

In terms of which standard, I'm mainly interested in C++17 or later.

Ranoiaetep
  • 5,872
  • 1
  • 14
  • 39
  • but https://stackoverflow.com/questions/55376433/is-there-a-standard-c-class-for-arrays-with-fixed-run-time-determined-size ? – KamilCuk Jan 21 '23 at 11:15
  • @KamilCuk - Neither `std::vector` or `std::unique_ptr` meet the use case here, as `vector` contains an additional member variable to store the capacity, and `std::unique_ptr` requires compile-time initialization if the type doesn't have a default constructor (FWIW, so do the custom types in https://stackoverflow.com/a/55379668/17789309). –  Jan 21 '23 at 11:36
  • `reinterpret_cast` is guaranteed to work in a very limited set of circumstances. This is not one of those. – n. m. could be an AI Jan 21 '23 at 11:37
  • This doesn't sound right. What's wrong with allocating a buffer with malloc in this case? After all, the default operator new() does call malloc, and using malloc() would not involve anything close to UB, not in any c++ standard yet published, nor hopefully in future c++ standard. I also fail to see what the effect of optimizing out array sizes (4 octect) per array will acrtually bring. – Michaël Roy Jan 21 '23 at 15:09
  • I regret asking this question on this website. I even put the language lawyer tag to make it clear I am asking about one specific feature of the language, and I still get people trying to make this into an XY problem. It's so tiring. –  Jan 21 '23 at 17:38

1 Answers1

1

am I allowed (according to the standard, regardless of whether a given compiler implementation permits it) to use reinterpret_cast<T*>(m_array) to treat the result as an array of the actual objects (the line marked "is this OK")?

No, any pointer arithmetic on the resulting pointer will result in UB (for indices >1) or result in a one-past the end pointer that can't be dereferenced (for index 1). Only accessing the element at index 0 this way is allowed (but needs to still be constructed).

The only way you are allowed to perform pointer arithmetic is on pointers to the elements of an array. Your pointer is not pointing to an object that is element of an array (which then for the purpose of pointer arithmetic is considered to be belong to an array of length 1).

If not, is there any way to get from MaybeUninit* to T* without copying?

The pointer conversion is not an issue, but you can't index into the resulting pointer. The only way to avoid this is to have an actual array of T objects.

Note however that you don't need to construct every element in an array of T objects. For example:

std::allocator<T> alloc;

T* ptr = std::allocator_traits<decltype(alloc)>::allocate(size);

Now ptr is a pointer to an array of size objects of type T, but no actual objects of type T in it have their lifetime started. You can construct individual elements into it with placement-new (or std::construct_at or std::allocator_traits<decltype(alloc)>::construct) and destruct them with a destructor call (or std::destroy_at or std::allocator_traits<decltype(alloc)>::destruct). You need to do this with your union approach as well anyhow. This approach also allows you to easily exchange the allocator with a different one.

There will be no overhead for size or capacity management. All of that is now responsibility of the user. Whether this is a good idea is a different question.

Instead of std::allocator or an alternative Allocator implementation you could also use other functions that allocate memory and are specified to implicitly create objects, e.g. operator new or std::malloc, etc.

user17732522
  • 53,019
  • 2
  • 56
  • 105
  • @sklott The array contains elements of type `MaybeUninit` and OP is trying to do pointer arithmetic on it as a `T` instead. `T` and `MaybeUninit` are not similar. Also the result of `reinterpret_cast` is a pointer to one of the `T` members of a `MaybeUninit`. And this is _not_ an element of the array at all. If the result of the cast would be a pointer to the `MaybeUninit` object, just with the wrong type _and_ the types were similar, then it would be ok. – user17732522 Jan 21 '23 at 16:47
  • question: If you would define the union to be a union of an array, so `template union U{ U(){} ~U(){} T mem[cap];}` then the cast to the array would be okay, *but* would it be okay to call `new (&u[i]) T{...}` for `U u`? The lifetime of the array itself did not start, but can I then start the lifetime of a specific element in the array? So do the independent elements of the array have independent lifetimes and access via array (lifetime?) is possible before initializing the overall array (and thereby all elements). And is cast to array(T) afterwards still ok? – Chris N Mar 23 '23 at 11:44
  • @ChrisN `u[i]` is a syntax error. Did you mean `u.mem[i]`? In that case see https://stackoverflow.com/questions/70549096/does-stdconstruct-at-make-an-array-member-of-a-union-active. – user17732522 Mar 23 '23 at 12:17