0

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?
  • did you forget `return` ? `return *(std::launder(T + index));` ? – 463035818_is_not_an_ai Mar 11 '21 at 11:53
  • 1
    From [`std::launder`](https://en.cppreference.com/w/cpp/utility/launder): *"an object X is located at the address A"* *"Otherwise, the behavior is undefined"*, so your first `launder` seems (pedantically) UB. (You no have yet object in your buffer). – Jarod42 Mar 11 '21 at 12:01
  • 1
    Pointer arithmetic is also very restrictive, leading also to UB pedantically (you don't have **array**, just elements one after the other). – Jarod42 Mar 11 '21 at 12:03
  • Following on from @Jarod42 comment - could the 1st `std::launder` not be replaced with placement new ? – Richard Critten Mar 11 '21 at 12:10
  • @RichardCritten: In the idea, `buffer` should not construct capacity `T`. As I remember, implementing its own `vector` class without UB (whereas library/compiler implementer **can** do, as they "control" UB) is not possible. – Jarod42 Mar 11 '21 at 12:48
  • @Jarod42 I fixed the first launder. Are you saying that pointer arithmetic is not valid, pedantically?. Does it mean that in general T* ptr = new T[n] and then using (T+i) is not guaranteed to work? (Is there difference between &T[i] and (T + i), formally?). – Myrddin Krustowski Mar 11 '21 at 13:00
  • @RichardCritten The idea is not to construct instances of T which may not be possible time consuming. This the reason these acrobatics are performed in first place. Otherwise. new T[capacity] would do. – Myrddin Krustowski Mar 11 '21 at 13:00
  • @Jarod42 As far as I am aware is supposed to solve the problem assuming that we work with raw pointers and not smart pointers. Also, allocators were changed and some restrictions on overwriting objects was made to reduce UB in some cases. Am I missing something? – Myrddin Krustowski Mar 11 '21 at 13:03
  • `T* ptr = new T[n]` (creates an **array**), `ptr[i]` is correct, "issue" is that you create N contiguous objects and treat them as array. In the similar way `int a[3][3]{}; int* p = &a[0][0]; p[4] /*UB even if in practice, it is a[1][1]*/`, `int[3][3]` as same layout than `int[9]` but pointer arithmetic treat them differently. – Jarod42 Mar 11 '21 at 13:13
  • @jarod42 - is there a formal difference between &ptr[i] and (ptr + i)? Are you saying that if ptr is has type of T*, then there is no formal guarantee that static_cast(ptr) + i * sizeof(T) and ptr + i are equal? – Myrddin Krustowski Mar 11 '21 at 13:46
  • Not sure if `&ptr[size]` is not UB as accessing out-of-bound access, but else (assuming no evil `overload&`, else `std::addressof(ptr[i])`) `&ptr[i]` and `(ptr + i)` are equivalent. – Jarod42 Mar 11 '21 at 13:53
  • Cannot do arithmetic on `void*` you probably meant `char*`. Issue is not the result, but to have the right to do the operation, as `/*int i */; i + 1 - 1` is not equivalent to `i`, (signed overflow is UB). – Jarod42 Mar 11 '21 at 14:09
  • @Jarod42 Well, subtraction of integer from pointer is not defined, but addition of pointer and integer (pointer_t + integer) is and type type pointer_t. Are you certain that there are no guarantees beyond that? Namely, as far as I am aware reinterpret_cast(T) is always defined. As is T+i. Are you saying that it is not guaranteed that reinterpret_cast(Tptr) and reinterpret_cast(T+1)+sizeof(T) are not guaranteed to be equal? – Myrddin Krustowski Mar 11 '21 at 15:55
  • See [expr.add](https://eel.is/c++draft/expr.add#4), pointer + integer is only valid (excluding `nullptr + 0`) when `P` points to an element of an array, and pedantically, `buffer_start` is not an array. (even if in practice it is ok). – Jarod42 Mar 11 '21 at 16:07
  • footnote 73 at the bottom of the page states that single object is treated as an array of length 1 for this purpose. – Myrddin Krustowski Mar 11 '21 at 16:17
  • @Jarod42 Can we agree that pointer arithmetic is well defined even when pointer does not point to an array member? It seems to be the case, even according to the link that you've provited (footnote 73 at the bottom of the page). If you do, I'd like to correct the question a bit. – Myrddin Krustowski Mar 11 '21 at 18:25
  • We don't agree :) footnode is to allow `int i; for (int* p = &i; p != &i + 1; ++p) ;` so `int i;` is "equivalent" to `int i[1];` – Jarod42 Mar 11 '21 at 19:31
  • @Jarod42 I know that the question is tagged as c++17, however, in c++20, aren't arrays constructed implicitly? – Myrddin Krustowski Mar 17 '21 at 23:11
  • IIRC, C++20 fixes life time/object creation issue when reading buffer. so maybe. – Jarod42 Mar 18 '21 at 08:18

0 Answers0