> std::move is like putting a sign on your object “I’m done with this, you can take its stuff.”
Which exactly is moving ownership.
> std::move is like putting a sign on your object “I’m done with this, you can take its stuff.”
Which exactly is moving ownership.
Not to mention, ownership in C++ is not entirely lost with moves in the traditional sense. For example, your code still has to destruct the object even if you did move it to somewhere else.
It's intended for transferring ownership, but what it actually does is mark the value as transferrable, whether or not the value is actually transferred is up to the callee.
It needs to remain destructible, and if the type satisfies things like (move-)assignable/copyable, those still need to work as well.
For boxed types, it's likely to set them into some null state, in which case dereferencing them might be ill-formed, but it's a state that is valid for those types anyway.
Thankfully clippy lints do exist here to help if you integrate that tooling
My claim is that, if I call `foo(std::move(myObj))`, it is statically knowable if `foo` receives a copy of `myObj` or whether it is moved to it. Of course, `foo` can choose to further copy or move the data it receives, but it can't choose later on if it's copied or not.
Now, if I give `foo` a pointer to myObj, it could of course choose to copy or move from it later and based on runtime info - but this is not the discussion we are having, and `std::move` is not involved from my side at all.
void foo(std::unique_ptr<int, Deleter>&& p) {
std::random_device rdev {};
auto dist = std::uniform_int_distribution<>(0, 1);
if (dist(rdev)) {
auto pp = std::move(p);
}
}If I call `foo(std::move(my_unique_ptr))`, I know for sure, statically, that my_unique_ptr was moved from, as part of the function call process, and I can no longer access it. Whether `foo` chooses to further move from it is irrelevant.
void foo() {
std::random_device rdev {};
auto dist = std::uniform_int_distribution<>(0, 1);
if (dist(rdev)) {
int res = 1 + 2;
}
}
I can tell you for sure that the result of 1 + 2 will be 3.It is completely unlike that. tsimionescu is asserting that they can always know statically whether `foo` will move its parameter. The function I provided is a counter-example to that assertion.
Of course the branch body always moves, that's what it's there for. That has no bearing on the argument.
>That has no bearing on the argument.
That is the whole argument. Let me quote the other person: "My claim is that, if I call `foo(std::move(myObj))`, it is statically knowable if `foo` receives a copy of `myObj` or whether it is moved to it."
It is saying that for "auto pp = std::move(p);" we will know if it uses the move assign constructor or the copy assign constructor.
No, it is not.
> Let me quote the other person: "My claim is that, if I call `foo(std::move(myObj))`, it is statically knowable if `foo` receives a copy of `myObj` or whether it is moved to it."
Yes. `foo`.
> It is saying that for "auto pp = std::move(p);" we will know if it uses the move assign constructor or the copy assign constructor.
`pp` is not `foo`. That `pp` uses a move constructor is not the subject of the debate.
You can literally take the function I posted, build a bit of scaffolding around it, and observe that whether the parameter is moved into `foo` or not is runtime behaviour: https://godbolt.org/z/jrPKhP35s
Nothing is constructed at call time. Check out this example, which compiles just fine, even though Foo is neither copy nor move constructible/assignable: https://godbolt.org/z/Wj57o773d
"&&" is just a type system feature, resolving function polymorphism matching rvalue reference and not lvalue reference. It's not a thing that causes a move.
I mistakenly treated in my mind `foo(obj&& x)` as `foo(obj x)`, which would indeed move or copy statically. But with functions that take an r-value reference, you're of course absolutely right, it becomes impossible to determine statically if they will move or not.
Look, the act of calling std::move and and calling a function taking an rvalue reference in no way invokes a move constructor or move assignment. It does not "move".
It's still just a reference, albeit an rvalue reference. std::move and the function shape is about the type system, not moving.
(Edit: amusingly, inside the callee it's an lvalue reference, even though the function signature is that it can only take rvalue references. Which is why you need std::move again to turn the lvalue into rvalue if you want to give it to another function taking rvalue reference)
I didn't reply to this thread until now because I thought you may simply be disagreeing about what "move" means (I would say move constructor or move assignment called), but the comment I replied to makes a more straightforward factually incorrect claim, that can easily be shown in godbolt.
If you mean something else, please sketch something up in godbolt to illustrate your point. But it does sound like you're confusing "moving" with rvalue references.
Edit: for the move to happen, you have to actually move. E.g. https://godbolt.org/z/b8M495Exq
In Rust if you pass say a Box<Goose> (not a reference, the actual object) into a function foo, it's gone, function foo might do something with that boxed goose or it might not, but it's gone anyway. If a Rust function foo wanted to give you it back they'd have to return the Box<Goose>
But C++ doesn't work that way, after calling foo my_unique_ptr is guaranteed to still exist, although for an actual unique_ptr it'll now be "disengaged" if foo moved from it. It has to still exist because C++ 98 (when C++ didn't have move semantics) says my_unique_ptr always gets destroyed at the end of its scope, so newer C++ versions also destroy my_unique_ptr for consistency, and so it must still exist or that can't work.
Creating that "hollowed out" state during a "move" operation is one of the many small leaks that cost C++ performance compared to Rust.
Maybe you're being too verbose for your point, and it would help readers if you summarize and narrow the argument to:
In Rust a function signature can force a move to happen at call time (by being non-reference and not Copy), but in C++ a function taking rvalue reference (&&) only signals the callee that it's safe to move if you want, as it's not an lvalue in the caller.
It's an added bonus that Rust prevents reusing the named variable in the caller after the move-call, but it's not what people seem to be confused about.
The point of contention is whether that is always the case, or whether there are situations where moving from the parameter is a runtime decision.
void foo(Obj && arg) {}
Does not move `arg`. It's fairly easy to write code that assumes `std::move` moves the value, but that can lead to bugs. For example: void some_function(std::vector<int> &&);
void some_function2(std::vector<int> &&);
void main() {
std::vector<int> a = { 1 };
some_function(std::move(a));
a.push_back(2);
some_other_function(std::move(a));
}
The expectation is that `some_other_function` is always called with `{ 2 }`, but this will only happen if `some_function` actually moves `a`.Code can be emitted but never executed.
That is but one thing that can static analysis can prove. It can also prove whether source code will call a move contractor or a copy constructor. Static analysis is about analyzing a program without actually running it. Analysizing what code is emitted is one way a program can be analyzed.
All the && does is prevent lvalues from being passed as arguments. It's still just a reference, not a move. Indeed, in the callee it's an lvalue reference.
But yeah, you can statically check if there exists a code path that calls the copy cons/copy assign. But you'll need to check if the callee calls ANY type's copy cons/assign, because it may not be the same type as the passed in obj.
At that point, what even is a move? char*p = smartptr.release() in the callee is a valid move into a raw pointer, satisfying the interface in callee. That's a move.[1] how could you detect that?
[1] if this definition of move offends you, then instead remember that shared_ptr has a constructor that takes an rvalue unique_ptr. The move only happens inside the move constructor.
How do you detect all cases of e.g. return cons(ptr.release()) ? It may even be the same binary code as return cons(std::move(ptr))
Probably in the end shared pointer constructor probably calls .release() on the unique ptr. That's the move.
Yup. That's what https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_p... says.
(Sorry, on phone so not full code. Hopefully I stopped autocorrect all times it interfered)
I apologize if you're making a different claim, but I'm not clear on what that is.
Anyway, for my point, here's an example where neither copy nor move happens, which one can only know by looking at the body of the callee: https://godbolt.org/z/d7f6MWcb5
By changing only the callee we can cause a move: https://godbolt.org/z/b8M495Exq
Equally we can remove the use of `std::move` in the callee, and now it's instead a copy. (of course, in this example with `unique_ptr`, it means a build failure as `unique_ptr` is not copyable)
> [assignment or construction of a variable] happens at one place
Not sure what you mean by that. The callee taking an rvalue reference could first copy, then move, if it wants to. Or do neither (per my example above). Unlike in Rust, the copy/move doesn't get decided at the call point.
You can, at one point, statically determine if the (usually const) single ampersand reference function is called, or the rvalue reference function, via standard polymorphism. But that's not the point where the move cons/assign happens, so for that one has to look in the callee.
>By changing only the callee we can cause a move
This move is for constructing t. p still is not constructed with a move constructor.
> Calling a function that takes a rref will never use a move constructor to create the paramete
This is what I mean by talking past each other. This is literally what I said.
Well, I wouldn't say the parameter is "created", since I'd say that's an imprecise term in this context.
The truth is that when you call a function which takes an r-value reference, it is NOT determined statically if the value you passed to the function is moved-from or not after the function call ends. This is ultimately similar to passing a value by non-const reference to a function - the function may or may not modify the value, so it may or may not be safe to use it the same way afterwards.
How do you mean accessing a valid object is UB?
No, it doesn't.
The standard library requires that for its classes, but not the language.
"Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state."[0]
[0] https://timsong-cpp.github.io/cppwp/n4950/lib.types.movedfro...
So you're saying if you use the language to write UB, then you get UB?
Seems kinda circular. Ok, you're not the same user who said it can be UB. But what does it then mean to same "sometimes it's UB" if the code is all on the user side?
"Sometimes code is UB" goes for all user written code.
It could be
- "don't touch this object after move" (and it's UB if you do) or
- "after move the object is in valid but unspecified state" (and you can safely call only a method without precondition) or
- "after move the object is in certain state"
- or even crazy "make sure the object doesn't get destroyed after move" (it's UB if you call delete after move or the object was created on the stack and moved from).
But of course it's a good practice to mimic the standard library's contract, first of all for the sake of uniformity.
Changing something to an rvalue means it'll now match a move constructor, but there is no guarantee a move constructor will be used, even if defined, because you've got classes like std::vector that are picky and are explicitly looking for a noexcept move constructor.
If we have foo(std::string a, std string b), and then call it like this:
std::string x;
std::string y;
foo(std::move(x), y);
Then x will be moved into a, and y will be copied into b.
The callee has no say in this - it's just the compiler implementing the semantics of the language.
An argument passed to a value parameter will be passed by copying, unless it's an rvalue (e.g. forced with std:move) where a move constructor has been defined for that type, in which case it will be moved. The callee has no say in this.
Yes, and std::move() works exactly the same. The compiler first determines whether to move or to copy, and then generates the call to the corresponding constructor or assignment operator. Just like how foo(x) doesn't tell you anything about whether a value is being copied, foo(std::move(x)) doesn't tell you anything about whether a value is being moved.
You might say "well, you need to look at all the signatures of foo() to tell if there's a copy", and to that I say, "yeah, and you need to look at what x is to tell if there's a move".
> Just because you wrote at the call site that you want to pass a copy of your object doesn't mean that the callee will actually make a copy of it.
A workaround for this, if you want an rvalue parameter to match an rvalue argument during overload resolution, is to make the alternate "value" (vs rvalue) overload a const reference argument vs a value one.
So, if you have f(T&&) and f(T), and call f(std::move(t)) then you'll get an ambiguous overload compilation error, but if you instead had f(T&&) and f(const T&), then f(std::move(t)) will match the rvalue one as you may hope for.
Okay, but that's not what I'm saying. I'm not talking about the program being invalid, I'm talking about the program being confusing to the programmer, which is what this article is about. Sure, in that case the program is ill-formed. What if you have f(const T &, double) and f(T, int)? If you call f(x, 0) you cause a copy, and if you call f(x, 0.0) you don't. A programmer who's not aware that the second overload exists will not realize this is happening, in the same way that he will not realize std::move() is not moving anything if they don't realize, for example, that the argument is const.
For example, this is perfectly valid C++, and it is guaranteed to have no issue:
std::string abc = "abc";
std::move(abc); //doesn't remove ownership or do anything really
std::print(abc); //guaranteed to print "abc"