Search code examples
c++movepush-back

Implementing push_back(T&& c) in custom Vector<T> class


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.


Solution

  • 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.