0

As part of an assignment in Uni I have to implement a partial clone of the stl::vector class. I've managed nearly all the functionality but I am stumped with how to do push_back(T&& c). We're not allowed to implement emplace like STL itself uses. The object used to test the implementation of the push_backs checks which constructors and operators are invoked on it. For this one ONLY the Move Constructor is allowed to be called. Due to the whole corona hoohaa it's extremely difficult to get any help from teacher / other students.

This is what I have so far:

template <typename T>
void Vector<T>::push_back(T&& c)
{
  // code that resizes the vector if needed. Checked to make sure it doesn't invoke anything.
  T* a = new T(std::move(c)); // Invokes the Move Constructor, which is fine.
  auto b = (_vector_data+_vector_size); // Destination
  //std::move(); Move invokes MA
  //std::swap(b, a); // Doesn't work, swaps but the value of a ends up somewhere else, I use this method in push_back(const T& c) successfully.
  //std::copy(); // Copy invokes CA
  ++_vector_size;
}

_vector_data is a T*, _vector_size is a size_t.

To help illustrate why I thought in these patterns here is the other push_back:

template <typename T>
void Vector<T>::push_back(const T& c)
{
  // code that resizes the vector if needed.
  T* a = new T(c); // Invokes CC.
  auto b = (_vector_data+_vector_size); // Destination
  std::swap(b, a);
  ++_vector_size;
}

I am at a complete loss what to do. I've perused the source material, the book, and google but I suppose I might be too stupid to realize what I do wrong or where to look :P Halp?

I also looked at this: Vector push_back move implementation which looks like my first attempt. It fails the test because the last line invokes the copy assignment operator, causing a failed assert.

Edit:

To clarify. The portion of the test program runs this:

struct C {
    static std::string usedConstr;
    void Test() {}
    C() {
        usedConstr += "DC";
    }
    C(const C& c) {
        usedConstr += "CC";
    }
    C(C&& c) {
        usedConstr += "MC";
    }
    C& operator=(const C& c) {
        usedConstr += "CA";
        return *this;
    }
    C& operator=(C&& c) {
        usedConstr += "MA";
        return *this;
    }
};

std::string C::usedConstr{};

//Test push_back&&
void TestMove() {
    Vector<C> a;
    C c;
    assert(C::usedConstr == "DC");
    a.reserve(4);
    C::usedConstr = "";
    a.push_back(c);
    assert(C::usedConstr == "CC");
    C::usedConstr = "";
    a.push_back(std::move(c));
    assert(C::usedConstr == "MC");
}

Where it is the last assert that fails. I can't get there by only invoking the Move Constructor.

Daniel
  • 33
  • 1
  • 5
  • if it's a vector, you should have something like a buffer where all the elements are stored, so there would be nothing like this `T* a = new T(c)`, just `*b = c` or `*b = std::move(c)` – fas Apr 07 '20 at 15:51
  • 1
    From the code shown, I suspect that this "`Vector`" is implemented as an array of pointers to individual elements? Regardless, the problem in your code is probably outside of what's being shown here. – Drew Dormann Apr 07 '20 at 15:53
  • @fas This causes the Move Assignment operator check to fire on the stored object, which causes an assert fail. – Daniel Apr 07 '20 at 16:07
  • @DrewDormann The rest of the code is not involved in how things are inserted into the array though? – Daniel Apr 07 '20 at 16:07
  • You might want to have a look [`std::aligned_storage`](https://en.cppreference.com/w/cpp/types/aligned_storage). That page even gives an example how to use it for a vector-like class. – bitmask Apr 07 '20 at 16:59

2 Answers2

2

std::vector would normally be implemented by allocating memory via operator new or some allocator passed to the vector class and constructing new objects in that storage. Here some demonstration for the case of calling operator new directly without use of an allocator. (The real std::vector always uses an allocator, std::allocator by default.):

_vector_data = static_cast<T*>(::operator new(sizeof(T)*new_capacity));

This allocates memory, but does not explicitly create any objects. (Note that this is memory is only correctly aligned for non-overaligned types, but that should be an acceptable limitation.)

Then to place a new object into that storage, you would use a non-allocating placement-new:

template <typename T>
void Vector<T>::push_back(T&& c)
{
    ::new(static_cast<void*>(_vector_data+_vector_size)) T(std::move(c));
    ++_vector_size;
}

(only that you need to verify that the capacity is sufficient first)

If you don't care about some edge cases relating to operator new class overloads, you can use

new(_vector_data+_vector_size) T(std::move(c));

instead of

::new(static_cast<void*>(_vector_data+_vector_size)) T(std::move(c));

and

operator new

instead of

::operator new

In order to remove elements you would then call their destructors explicitly with e.g.

(_vector_data+_vector_size)->~T();

and you would free the memory by calling the operator delete matching the form you used for operator new.

This technically has undefined behavior before C++20, because pointer arithmetic as in _vector_data+_vector_size is only allowed if _vector_data point to an element of an array, but we never created an array. Since C++20 this array is created implicitly.

There is no way to implement a std::vector that has the proper array semantics on pointers to its elements with the requirements you have given before C++20 without relying on semantics that are undefined behavior according to the standard, although this undefined behavior is more of a technicality and would probably never cause issues in practice.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • Thank you. I am not entirely sure what is going on here but it does work and passes all the tests. I think I have a lot more studying to do. My hat off to you, mate. Does it allocate a new void pointer in the allotted slot of the array, then move the contents of the T&& in there? – Daniel Apr 07 '20 at 16:45
  • You can't use `new` for implementing `vector`: it doesn't provide the opportunity of manual construction and destruction which is needed for functions like `reserve()` and `pop_back()`. – passing_through Apr 07 '20 at 16:51
  • @Daniel The first line using `operator new` allocates a memory block large enough to hold `_vector_capacity` objects of type `T`, but does not create any of these objects. `new(_vector_data+_vector_size) T(std::move(c));` then creates a new `T` object *directly in that storage at the location `_vector_data+_vector_size`. This is called a *placement-new*. Please read up on how that is used and how it is different from a regular `new`. (Important differences!) – walnut Apr 07 '20 at 17:00
  • @passing_through Was this addressed to me? This is why I am using the non-allocating placement-new form, which *does* allow constructing and destructing elements in-place. – walnut Apr 07 '20 at 17:01
  • @walnut you're right, sorry. Hope I at least provided some info about allocating new for other readers. – passing_through Apr 07 '20 at 17:09
  • Thanks to both of you for this. Been quite informative. I do evidently have a good way to go :P. – Daniel Apr 07 '20 at 17:56
0

It would seem that you forgot to adjust your vector's size in this version of push_back.

template <typename T>
void Vector<T>::push_back(T&& c)
{
  T* a = new T(std::move(c)); // Invokes the Move Constructor, which is fine.
  auto b = (_vector_data+_vector_size); // Destination
  std::move(b,a);
  ++_vector_size;  // WHOOPS
}

And since I'm here, there's no need to assign the pointer to a. That is the wrong destination.

void Vector<T>::push_back(T&& c)
{
  auto b = (_vector_data+_vector_size); // Correct destination
  b = new T(std::move(c)); // Invokes the Move Constructor, which is fine.
  ++_vector_size;
}
Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
  • Thanks Drew! I knew I forgot to copy that one line. It's not the problem, however. The updated code you have there doesn't work, it updates b accordingly but not the underlying vector. I.e. ``` Vector vecBar("Bar"); vecBar.push_back('a'); assert(vecBar == "Bara"); // fails ``` – Daniel Apr 07 '20 at 16:34