Home > OS >  Why can I std::move elements from a const vector?
Why can I std::move elements from a const vector?

Time:12-10

Why does the following code compile?

#include <vector>
#include <iostream>

struct Foo {
  std::vector<int> bar = {1, 2, 3};
};

int main()
{
    Foo foo1;
    const Foo& foo2 = foo1;
    
    std::vector<int> target;
    
    std::move(foo2.bar.begin(), foo2.bar.end(), std::back_inserter(target));

    return 0;
}

The documentation of std::move says

After this operation the elements in the moved-from range will still contain valid values of the appropriate type, but not necessarily the same values as before the move.

So this can actually change the object foo2 even though it's declared const. Why does this work?

CodePudding user response:

So this can actually change the object foo2 even though it's declared const. Why does this work?

The std::move algorithm is allowed to move the input elements, if it can.

For each input element, it executes *dest = std::move(*from), where dest and from are the output and input iterators. Since from dereferences to a constant obect, std::move(*from) creates an rvalue reference const int&&. Since ints don't have user defined constructors, the assignment to *dest actually results in a copy construction that is defined by the language.

If your elements were of a class type T with user-defined copy and move constructors, overload resolution would have to select the copy constructor (T(const T&)) instead of a move constructor (T(T&&)) because const lvalue reference can bind to a const rvalue and non-const rvalue reference can't (as that would require casting away the const).

The bottom line is that std::move (the algorithm with iterators) is performing a move operation, which may or may not invoke a move constructor or assignment. If the move constructor or assignment is invoked, and that move is destructive on the source, then the algorithm will modify the source elements. In other cases, it will simply perform a copy.

CodePudding user response:

To demonstrate Andrey Semashev's answer with examples, consider this:

#include <vector>

struct movable
{
    movable() = default;
    
    movable(const movable&) = delete;
    movable& operator=(const movable&) = delete;

    movable(movable&&) = default;
    movable& operator=(movable&&) = default;
};

struct copyable
{
    copyable() = default;
    
    copyable(const copyable&) = default;
    copyable& operator=(const copyable&) = default;

    copyable(copyable&&) = delete;
    copyable& operator=(copyable&&) = delete;
};

int main()
{
    // original example
    const std::vector<int> si;
    std::vector<int> ti;
    
    std::move(si.begin(), si.end(), std::back_inserter(ti)); // OK

    // example 2
    const std::vector<copyable> sc;
    std::vector<copyable> tc;
    
    std::move(sc.begin(), sc.end(), std::back_inserter(tc)); // OK

    // example 3
    const std::vector<movable> sv;
    std::vector<movable> tv;
    
    std::move(sv.begin(), sv.end(), std::back_inserter(tv)); // ERROR - tries to use copy ctor

    return 0;
}

Even though copyable doesn't have a move constructor, example 2 compiles with no error, as std::move picks copy constructor here.

On the other hand, example 3 fails to compile because the move constructor of movable is negated (better word?) by the constness of sv. The error you get is:

error: use of deleted function 'movable::movable(const movable&)'

Here is a complete example.

  • Related