Oersted
Oersted

Reputation: 2734

is aliasing a plain array to an `std::array` not UB

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:

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());
}

playground

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

Answers (1)

Caleth
Caleth

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

Related Questions