Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 448
matthias_buehlmann Avatar asked Dec 09 '21 16:12

matthias_buehlmann


2 Answers

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.

like image 133
Andrey Semashev Avatar answered Sep 30 '22 02:09

Andrey Semashev


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.


UPDATE

Step-by-step explanation:

  1. Since our type is const std::vector<T>, its vector::begin() function returns const_iterator.

  2. const_iterator when dereferenced inside std::move algorithm, returns const T&.

  3. std::move algorithm uses std::move function internally, eg:

    // taken from cppreference.com
    while (first != last) *d_first++ = std::move(*first++);
    
  4. std::move function in its turn returns:

    // taken from cppreference.com
    static_cast<typename std::remove_reference<T>::type&&>(t)`
    

    So, for const T& it returns const T&&.

  5. Since we don't have a constructor or operator= defined with const T&& parameter, overload resolution picks the one that takes const T& instead.

  6. Voila.

like image 23
Innocent Bystander Avatar answered Sep 30 '22 00:09

Innocent Bystander