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

2

20

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?

Doorsill answered 9/12, 2021 at 16:54 Comment(4)
You are allocating memory for Foo struct and then marks it as const, so you are preventing to change the reference of that variable. Since vector<> points to the another block of memory you can tweak it on your own. If you want to prevent that vector for changes mark it as const: const vector<int> nums {1, 2, 3}Uniflorous
@Uniflorous Not sure that explains the whole story. I can simplify the example code further. Even when bar is a const std::vector<int> the std::move is allowed.Sturdivant
The "not necessarily" is a general statement, but it may not apply to specific scenarios. For example "The autographed item you receive may not necessarily be identical to the one on display." But you say "Aha, but in my case, I know that the item is one-of-a-kind, and the only item in existence is the display item. Does the 'not necessarily' mean that the store has the power of resurrection, can bring the celebrity back from the dead, get them to autograph one more item, and then sell that one to me?"Humanitarianism
@nemo, you shouldn't be able to mutate a containers elements using a const reference to the container (barring the use of mutable). Similarly, a struct being const transfers that property to its content. So indeed, that question is very justified in my opinion!Maddalena
N
27

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 object, 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.

Nope answered 9/12, 2021 at 17:19 Comment(2)
@Evg The question and the answer is primarily talking about the std::move algorithm taking iterators, not the single object std::move. Next, the answer does not contradict what you're saying re. function that actually performs the move.Nope
Sorry, you're right. I forgot about std::move that takes iterators. Upvoted.Jago
L
9

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.

Limousin answered 9/12, 2021 at 19:14 Comment(9)
That's kind of unexpected for me but when I used msvc 19.29 the compilation failed for the second example as well saying that I attempted to reference a deleted function copyable::copyable(copyable &&). Not the case for other compilers.Jari
After reading some answers here I think that's correct behaviour. see for example https://mcmap.net/q/662922/-deleting-move-constructor-and-constructing-object-from-rvalueJari
@Jari MSVC would be wrong in this case because move constructor of copyable is not used in this code. It must be discarded during overload resolution.Nope
According to the answer(link) above the move constructor is not discarded because it actually used declared, so it takes part in overload resolution, but because it marked as deleted the compilation error pops up. Am I being wrong with that?Jari
@Jari Overload resolution takes place before the function is used. As long as the deleted function is not actually selected, it being in the overload candidate set is not an error. See here. As an example, this is what allows movable-only types, like std::unique_ptr, to work. For such types, copy constructor is defined as deleted, and when you perform a move operation, both copy and move ctors are considered as candidates, and the move ctor is selected. Discarded deleted copy ctor does not cause an error.Nope
@Jari this might be of interest: https://mcmap.net/q/135114/-why-can-we-use-std-move-on-a-const-objectLimousin
@Jari I've also added step-by-step explanation to my answer.Limousin
@InnocentBystander thanks for the explanation, I think I lost "const" in my reasoning, but it is not clear why msvc did it too.Jari
@Jari because it's buggy :P I believe Herb Sutter was saying they are going to ditch it in favor of Clang. Don't quote me on that though :)Limousin

© 2022 - 2024 — McMap. All rights reserved.