Reputation: 2734
I proposed several techniques to answer to Idiom for initializing an std::array using a generator function taking the index?.
A classical way to answer would be to use std::index_sequence
-based solution but op had two constraints:
std::array
sizes, larger than what common std::index_sequence
implementations are supporting.Using std::array
for large array instead of a std::vector
is debatable but it's not the "assignment".
Some of my answers are relying on creating a temporary raw storage where I'm building objects in place and then I'm aliasing the memory to a pointer to std::array
that I'm using to move-initialized a new array.
I have concerns about the legality of the code, that can be pinned down to:
#include <array>
#include <cstddef>
// allocate storage for an std::array and construct its object inplace before
// moving it to destination
template<typename T, std::size_t N, typename Gen) std::array<T,N> make_array(gen)
{
// checking std::array layout
static_assert(sizeof(std::array<T, N>) == N * sizeof(T));
alignas(T) unsigned char storage[N * sizeof(T)];
for (std::size_t i = 0; i != N; ++i) {
new (storage + i * sizeof(T)) T(gen(i));
}
// aliasing from array of bytes
// is this legal?
return std::move(*reinterpret_cast<std::array<T, N>*>(storage));
}
// allocate storage for an std::array inside a vector and construct its object
// inplace before moving it to destination
template<typename T, std::size_t N, typename Gen) std::array<T,N> make_array(gen)
{
// checking std::array layout
static_assert(sizeof(std::array<T, N>) == N * sizeof(T));
auto v = std::vector<T>{};
v.reserve(N);
for (std::size_t i = 0; i != N; ++i) {
v.emplace_back(gen(i))
};
// aliasing from array of T
// is this legal?
return std::move(
*reinterpret_cast<std::array<T, N>*>(v.data());
}
Are the return statements above valid?
And, if no, is there a way to solve the issue?
Wording from the standard whould be appreciate to justify the answer(s).
Upvotes: 0
Views: 176
Reputation: 63142
I think this is legal post C++20, because an instance of std::array<T, N>
is implicitly created, however cppreference's example for std::start_lifetime_as
has comments claiming similar constructs are UB.
However, if you have C++20
then you can use std::bit_cast
, which is much less suspicious than the reinterpret_cast
:
// allocate storage for an std::array and construct its object inplace before
// moving it to destination
template<typename T, std::size_t N, typename Gen) std::array<T,N> make_array(Gen gen)
{
// checking std::array layout
static_assert(sizeof(std::array<T, N>) == N * sizeof(T));
alignas(T) unsigned char storage[N * sizeof(T)];
for (std::size_t i = 0; i != N; ++i) {
new (storage + i * sizeof(T)) T(gen(i));
}
// Explicit From to ensure no array to pointer decay
return std::bit_cast<std::array<T, N>, unsigned char[N * sizeof(T)]>(storage);
}
If you have C++23 you can potentially do better (unless the copy in bit_cast
is elided) with std::start_lifetime_as
if T
is an ImplicitLifetimeType:
// allocate storage for an std::array and construct its object inplace before
// moving it to destination
template<typename T, std::size_t N, typename Gen) std::array<T,N> make_array(Gen gen)
{
// checking std::array layout
static_assert(sizeof(std::array<T, N>) == N * sizeof(T));
alignas(T) unsigned char storage[N * sizeof(T)];
for (std::size_t i = 0; i != N; ++i) {
new (storage + i * sizeof(T)) T(gen(i));
}
return *std::start_lifetime_as<std::array<T, N>>(storage);
}
Upvotes: 2