Storing end pointer to uninitialized array in constexpr context

Viewed 145

I'm trying to create a constexpr friendly small buffer optimized vector type that stores a begin, end and capacity pointer as usual, but when it is default constructed the begin and capacity pointer point towards local memory before reallocating to heap when required. However I cannot figure out how to store the capacity pointer in the constexpr context, because I want it to be able to support non-trivially constructed types. In order to store non-trivially constructed types I cannot use std::aligned_storage because of the full type erasure which will not allow me to get the elements later (because reinterpret_cast is not allowed), so I decided to store the elements in an array of unions (kind of like optional does it). Storing it like this allows me to get the elements later through union access and allows me to leave the array uninitialized, but i cannot figure out how to store a value pointer to capacity end when it is stored inside of unions because it does not detect the whole one beyond last pointer stuff when pointing to a union value. Ofcourse all of this requires c++20.

#include <algorithm>
#include <memory>
#include <utility>
#include <array>

struct Null{};

template<typename T>
union Storage
{
    Null uninitialized;
    T value;

    constexpr Storage()
    : uninitialized{}
    {}

    template<typename... Args>
    constexpr Storage(Args&&... args)
    : value(std::forward<Args>(args)...)
    {}

    constexpr ~Storage(){}
};

template<typename T, size_t N>
struct Vec
{
    std::array<Storage<T>, N> storage;

    T* begin; 
    T* end;   

    constexpr Vec()
    : begin(makeBegin())
    , end(makeEnd())
    {}

    constexpr T* makeBegin()
    {
        return &storage[0].value;
    }
    constexpr T* makeEnd()
    {
        return (&storage[N].value);
    }
};


constexpr bool test()
{
    Vec<std::pair<float, float>, 10> vec{};

    for(auto it = vec.begin; it < vec.end; ++it)
    {
        std::construct_at(it, 10.0f, 10.0f);
    }

    return vec.begin[5] == std::pair{ 10.0f, 10.0f };
}


int main()
{
    static_assert(test());
}

https://godbolt.org/z/46o19qcvP

Is there another way to get a pointer to stored non-trivially constructible types like pairs in an array without initializing them?

1 Answers

There are multiple problems with your code:

  • T* is not a valid iterator for your representation, as the Ts are actually a member of a structure. The iterator needs to operate on the array's value type.
  • Using storage[N] is an out of bounds access, even if you just try to use the address of the member within.

One way fix both problems is to use a custom iterator type. Here is an example based on your original code (the iterator implementation is a bit incompete - I just implemented what is needed to get the code to compile):

#include <algorithm>
#include <memory>
#include <utility>
#include <array>


template<typename T>
union Storage
{
    T value;

    constexpr Storage() {}

    template<typename... Args>
    constexpr Storage(Args&&... args)
    : value(std::forward<Args>(args)...)
    {}

    constexpr ~Storage(){}
};

template<typename T, size_t N>
struct Vec
{
    std::array<Storage<T>, N> storage;

    struct iterator {
        Storage<T>* p;
        constexpr T& operator*() { return this->p->value; }
        constexpr T* operator->() { return &this->p->value; }
        constexpr T& operator[](std::size_t n) { return (this->p)[n].value; }
        constexpr iterator& operator++() { ++this->p; return *this; }
        constexpr iterator  operator++(int) { auto rc(*this); ++*this; return rc; }
        constexpr iterator& operator+= (std::size_t n){ this->p += n; return *this; }
        friend constexpr iterator operator+ (iterator it, std::size_t n) { return it += n; }
        constexpr bool      operator== (iterator const&) const = default;
    constexpr bool      operator< (iterator const& other) const { return this->p < other.p; }
    };
    
    
    iterator begin; 
    iterator end;   
    
    constexpr Vec()
    : begin(makeBegin())
    , end(makeEnd())
    {}

    constexpr iterator makeBegin()
    {
        return {&this->storage[0]};
    }
    constexpr iterator makeEnd()
    {
        return this->makeBegin() + N;
    }
};


constexpr bool test()
{
    Vec<std::pair<float, float>, 10> vec{};

    for(auto it = vec.begin; it < vec.end; ++it)
    {
        std::construct_at(&*it, 10.0f, 10.0f);
    }

    return vec.begin[5] == std::pair{ 10.0f, 10.0f };
}


int main()
{   
    static_assert(test());
}

As the T elements need to be individually created to qualify as the active union member, here isn't really a way to get to store T* objects. The implication is that a potentially variable sized portion of a vector also stores Storage<T> elements to avoid messing with two different iterator types.

Related