Move semantics for a custom vector and allocators

  allocator, c++, move-semantics, stdvector

I’m implementing a basic std::vector by using an allocator, following PPP by Stroustrup, and in particular I’m sure that all the functions like resize, push_back, emplace_back, and so on are okay.

What is really puzzling me a bit is the copy and move semantics: it works now, but I find it a bit too hard-coded and, more importantly, it’s leaking 1 byte in the move assignment, as can be seen from this

==7853== LEAK SUMMARY:
==7853==    definitely lost: 1 bytes in 1 blocks
==7853==    indirectly lost: 0 bytes in 0 blocks
==7853==      possibly lost: 0 bytes in 0 blocks
    

In the following the whole code. I’d like to see especially how the move assignment should be implemented, because I believe that a problem is the fact that I have to move an allocator, and I’m totally new to this topic.

#include <iostream>
#include <memory>
#include <utility>
#include <initializer_list>
#include <string>

template <typename T, typename Allocator = std::allocator<T>>
class Vector
{
private:
    Allocator allocator; //used to handle memory for the elements
    T *elem;
    std::size_t _size{};
    std::size_t _capacity{};

    void reserve(const std::size_t n)
    {
        if (_capacity < n) //if the capacity is smaller than what I want to allocate
        {
            T *tmp{std::allocator_traits<Allocator>::allocate(allocator, n)};
            for (std::size_t i = 0; i < _size; ++i)
            {
                // allocator.construct(tmp + i, std::move(elem[i])); //tmp[i] = std::move(elem[i]);
                std::allocator_traits<Allocator>::construct(allocator, tmp + i, elem[i]);
            }

            for (std::size_t i = 0; i < _size; ++i)
            {
                std::allocator_traits<Allocator>::destroy(allocator, elem + i);
            }
            std::allocator_traits<Allocator>::deallocate(allocator, elem, _capacity);

            elem = tmp;    //move the pointer
            _capacity = n; //size increased!
        }
    }

    void check_and_increase_capacity()
    {
        if (_capacity == 0)
        {
            reserve(8);
        }
        else if (_size == _capacity)
        {
            reserve(2 * _size);
        }
    }

    template <typename O>
    void _push_back(O &&x)
    {
        check_and_increase_capacity();
        std::allocator_traits<Allocator>::construct(allocator, elem + _size, std::forward<O>(x));
        ++_size;
    }

public:
    explicit Vector(std::initializer_list<T> list) : elem{std::allocator_traits<Allocator>::allocate(allocator, list.size())}, _size{list.size()}, _capacity{list.size()}
    {
        std::uninitialized_copy(list.begin(), list.end(), begin());
        std::cout << "custom cstr"
                  << "n";
    }

    ~Vector() noexcept
    {
        for (std::size_t i = 0; i < _size; ++i)
        {
            std::allocator_traits<Allocator>::destroy(allocator, elem + i); //call the destructor
        }
        std::allocator_traits<Allocator>::deallocate(allocator, elem, _capacity); //deallocate memory
    }

    T *begin() { return elem; }
    const T *begin() const { return elem; }

    T *end() { return elem + _capacity; }
    const T *end() const { return elem + _capacity; }

    Vector(const Vector &v) : allocator{std::allocator_traits<Allocator>::select_on_container_copy_construction(v.allocator)}, elem{std::allocator_traits<Allocator>::allocate(allocator, v._capacity)}
    {
        T *tmp{std::allocator_traits<Allocator>::allocate(allocator, _capacity)};
        _size = v._size;
        _capacity = v._capacity;
        for (std::size_t i = 0; i < _size; ++i)
        {
            std::allocator_traits<Allocator>::construct(allocator, tmp + i, std::move(v[i]));
        }

        std::uninitialized_copy(v.begin(), v.end(), begin()); //copy the elements

        //destroy and deallocate tmp
        for (std::size_t i = 0; i < _size; ++i)
        {
            std::allocator_traits<Allocator>::destroy(allocator, tmp + i);
        }
        std::allocator_traits<Allocator>::deallocate(allocator, tmp, _capacity);
    }

    Vector &operator=(const Vector &v)
    {
        T *tmp{std::allocator_traits<Allocator>::allocate(allocator, v._capacity)};
        std::uninitialized_copy(v.begin(), v.end(), begin()); //copy the elements
        for (std::size_t i = 0; i < v._size; ++i)
        {
            std::allocator_traits<Allocator>::destroy(allocator, elem + i);
        }
        std::allocator_traits<Allocator>::deallocate(allocator, elem, _capacity);
        elem = tmp;
        _size = v._size;
        _capacity = v._capacity;
        return *this;
    }

    Vector(Vector &&v) noexcept : allocator{std::move(v.allocator)}
    {

        elem = v.elem;
        v.elem = nullptr;
        _size = v._size;
        v._size = 0;
        _capacity = v._capacity;
        v._capacity = 0;
        std::cout << elem << "n";
    }

    Vector &operator=(Vector &&v) noexcept
    {
        std::cout << "move assignment"
                  << "n";
        allocator = std::move(v.allocator);
        elem = v.elem;
        v.elem = nullptr;
        _size = v._size;
        v._size = 0;
        _capacity = v._capacity;
        v._capacity = 0;

        return *this;
    }

    void push_back(const T &x)
    {
        _push_back(x);
    }

    void push_back(T &&x)
    {
        _push_back(std::move(x));
    }

    template <typename... Types>
    void emplace_back(Types &&...args)
    {
        check_and_increase_capacity();
        std::allocator_traits<Allocator>::construct(allocator, elem + _size, std::forward<Types>(args)...);
    }

    T &operator[](const std::size_t i) noexcept { return elem[i]; }
    const T &operator[](const std::size_t i) const noexcept { return elem[i]; }

    friend std::ostream &operator<<(std::ostream &os, const Vector &v)
    {
        for (std::size_t i = 0; i < v._size; i++)
        {
            std::cout << v[i] << "n";
        }
        return os;
    }

    void resize(const std::size_t newsize, T val = T{})
    {
        reserve(newsize);
        for (std::size_t i = _size; i < newsize; ++i)
        {
            std::allocator_traits<Allocator>::construct(allocator, elem + i, val);
        }

        //destroy all the new extra elements
        for (std::size_t i = newsize; i < _size; ++i)
        {
            std::allocator_traits<Allocator>::destroy(allocator, elem + i); //just destroy them, don't call release the memory!
        }

        _size = newsize;
    }
};

struct Foo
{
    std::string _s;
    Foo()
    {
        std::cout << "foo cstr"
                  << "n";
    };
    explicit Foo(const std::string &s) : _s{s} {}
    ~Foo() = default;
};

int main()
{

    Vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    std::cout << v << "n";
    v.push_back(11);
    std::cout << "After push_back n"
              << v << "n";

    Vector<Foo> w{{}, {}};
    w.emplace_back();

    v.resize(6);
    std::cout << "After resize n"
              << v << "n";

    // Copy/Move semantics tests
    Vector<int> v1{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    Vector<int> v2{v1};
    std::cout << "After copy cstr n"
              << v2 << "n";
    v2.push_back(20);
    std::cout << "after push_back n"
              << v2 << "n";

    Vector<int> v3{};
    v3 = v1;
    std::cout << v3 << "and v1: n"
              << v1 << "n";

    Vector<int> v4{std::move(v1)};
    std::cout << v4 << "and v1: n"
              << v1 << "n";

    Vector<int> v5{};
    v5 = std::move(v4);
    std::cout << v5 << "and v4: n"
              << v4 << "n";
    return 0;
}

Source: Windows Questions C++

LEAVE A COMMENT