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.
CodePudding user response:
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.