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?
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 int
s 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.
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:
Since our type is const std::vector<T>
, its vector::begin()
function returns const_iterator
.
const_iterator
when dereferenced inside std::move
algorithm, returns const T&
.
std::move
algorithm uses std::move
function internally, eg:
// taken from cppreference.com
while (first != last) *d_first++ = std::move(*first++);
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&&
.
Since we don't have a constructor or operator=
defined with const T&&
parameter, overload resolution picks the one that takes const T&
instead.
Voila.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With