using std::launder to implement memory buffer

176 Views Asked by At

I apologize if this has been answered before, I just fail to find the precise answer to the questions at the end.

Here is a simplified version of collection.

template <typename T>
struct my_collection
{
    T* buffer_start = nullptr;
    std::size_t capacity;
    std::size_t size;
    T* buffer_end = nullptr;

    // definition of reference, iterators, value_type etc are ommited.

    my_collection(std::size_t capacity) : capacity{ capacity }, buffer_end{ 0 }
    {
        auto memory = ::operator new(capacity * sizeof(T), std::align_val_t{ alignof(T) });
        // I am not sure I am using std::align_val_t correctly here and feel free to correct me, 
        // but I assume that idea is clear
        buffer_start = static_cast<T*>(memory);
        // I think that memory variable can be eliminated, and everything can be in one line
        buffer_end = buffer_start;
    }

    // assume that insertions and removals occur at the end for simplicity and omit move semantics
    // for simplicity and assume copy construction is legal
    void insert(const T& x)
    {
        // overflow check is omitted - assume that size < capacity
        new (buffer_end) T{ x };
        ++size;
        ++buffer_end;
        // buffer_start = std::launder(buffer_start)
    }

    void remove_last()
    {
        // omit checking for empty etc
        --buffer_end;
        --size;
        // Assumption that destructor exists for simplicity - otherwise use destroy_at or implement
        // custom destruction
        std::launder(buffer_start + size)->~T(); 
        // is this even legal or also leads to UB?
        // will these buffer_start = std::launder(buffer_start);
    }

    // const version is omitted
    T& operator[](std::size_t index)
    {
        return *(std::launder(T + index));
        // if I use buffer_start = std::launder(buffer_start) after every insertion, is std::launder still needed?
    }

    ~my_collection()
    {
        while (size > 0) remove_last();
        ::operator delete(static_cast<void*>(buffer_start), capacity*sizeof(T), std::align_val_t{ alignof(T) });
    // is it safe, or there are more unexpected quirks?
    }
};

Here are my questions. (Sums the questions that appear in the comments).

  1. When allocating memory, is it enough to allocate it and keep the T* pointer buffer_start? Or original memory pointer is still needed? (I do not see a reason why, but until few days ago I was not aware that reintpret_cast<T*> can lead to UB).
  2. If I want to access the elements in collection, should I use std::launder(reinterpret_cast<T*>(buffer_start + i)), since I have placement new allocation or std::launder(buffer_start) solves that for me (does it even help, or just has no effect) if i remember to perform it every time I remove an element from the collection? (is it even needed when removing elements?).
  3. Is usage of delete correct in the destructor?
  4. Is it safe to construct elements using new(buffer_start+size) or I actually need the original memory pointer for that?
0

There are 0 best solutions below