7 ms·
I've been writing C++ for 21 years now (started when I was 14). To be honest, I have never seen a solid case where move semantics provided added value (in terms
by rsp1984 7y ago
I've been writing C++ for 21 years now (started when I was 14). To be honest, I have never seen a solid case where move semantics provided added value (in terms of code readability and maintainability) over just passing object references as function parameters.
That big ugly object that would get copied on function return -- just create it before the function call and pass it in as a reference! No copy required.
- namirez 7y agoMove semantics is the language support for shallow copies. If you never use shallow copies, fine! But a lot of code bases use shallow copies, and it's a valuable tool.
- Someone 7y agoMove semantics moves objects, it doesn’t copy them. For example, if you have a class C containing a pointer P to some data, where C’s destructor frees that pointer: - a shallow copy of object O would return an object O2, containing that same pointer P, and leave O unmodified. - a move of O to O2 would (1) make O2 contain P, but also would update O to no longer have that pointer P (it has to make that change, as destructing O at a later time shouldn’t free P anymore) (1) yes, an implementation could also copy the data or increase a reference count, or, possibly, a zillion different things without running into problems.
- namirez 7y agoShallow copy is the general concept. Move semantics is a special case where the unique ownership is enforced. Here is simple example of the idea: Widget::Widget(Widget&& w) { this->ptr = w.ptr; // shallow copy w.ptr = nullptr; // nullify the source }
- clappski 7y agoYou could use; auto* p2 = std::exchange( p1, nullptr );
- rsp1984 7y agoNo, you do shallow copies by using pointers or references. No move required.
- kllrnohj 7y agoWho owns the pointer or reference and what, if anything, enforces that?
- BubRoss 7y agoMoving out of functions simplifies dependencies and arguments of calling a function. Ownership control simplifies architecture. All of eigen was originally based on template expressions, which are very complex. That technique / hack is not necessary if you move data structures. You no longer have to do elaborate work around to avoid copying temporaries.
- Const-me 7y ago> That technique / hack is not necessary if you move data structures longer have to do elaborate work around to avoid copying temporaries. AFAIK Eigen uses these scary template things to save RAM bandwidth. When you write x=a+b+c for large vectors, lazy evaluation will produce code that sequentially loads a, b, c, computes the sum in a register, stores to x. If you’ll use move semantics and temporary variables, the code will be compiled into auto tmp = a+b; x=tmp+c; This code computes the complete tmp variable, stores in RAM, then load again. For simple things like sum or component-wise multiplication of long vectors, RAM is way slower than ALUs.
- DerDangDerDang 7y agoReferences mean the optimizer cannot rule out aliasing, which is kind of a big deal.
- ska 7y agoYou can structure your code so that you don't need it - sure. But that doesn't mean that there aren't use cases where this makes managing lifetimes much clearer. RVO can often sort out your object return problem just fine. But lifetimes are more subtle. Sometime move semantics can significantly clean up your overall design.
- hermitdev 7y agoYou see the ROI when your big ugly object has a bunch of heap allocated data. You won't see any benefit when the object has everything stored by value.
- pjmlp 7y agoThat doesn't work for composable functions or fluent API designs.
- jcranmer 7y agoC++ has a few major flaws with respect to move and copy semantics. The biggest one is that copy semantics are default and silent: it requires less work to copy something than it does to use it by reference, and there is no visual indication if the value is being copied or accessed via reference. This means that it is way too easy to accidentally copy large objects (such as std::vector) without realizing you're copying them. Most newer languages have realized that implicit copy semantics is usually a bad thing, and duplicating objects requires explicitly saying that you're duplicating them (such as calling a .clone() method). Of course, some types are value types, where copy is cheap and better than reference, but such mechanisms are opt-in. C++ automatically generates these mechanisms, and gives you nasty error messages when you opt-out of default copy. Move semantics are almost always better, but in C++, with its historical baggage of opt-out-of-implicit-copy-semantics, it means that constructing move-only types requires a lot of excessive calls to std::move. Compilers do a good job of telling you when you put one too many calls to std::move in, but the code is definitely verbose compared to C++, to the point that it tends to strongly weigh against actually using C++'s ability to annotate move-only methods. Furthermore, without something like the mechanism in the blogpost here, compilers don't give any indication of API misuse, so you can't leverage move-only types to construct safe-by-construction APIs. This is something I've been tripping over a lot recently, as I have a type system where calling most methods makes the original object unusable.
- namirez 7y ago> The biggest one is that copy semantics are default and silent This is not true. The rules are simple: rule of three, rule of five, or the recommended one, rule of zero [1]. If you define a copy constructor, the compiler won't generate a move constructor. If you define a move constructor explicitly, the compiler won't generate a copy constructor silently. You have total control over how the language behaves. So in summary, either stick to the rule of 5 or the rule of zero and you won't be surprised. If you don't mind expensive copies, rule of 3 is sufficient. [1] https://en.cppreference.com/w/cpp/language/rule_of_three https://en.cppreference.com/w/cpp/language/rule_of_three
- jcranmer 7y ago
- d1zzy 7y agoThen it sounds like you haven't worked much in system development with many classes that have identity semantics (encapsulate system resources like processes, locks, mutexes, threads, etc) trying to write highly performant code while being typesafe (turn invariant violations into compile time errors). If you did you'd find out that using identity semantics objects is a PITA compared to value semantics ones. For example, how do you cleanly create a factory function? A pretty simple thing. You'd return a pointer to a dynamically allocated object? But how do you guarantee that the caller doesn't just discard that return value or doesn't forget to delete it? Also this forces dynamic allocation for that object and adds an indirection to access, even when the caller might not want that. Move semantics allow you to either make your resource wrapping objects movable (so you return them "by value" as value semantics objects but they get moved) or to use something like std::unique_ptr to wrap the returned dynamically allocated object. The former has the advantage that it gives the caller complete flexibility where to store the object (ex. it can store it locally on the stack or as a member) and avoids a pointer indirection. Similar issues exist for producing copyable but expensive to copy objects (ex. containers). Move semantics allows for a typesafe/clean way to return them from factory functions while not having to worry about lifetime and performance.
- rsp1984 7y agoCan you explain what you mean by Identity Semantics? Believe me, I would dearly like to learn something new that can benefit me in my daily work. But unfortunately I can't make sense of your examples as they stand so it would be great if you could provide some example code to illustrate your point. I do not know about your background or experience in C++ and you may very well have valid points (hence my request for code examples). However I get to read a lot of code of inexperienced programmers in my job who use && references and std::move "because that's how you do it in a modern way" and then run into crap like use after move and then wonder what the hell went wrong...
- rleigh 7y agoResources referenced by an index. Examples were provided in the post you replied to. Threads, processes, file handles, mutexes, etc. Or in an embedded context could be memory addresses for control registers etc. Many of these require functions to acquire and release, and wrapping that integer into a moveable class allows its ownership and lifetime to be carefully and safely managed.
- badamp 7y ago> I've been writing C++ for 21 years now (started when I was 14). So? As far as I know this could be 1 year+ 20 repeats. In contrast to your experience, I missed move semantics since the early 2000s. It’s not just to avoid copies. How about the ability to avoid pointers, aliasing, and allocation. Move semantics afford more than just avoiding a copy.
- rsp1984 7y agoPlease. May I ask that you provide some code examples?
- kllrnohj 7y agostd::vector<std::unique_ptr<SomeThing>> That works thanks entirely to move semantics. Do you dispute the usefulness of such a container? > That big ugly object that would get copied on function return -- just create it before the function call and pass it in as a reference! No copy required. Or just return it without a std::move and it still won't be copied. In fact 'return std::move(any_local_value)' is not just silly it's bordering on wrong as it defeats some RVO optimizations (and returning a T&& of a local is actually wrong as it's returning a reference to a local). That's not what the purpose or use is of move semantics at all.
- rsp1984 7y agoFinally, an answer with some code! > Do you dispute the usefulness of such a container? Sort of. I agree it can be useful in some cases but in reality the added value over vector<shared_ptr<SomeThing>> or just vector<SomeThingPtr> (both of which do not require move semantics) is way too small IMO to justify the butchering of the language with std::move and rvalue references. At the end of the day to me std::move is like const_cast. It's a quick fix that you can use to sort out a situation, but you shouldn't be proud of it because if you require it, chances are your design is messed up in a more fundamental way and the actual fix is sorting that out.
- kllrnohj 7y ago> vector<shared_ptr<SomeThing>> Also uses move semantics. It's not _required_ here, no, but it highly benefits from it. Huge efficiency gain to avoid all the atomic refs & unrefs during resize. > vector<SomeThingPtr> This is error-prone and a leak risk. You just made the code buggy and worse, kinda like using const_cast. > At the end of the day to me std::move is like const_cast. It's a quick fix that you can use to sort out a situation, but you shouldn't be proud of it because if you require it, chances are your design is messed up in a more fundamental way and the actual fix is sorting that out. No it's pretty much the exact opposite. Avoiding std::move or insisting you don't need it is like scattering const_cast everywhere. You're avoiding a thing that helps you categorically avoid bugs (like unique_ptr does) in favor of just winging it and hoping for the best.