| ▲ | shmerl 14 hours ago |
| I always understood move as moving ownership, so it's not a misnomer. > 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. |
|
| ▲ | tsimionescu 12 hours ago | parent | next [-] |
| std::move itself doesn't move ownership, though. It allows the compiler to transfer ownership to the receiver of the value, but it doesn't force it in any way. This is important, because it means YOU may still be the owner of a value even after you called std::move on it. 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. |
|
| ▲ | vlovich123 14 hours ago | parent | prev | next [-] |
| Std move doesn’t move ownership. It simply casts into something that could have its ownership taken. Whether or not that actually happens is impossible to identify statically and the value after ownership is consumed is unspecified - sometimes it’s UB to access the value again, sometimes it’s not. |
| |
| ▲ | mgaunard 13 hours ago | parent | next [-] | | That's quite inaccurate. 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. | | |
| ▲ | vlovich123 7 hours ago | parent [-] | | Well it’s unspecified what empty/size return for collections after a move. Not a dereference, not UB but unspecified as I said. UB pops up in hand written code - I’ve seen it and the language doesn’t provide any protection here. Thankfully clippy lints do exist here to help if you integrate that tooling |
| |
| ▲ | shmerl 14 hours ago | parent | prev | next [-] | | May be disown would be more descriptive, but the point is that it's intended for transferring of ownership versus copying data. | | |
| ▲ | masklinn 12 hours ago | parent [-] | | > it's intended for transferring of ownership versus copying data. 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. |
| |
| ▲ | knorker 11 hours ago | parent | prev | next [-] | | After moving a value, it needs to remain in a "valid but unspecified state". How do you mean accessing a valid object is UB? | | |
| ▲ | masklinn 9 hours ago | parent | next [-] | | "Validity" is an extremely low bar in C++, it just means operations with no preconditions are legal, which in the most general case may be limited to destruction (because non-destructive moves means destruction must always be possible). | |
| ▲ | drysine 11 hours ago | parent | prev [-] | | >After moving a value, it needs to remain in a "valid but unspecified state". 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... | | |
| ▲ | knorker 9 hours ago | parent [-] | | Ok, fair enough. 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. | | |
| ▲ | drysine 9 hours ago | parent [-] | | I mean the language doesn't dictate what post-condition your class has for move-ctor or move-assignment. 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. |
|
|
| |
| ▲ | tsimionescu 13 hours ago | parent | prev [-] | | It is absolutely knowable statically if ownership will be taken. It's not necessarily very easy to do so, but the decision is 100% up to the compiler, as part of overload resolution and optimization choices (like the NRVO analysis that the article mentions). Since ownership is an inherently static concept, it doesn't even make sense to think about "runtime ownership". | | |
| ▲ | adrianN 12 hours ago | parent | next [-] | | My function can choose to move or not to move from an object based on io input. | | |
| ▲ | tsimionescu 12 hours ago | parent [-] | | Can you show an example of what you mean? 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. | | |
| ▲ | ben-schaaf 9 hours ago | parent | next [-] | | No, it is not statically knowable if it is actually moved. 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`. | | |
| ▲ | adrianN 5 hours ago | parent [-] | | Is pushing to a moved-from vector even legal? I thought in general the only guarantee you have after a move is that is save to destruct the object. |
| |
| ▲ | masklinn 12 hours ago | parent | prev [-] | | > Can you show an example of what you mean? 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);
}
}
| | |
| ▲ | tsimionescu 11 hours ago | parent [-] | | This is exactly what I meant as irrelevant. 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. | | |
| ▲ | masklinn 10 hours ago | parent | next [-] | | The only thing that is statically known here is that you’re wrong. The function I posted only moves its parameter half the time, at random. You may want to treat it as moved-from either way, but factually that’s just not what is happening. | | |
| ▲ | charcircuit 10 hours ago | parent [-] | | This is like trying to defend that you can't statically know the result of 1 + 2 because: 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. | | |
| ▲ | masklinn 9 hours ago | parent [-] | | > This is like trying to defend that you can't statically know the result of 1 + 2 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. | | |
| ▲ | charcircuit 2 hours ago | parent [-] | | >Of course the branch body always moves >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. | | |
| ▲ | masklinn 30 minutes ago | parent [-] | | > That is the whole argument 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 |
|
|
|
| |
| ▲ | vlovich123 8 hours ago | parent | prev | next [-] | | aside from what others wrote, it’s also non local - whether std::move even does anything is dependent on the signature of foo - if foo takes it by const& you may think you’ve transferred ownership when it hasn’t actually happened. | | |
| ▲ | masklinn 5 hours ago | parent [-] | | That is static though, that `foo` takes its parameter by `const&` and will thus not move it is available to the compiler (or other tooling) at compile time. 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. |
| |
| ▲ | knorker 9 hours ago | parent | prev | next [-] | | No: https://godbolt.org/z/d7f6MWcb5 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 | |
| ▲ | tialaramex 8 hours ago | parent | prev [-] | | Yeah no. 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. |
|
|
|
| |
| ▲ | charcircuit 10 hours ago | parent | prev [-] | | I don't understand the downvoted here. Either the compiler emits the code to call a move constructor or it doesn't. | | |
| ▲ | Maxatar 10 hours ago | parent [-] | | Static analysis is about proving whether the code emitted by a compiler is actually called at runtime. It's not simply about the presence of that code. Code can be emitted but never executed. | | |
| ▲ | charcircuit 2 hours ago | parent [-] | | >Static analysis is about proving whether the code emitted by a compiler is actually called at runtime. 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. |
|
|
|
|
|
| ▲ | HarHarVeryFunny 8 hours ago | parent | prev | next [-] |
| Well, no, because CAN take isn't the same as WILL take. 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. |
| |
| ▲ | fluoridation 7 hours ago | parent [-] | | In that sense, std::move() is no different than other passing semantics. 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. | | |
| ▲ | HarHarVeryFunny 6 hours ago | parent [-] | | I'm not sure what you are saying. 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. | | |
| ▲ | fluoridation 4 hours ago | parent [-] | | Who says there's only one resolution candidate? A different overload could be defined elsewhere that the compiler prefers for that particular combination of arguments, that doesn't cause a copy. std::move() works the same way. The semantics of the operation is defined not by what's at the call site, but by the context. | | |
| ▲ | HarHarVeryFunny 4 hours ago | parent [-] | | Sure overload resolution happens first, but once the compiler has found the correct match then the way arguments are passed depends only on the function signature of that match (callee), and how the caller is passing. 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. | | |
| ▲ | fluoridation 4 hours ago | parent [-] | | >Sure overload resolution happens first, but once the compiler has found the correct match then the way arguments are passed depends only on the function signature of that match (callee), and how the caller is passing. 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". | | |
| ▲ | HarHarVeryFunny 3 hours ago | parent [-] | | Not sure how this relates to your original claim, below, that I have been responding to? > 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. | | |
| ▲ | fluoridation 2 hours ago | parent [-] | | Meaning, just like move semantics, overload resolution can sometimes be surprising, and the compiler may not always do what you expected if you don't fully understand the types you're working with. std::move() is not special in this sense. | | |
| ▲ | HarHarVeryFunny 6 minutes ago | parent [-] | | Ah, yes, unfortunately the language says that a value parameter of type T and an rvalue parameter of type T (T&&) are both equal priority "exact matches" to a call passing an rvalue argument, but at least if that was the only difference between two functions you'd get an ambiguous overload compilation error rather than it just selecting an unexpected one. A workaround for this, if you want an rvalue parameter to match an rvalue argument during overload resolution, is to make the "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. |
|
|
|
|
|
|
|
|
|
| ▲ | usefulcat an hour ago | parent | prev | next [-] |
| std::allow_move probably would have been a more accurate name for std::move. |
|
| ▲ | cocoto 14 hours ago | parent | prev [-] |
| Personally I see std::move more like removing ownership because it’s not explicit from its call where the ownership is transferred. |
| |
| ▲ | tsimionescu 12 hours ago | parent [-] | | Even that is a bit suspect, because ownership may well remain with you even after the call, so it's not really removed. 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"
|
|