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?

Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
matthias_buehlmann
  • 4,641
  • 6
  • 34
  • 76
  • 1
    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 nums {1, 2, 3}` – yemo Dec 09 '21 at 17:07
  • 1
    @yemo Not sure that explains the whole story. I can simplify the example code [further](https://godbolt.org/z/qaEfsvGjz). Even when `bar` is a `const std::vector` the `std::move` is allowed. – Nathan Pierson Dec 09 '21 at 17:11
  • 2
    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?" – Raymond Chen Dec 09 '21 at 17:28
  • @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! – Ulrich Eckhardt Dec 10 '21 at 19:42

2 Answers2

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.

Th. Thielemann
  • 2,592
  • 1
  • 23
  • 38
Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27
  • @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. – Andrey Semashev Aug 28 '23 at 14:50
  • Sorry, you're right. I forgot about `std::move` that takes iterators. Upvoted. – Evg Aug 28 '23 at 14:59
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.

  • 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. – deephace Dec 10 '21 at 10:37
  • After reading some answers here I think that's correct behaviour. see for example https://stackoverflow.com/a/56242112/197953 – deephace Dec 10 '21 at 10:57
  • 1
    @deephace 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. – Andrey Semashev Dec 10 '21 at 12:26
  • 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? – deephace Dec 10 '21 at 16:54
  • 1
    @deephace 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](https://en.cppreference.com/w/cpp/language/function#Deleted_functions). 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. – Andrey Semashev Dec 10 '21 at 18:58
  • 1
    @deephace this might be of interest: https://stackoverflow.com/a/28595207/4358570 – Super-intelligent Shade Dec 10 '21 at 19:00
  • @deephace I've also added step-by-step explanation to my answer. – Super-intelligent Shade Dec 10 '21 at 19:30
  • @InnocentBystander thanks for the explanation, I think I lost "const" in my reasoning, but it is not clear why msvc did it too. – deephace Dec 10 '21 at 19:54
  • 1
    @deephace 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 :) – Super-intelligent Shade Dec 10 '21 at 20:11