4 ms·
Move semantics are a great addition to C++, but off the top of my head, there are two warts: - C++ moves are not destructive (as compared to move semantics in
by butterthebuddha 7y ago
Move semantics are a great addition to C++, but off the top of my head, there are two warts:
- C++ moves are not destructive (as compared to move semantics in Rust). This means that types must arrange for a valid "moved-from" state. This isn't a huge issue if the type has a natural sentinel value, but not all types have one and I often end up wrapping class members in std::optional to arrange for one. Frankly, this is annoying, because I now have to pay the overhead of std::optional for no good reason (and also use a C++17 compiler, which is not always available).
- the syntax for rvalue references and perfect forwarding references is the same, which is the cause for much confusion, especially among beginners. I happily used move semantics for a year before I realized that rvalue references and perfect forwarding references are distinct concepts.
- GeneralMayhem 7y ago>C++ moves are not destructive, as they are in Rust. This means that types must arrange for a "moved-from" state Using an object that has been moved from is only required not to lead to a crash - the object needs to be in a "valid but unspecified state". If there's no sentinel value, you can leave the object in pretty much any state at all, and it's on the caller to not do anything silly before resetting it. There's also https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-after-move.html https://clang.llvm.org/extra/clang-tidy/checks/bugprone-use-..., which will detect any such silliness.
- roca 7y agoHerb Sutter's article shows why this is actually a big problem in practice. He has an example of a class containing a guaranteed-non-null owning pointer. Leaving an object of this class in a "valid but unspecified state" means when you move out of it, you'll have to replace the internal pointer with a pointer to some new allocation, the very thing move semantics is supposed to help avoid. He suggests that such a class should simply not be moveable. So the fact that moves are not destructive is indeed very limiting.
- AnimalMuppet 7y agoNot quite. Let's suppose that I've got a pointer to a rather large array, and that array can't be null. When I move, the moved-from then has to allocate a new array, to satisfy the can't-be-null constraint. So I have the overhead of the allocation. But I don't have the overhead of copying all the bytes of the array.
- evanpw 7y agoThat workaround is also problematic, because move constructors are supposed to be noexcept, but 'new' can throw std::bad_alloc. So you either have to lie to the compiler and tell it that your move constructor is noexcept (maybe reasonable in this case since a bad allocation will then call std::terminate), or else you have bad consequences like std::vector silently copying your object instead of moving it.
- roca 7y agoSure, but that's almost as bad.
- unlinked_dll 7y ago>So I have the overhead of the allocation. But I don't have the overhead of copying all the bytes of the array. The overhead of allocation is a magnitude greater evil than copying the bytes of the data structure the vast majority of the time. Most data is small and short lived. As well, copying bytes in memory doesn't have side effects and is far more deterministic than allocation. The fact the spec says non trivially-copyable data must be in some "valid but unspecified" state means that move semantics in C++ are borderline useless, except in contrived cases like the one you present.
- AnimalMuppet 7y agoNot quite. It's the "pointer cannot be null" requirement that makes it useless here. If you allow "pointer can be null for a moved-out-of object", then they're much more useful. I'd say that it's the "pointer must not be null, even for a moved-out-of object" that's the "contrived" part.
- deleted 7y ago[deleted]
- Jaxan 7y agoI just started to look into Rust. And while reading the book, I kept thinking: “this is much easier than c++”, exactly for the reasons you mention. In C++ there is quite some things you need to keep in mind while programming (like whether something is moved from), rust takes some of that away.
- ot 7y ago> I often end up wrapping class members in std::optional to arrange for one That's not going to help you unless you explicitly unset the optional in your move constructor. std::optional's move constructor doesn't unset the argument (see #3 in https://en.cppreference.com/w/cpp/utility/optional/optional https://en.cppreference.com/w/cpp/utility/optional/optional). But if you have to implement your own constructors to emplace/unset the optionals, you can just have a sentinel state for your whole object.
- danidiaz 7y agoI also get moves confused with the return-value optimization, though my understanding is that RVO appeared earlier.
- blux 7y agoThe non-destructiveness of move semantics in C++ is very annoying. It causes for example `std::unique_ptr` to not be a zero-cost abstraction in all cases. See https://www.youtube.com/watch?v=rHIkrotSwcc https://www.youtube.com/watch?v=rHIkrotSwcc for a nice expose on this.
- quietbritishjim 7y agoLet me add to that list: - Calling std::move on a const object, or an object without a mine constructor, is not a compilation error or even usually a warning, and it will just silently perform a copy instead (assuming that the result of std::move is used to initialise another object). - Oops, I had to add that disclaimer in brackets, because std::move(foo) is valid code that has no effect if not passed to something else. As much as I understand why, chalk that up as another confusing aspect of mine semantics in C++.
- afranchuk 7y agoWith regard to the rvalue vs perfect forwarding references comment: there's actually no special syntactic difference between the two. The reason forwarding works is because in template <typename T> void do_thing(T&&) T will (with an rvalue) match the actual type, whereas with an lvalue it will match a reference to the type (i.e., you're moving a reference). This is what allows something like std::forward to be in the standard library and implemented in the language itself (without accessing any compiler builtins). That's not to say what's going on there is obvious nor really easy to reason about. It's more a hack that happens to work, like much of the templating ecosystem.