Why can we use `std::move` on a `const` object?
Asked Answered
P

6

188

In C++11, we can write this code:

struct Cat {
   Cat(){}
};

const Cat cat;
std::move(cat); //this is valid in C++11

when I call std::move, it means I want to move the object, i.e. I will change the object. To move a const object is unreasonable, so why does std::move not restrict this behaviour? It will be a trap in the future, right?

Here trap means as Brandon mentioned in the comment:

" I think he means it "traps" him sneaky sneaky because if he doesn't realize, he ends up with a copy which is not what he intended."

In the book 'Effective Modern C++' by Scott Meyers, he gives an example:

class Annotation {
public:
    explicit Annotation(const std::string text)
     : value(std::move(text)) //here we want to call string(string&&),
                              //but because text is const, 
                              //the return type of std::move(text) is const std::string&&
                              //so we actually called string(const string&)
                              //it is a bug which is very hard to find out
private:
    std::string value;
};

If std::move was forbidden from operating on a const object, we could easily find out the bug, right?

Plagio answered 18/2, 2015 at 22:22 Comment(13)
But try to move it. Try to change its state. std::move by itself doesn't do anything to the object. One could argue std::move is poorly named.Dorisdorisa
It doesn't actually move anything. All it does is cast to an rvalue reference. try CAT cat2 = std::move(cat);, assuming CAT supports regular move-assignment.Baumgardner
std::move is just a cast, it does not actually move anythingGalatians
@WhozCraig: Careful, as the code you posted compiles and executes without warning, making it sort of misleading.Decoder
@MooingDuck Never said it wouldn't compile. it works only because the default copy-ctor is enabled. Squelch that and the wheels fall off.Baumgardner
If std::move was forbidden from operating on a const object, it would still be allowed on types that lack a move constructor, which would still silently copy, exactly what you want to prevent.Corissa
@hvd That seems like a bit of a non-argument to me. Just because OP's suggestion doesn't fix all bugs in the world doesn't necessarily mean it is a bad idea (it probably is, but not for the reason you give).Ailyn
I'd say there's nothing wrong with move in Scott's snippet. On the other hand taking an argument by const value is odd and you usually don't want to do that. That's really in a section about move?Deponent
@ChrisDrew But it doesn't even fix the bug the OP wants to fix (insofar as it can be called a bug). The OP wants to fix copy constructors being called even when the caller spells out std::move. That would still be the case with the OP's change.Corissa
Poor question : the problem is that you're trying to move a cat, which can't succeed by definition.Vaas
Compiler actually have all needed information to tell if move semantic will be applied. At least compiler should give a choice of turning on warnings on such ridiculous errors. I just found dummy error in my code where I took const Value &value = stack.top() instead of Value &value = stack.top() before doing std::move(value).Lafreniere
Warning proposal for gcc: gcc.gnu.org/bugzilla/show_bug.cgi?id=67906Lafreniere
@Deponent If you stress const-correctness, you will make anything that doesn't change const to document its nature. Here, value(std::move(text)) is an attempt at changing text, so it makes no sense. However, there's nothing wrong with a const that came by value. This example is in a chapter about std::move, because a person new to the feature needs to know that std::move will basically do nothing if you pass it a const.Unbound
R
77
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

here we see a use of std::move on a T const. It returns a T const&&. We have a move constructor for strange that takes exactly this type.

And it is called.

Now, it is true that this strange type is more rare than the bugs your proposal would fix.

But, on the other hand, the existing std::move works better in generic code, where you don't know if the type you are working with is a T or a T const.

Ringed answered 18/2, 2015 at 22:44 Comment(2)
+1 for being the first answer that actually attempts to explain why you would want to call std::move on a const object.Ailyn
+1 for showing a function taking const T&&. This expresses an "API protocol" of the kind "I will take an rvalue-ref but I promise I won't modify it". I guess, apart from when using mutable, it's uncommon. Maybe another use case is to be able to use forward_as_tuple on almost anything and later use it.Resor
D
171

There's a trick here you're overlooking, namely that std::move(cat) doesn't actually move anything. It merely tells the compiler to try to move. However, since your class has no constructor that accepts a const CAT&&, it will instead use the implicit const CAT& copy constructor, and safely copy. No danger, no trap. If the copy constructor is disabled for any reason, you'll get a compiler error.

struct CAT
{
   CAT(){}
   CAT(const CAT&) {std::cout << "COPY";}
   CAT(CAT&&) {std::cout << "MOVE";}
};

int main() {
    const CAT cat;
    CAT cat2 = std::move(cat);
}

prints COPY, not MOVE.

http://coliru.stacked-crooked.com/a/0dff72133dbf9d1f

Note that the bug in the code you mention is a performance issue, not a stability issue, so such a bug won't cause a crash, ever. It will just use a slower copy. Additionally, such a bug also occurs for non-const objects that don't have move constructors, so merely adding a const overload won't catch all of them. We could check for the ability to move construct or move assign from the parameter type, but that would interfere with generic template code that is supposed to fall back on the copy constructor. And heck, maybe someone wants to be able to construct from const CAT&&, who am I to say he can't?

Decoder answered 18/2, 2015 at 22:28 Comment(9)
Upticked. Worth noting the implicit deletion of the copy-ctor upon user-defined definition of the regular move-constructor or assignment operator will also demonstrate this via broken compilation. Nice answer.Baumgardner
Also worth mentioning that a copy-constructor which needs a non-const lvalue also won't help. [class.copy] §8: "Otherwise, the implicitly-declared copy constructor will have the form X::X(X&)"Fink
I don't think he meant "trap" in computer/assembly terms. I think he means it "traps" him sneaky sneaky because if he doesn't realise, he ends up with a copy which is not what he intended. I guess..Calicut
Let's write a const CAT&& constructor on a CAT object that has all mutable members :)Lightproof
std::move isn't an attempt to move something. All it does is change a type T to T&& (or const T to const T&&). That could be done anywhere, including places where "moving" has no meaning such as a function like int f(int const &&i) { return 2; }.Unbound
if std::move may not actually move anything, it should be called std::move_or_copy then.Ical
@Ical std::enable_move_from?Decoder
std::permission_granted_to_suck_out_the_objects_innards_and_leave_behind_an_empty_huskBreakneck
Here's a story of a real bug (in Qt) from this tricky move trap that was more than just a performance issue... arnorehn.de/blog/2023/09/27/…Skiing
R
77
struct strange {
  mutable size_t count = 0;
  strange( strange const&& o ):count(o.count) { o.count = 0; }
};

const strange s;
strange s2 = std::move(s);

here we see a use of std::move on a T const. It returns a T const&&. We have a move constructor for strange that takes exactly this type.

And it is called.

Now, it is true that this strange type is more rare than the bugs your proposal would fix.

But, on the other hand, the existing std::move works better in generic code, where you don't know if the type you are working with is a T or a T const.

Ringed answered 18/2, 2015 at 22:44 Comment(2)
+1 for being the first answer that actually attempts to explain why you would want to call std::move on a const object.Ailyn
+1 for showing a function taking const T&&. This expresses an "API protocol" of the kind "I will take an rvalue-ref but I promise I won't modify it". I guess, apart from when using mutable, it's uncommon. Maybe another use case is to be able to use forward_as_tuple on almost anything and later use it.Resor
S
27

One reason the rest of the answers have overlooked so far is the ability for generic code to be resilient in the face of move. For example lets say that I wanted to write a generic function which moved all of the elements out of one kind of container to create another kind of container with the same values:

template <class C1, class C2>
C1
move_each(C2&& c2)
{
    return C1(std::make_move_iterator(c2.begin()),
              std::make_move_iterator(c2.end()));
}

Cool, now I can relatively efficiently create a vector<string> from a deque<string> and each individual string will be moved in the process.

But what if I want to move from a map?

int
main()
{
    std::map<int, std::string> m{{1, "one"}, {2, "two"}, {3, "three"}};
    auto v = move_each<std::vector<std::pair<int, std::string>>>(m);
    for (auto const& p : v)
        std::cout << "{" << p.first << ", " << p.second << "} ";
    std::cout << '\n';
}

If std::move insisted on a non-const argument, the above instantiation of move_each would not compile because it is trying to move a const int (the key_type of the map). But this code doesn't care if it can't move the key_type. It wants to move the mapped_type (std::string) for performance reasons.

It is for this example, and countless other examples like it in generic coding that std::move is a request to move, not a demand to move.

This characteristic was recognized and designed into the very first paper on move semantics:

The "request to move" semantics turn out to be very handy in generic code. One can request that a type move itself without having to know whether or not the type is really movable. If the type is movable it will move, else if the type is copyable, it will copy, else you will get a compile-time error.

Shuler answered 19/2, 2015 at 0:10 Comment(2)
A bit late to the party, but I'm tempted to say (as others did in this thread) that std::move is not an attempt to move anything. It also does not reject const arguments at all: It does for a const exactly what it always does, return a T&& from a T. It is the subsequent overload resolution for that type that does not match the move constructor (unless that gets a const T&&, as demonstrated by Yakk). Instead, the copy constructor is chosen.Longanimity
When @HowardHinnant talks about move, we all listen.Mccafferty
W
2

I have the same concern as the OP.

std::move does not move an object, neither guarantees the object is movable. Then why is it called move?

I think being not movable can be one of following two scenarios:

1. The moving type is const.

The reason we have const keyword in the language is that we want the compiler to prevent any change to an object defined to be const. Given the example in Scott Meyers' book:

    class Annotation {
    public:
     explicit Annotation(const std::string text)
     : value(std::move(text)) // "move" text into value; this code
     { … } // doesn't do what it seems to!    
     …
    private:
     std::string value;
    };

What does it literally mean? Move a const string to the value member - at least, that's my understanding before I reading the explanation.

If the language intends to not do move or not guarantee move is applicable when std::move() is called, then it is literally misleading when using word move.

If the language is encouraging people using std::move to have better efficiency, it has to prevent traps like this as early as possible, especially for this type of obvious literal contradiction.

I agree that people should be aware moving a constant is impossible, but this obligation should not imply the compiler can be silent when obvious contradiction happens.

2. The object has no move constructor

Personally, I think this is a separate story from OP's concern, as Chris Drew said

@hvd That seems like a bit of a non-argument to me. Just because OP's suggestion doesn't fix all bugs in the world doesn't necessarily mean it is a bad idea (it probably is, but not for the reason you give). – Chris Drew

Wicker answered 26/8, 2016 at 20:8 Comment(0)
L
2

I'm surprised nobody mentioned the backward compatibility aspect of this. I believe, std::move was purposely designed to do this in C++11. Imagine you're working with a legacy codebase, that heavily relies on C++98 libraries, so without the fallback on copy assignment, moving would break things.

Lumen answered 5/10, 2018 at 17:27 Comment(0)
T
2

Fortunately you can use clang-tidy's check to find such issues: https://clang.llvm.org/extra/clang-tidy/checks/performance/move-const-arg.html

Tollgate answered 14/2, 2020 at 9:6 Comment(1)
Link rot: the link is broken.Breakneck

© 2022 - 2024 — McMap. All rights reserved.