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).
- When allocating memory, is it enough to allocate it and keep the
T*
pointerbuffer_start
? Or original memory pointer is still needed? (I do not see a reason why, but until few days ago I was not aware thatreintpret_cast<T*>
can lead to UB). - 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 orstd::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?). - Is usage of
delete
correct in the destructor? - Is it safe to construct elements using
new(buffer_start+size)
or I actually need the original memory pointer for that?