Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing insert() on a container with move semantics when rvalue does not implement move

I'm trying to implement a container that provides insert methods with both copy and move semantics.

The implementation is something like this:

template <typename T> class Buffer {
    // lots of stuff ommitted here
    ...

    // copy semantics
    Buffer<T>::Iterator push(const T& item) {
        Buffer::Iterator head = push();
        *head = item;
        return head;
    }

    // move semantics
    Buffer<T>::Iterator push(T&& item) {
        Buffer::Iterator head = push();
        *head = std::move(item);
        return head;
    }
}

This works fine if type T (the type to be pushed into the buffer) implements a move assignment operator. However, I get a compiler error if I try to push instances of something like this:

struct Foo {
    Foo(int a) : m_bar(a) {}
    int m_bar;
    Foo& operator=(Foo& other) {
        this.m_bar = other.m_bar;
    }
}

If I try to compile buffer.push(Foo(42)); I get a compiler error for the push(T&& item)-method on the line that reads *head = std::move(item);. The error is that there is no viable overload for operator= that accepts an rvalue - which is correct, there isn't one. There is only an assignment operator that accepts lvalues.

But since I can't make sure that every object ever to be stored in my container will have a properly implemented move assignment operator I need to make sure that this case is handled correctly. What's more, std::vector handles this without a problem. When an object implements move assignment push_back will move it, if not it will copy it. Regardless if it is an rvalue or not. In fact if I put my problematic Foo rvalue that caused the error earlier into a std::vector it works like it should.

So what am I missing? How can my container implement move semantics and still support rvalue references for objects that don't implement move assignment?

like image 933
MadMonkey Avatar asked Oct 12 '14 21:10

MadMonkey


1 Answers

What you are doing/assuming wrong is the incorrect signature of a copy-assignment operator:

Foo& operator=(Foo& other);

which takes a non-const lvalue reference to the other instance. This prevents a move from falling back into a regular copy if there is no user-provided assignment operator taking an rvalue reference (that is, an rvalue can be bound by a const lvalue reference), so it should be:

Foo& operator=(const Foo& other);
//             ~~~~^

Then why it works with std::vector<Foo> then ?

The buffer.push_back(Foo(42)); statement utilizes a copy constructor, not an assignment operator. This works, since Foo has an implicitly generated copy-constructor of the following signature:

Foo(const Foo&);

which works for both lvalues and rvalues (DEMO).

What you are trying to do:

*head = std::move(item);

is to use an assignment operator. Since you have declared one on your own, a compiler can't generate implicitly the one taking a const lvalue reference, and it can't also use the user-declared one taking a non-const lvalue reference, which results in the error you see.

Consider using allocators or the placement-new operator in your push operations, using the item parameter as the argument for the constructor of Foo, instead of using copy-assignment and move-assignment operators.

like image 138
Piotr Skotnicki Avatar answered Sep 29 '22 07:09

Piotr Skotnicki