Move, simply(herbsutter.com) |
Move, simply(herbsutter.com) |
No, they aren't simple and the fact that are still widely misunderstood is basically proof of that.
Maybe, just maybe, it has something to do with stuffing rvalue references, perfect forwarding and the whole universal-references-template-clusterfuck into one and the same syntax. [1]
The default compiler-generated move can leave behind a null sp member
Which is precisely why std::move should be used with caution, which, in turn, is precisely why many codebases are still avoiding it. This problem in particular could have been avoided by not auto-generating move constructors.
I really want to like this feature of C++, but I feel its design is just so horribly bad and confusing that I'd feel like a total d..k if I started to force it on a team of developers (of various levels of experience) if there's no crystal clear need for it (and let's be honest, C++ was already pretty successful in getting shit done before there was std::move and rvalue references).
[1] http://thbecker.net/articles/rvalue_references/section_01.ht...
How much better the world would be if software developers started making judgements based on empirical evidence instead of rhetoric and navel gazing...
To paraphrase the old bit:
If an idiot misuses your software in the morning, you have a user who's an idiot. If idiots misuse your software all day, you're the idiot.
The article disproves itself. Sure, move is simple, if you stick to that one very narrow use case (that still needs std::move() boilerplate around moves to signal intent).
The problem (that the article then goes to point out) is that there are so many non-simple and buggy ways move can (and does) get used.
So you don't just need to learn the simple use case (with added boilerplate), you also need to learn all the complicated buggy use cases so that you can avoid using them.
For actual "simple" moves, try Rust.
But I agree completely with your point about "move" semantics. Some language rules that seem clear to Sutter et al are insanely complicated to normal C++ developers.
Most of us could probably stay on top of C++'s rules if we spent many hours per week on that task. But few of us have the time or interest to do that.
It has now morphed into a language where today's optimal code looks horribly inefficient by yesterday's standards. Which adds another layer of uncertainty to what was already a pretty serious mess of a language.
Calling this simple is about as silly as it gets. Thanks, but no thanks, I have problems to solve.
g-fu ¹, a Lisp dialect in Go, is a marvel. Other languages you've been creating (gfoo, cfoo, lila) are tastefully done too.
So I'm in total agreement with you about the monstrosity that is modern C++. Nobody would have designed such a language from scratch.
It seems to be a common fate of popular and long-lived languages (or projects, companies even), that it grows into a complicated monster that would horrify its original creator.
The biggest issue with g-fu is that it currently expands macros on evaluation. What can I say, it was the first time I implemented quasi-quoting which twisted my brain in exotic ways. That's also why it's so close to Lisp, because it's the only decent macro system I have experience from.
g-foo is definitely a cleaner design, but more Forth than Lisp which may not be everyone's cup.
Agreed, and Bjarne said so himself; that there's a cleaner, more consistent language hidden deep inside C++. C compatibility has been a blessing and a curse. I think Stepanov is a better designer though; if it wasn't for the STL, I would have given up a long time ago.
Herb Sutter seems to be burying the lede here. My read on Herb Sutter's post is that the current typical methodology (as represented by the IndirectInt example), is buggy as per the current specification.
What we all want is "destructive move". A lot of us believe that C++ move is "destructive move", but it is not. C++ move is this slightly different, simpler move, that isn't in fact the destructive move that we all want.
---------
As such, Herb Sutter seems to be pushing for a "True Destructive Move" to be included into the C++ specification. Since std::move is CLOSE to the behavior of true-destructive move, we might as well use it if we're writing code today. But the C++ standard should be fixed to include the concept of a real destructive move.
-----
At least, that's my read on things. Anyone else have thoughts? In essence, "C++ Move is simple, perhaps too simple and it doesn't really get the job done".
As the specification is currently written, a "moved-from" C++ object is NOT in any special state, like it is in some other popular languages. Programmers seem to expect a special state however (see IndirectInt).
I'm a huge proponent that classes should be fully configured on construction and should stay that way until the end of their life-cycle. I really dislike classes that are only partially configured on construction, and then you have to call other methods to complete their initialization. Then every method in the class has to have guard code to check whether the object has been fully initialized. And if you really want to code defensively users of the class also have to handle the case where an instance of that class hasn't been fully initialized, either by checking before calling a method, or by catching exceptions . It complicates things for both the user and the implementator of the class, not to mention adding computational overhead on both sides of the API.
If you require objects to be usable after they are moved from then, every object with a move constructor will either need to do extra work during the move to keep itself in a valid (but different) state, or it will need to support being in a partially configured state. In some cases it will be inexpensive to create a valid state (say change an object to point to a preallocated singleton), but in others it would completely defeat the benefit of doing a move to begin with, so you are back to supporting partially configured objects.
In the end this seems like a lot of work to keep moved objects valid, when in practice the vast majority of moved objects are implicitly moved temporaries, that are impossible to used after they are moved. And for the cases where an object was explicitly moved, the misconception that you shouldn't use an object after moving it is already well ingrained enough that it rarely happens by accident.
I can see putting in sentinels and asserts to sanity check that objects aren't being used after move (and typically do), but I'd still prefer to treat use-after-move as the bug, than complicate the normal uses of the object to support an unnecessary corner-case.
Edit: Reworded a few sentences for clarity.
- 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.
To use a quote from one of my favorite movies: you keep using that word. it doesn't mean what you think it means!
Articles such as this one remind of how C++ is such a design-by-committee language. Watching it evolve is like watching a 100 chefs in a kitchen working on a single dish, where everyone wants the dish to taste the way they like it.
This comment is probably not directly relevant to the article, but I had to get it off my chest.
Is it not the case that one of the main reasons for the language giving us the ability to write an explicit no-argument constuctor is to address this sort of problem on construction: it allows us to put the object in an application- or library-defined state, thus establishing a convention that helps with correct use? And is it not a common idiom, when writing a move constructor for a class that has an explicit no-argument constructor, to leave the moved-from object in the same state as if it were newly constructed without arguments? (e.g. std::mutex, where that state is unlocked.) (Update: another common idiom is to swap source and destination.) These are the some of the conventions that make an explicit move robust.
Q: Does “but unspecified” mean the only safe operation on a moved-from object is to call its destructor?
A: No.
...
Q: What about objects that aren’t safe to be used normally after being moved from?
A: They are buggy...
The gist seems to be that newer standards simplify move semantics, so GCC introduced a warning when you write `return std::move(result);` because the manual move is redundant (and could actually slow down your code).
However, if you need your code to run on older compilers, you can get hard-errors by, for example, older versions of GCC not binding the move constructor on a return.
Also, because not all compilers have implemented the newer move semantics, you could get copies rather than moves even on newer compilers.
So while I'm sure "only call std::move in this one circumstance" will be great advice one day, in the real world at the moment the situation seems decidedly more complex.
[0]http://lists.llvm.org/pipermail/cfe-dev/2020-February/064662... (mid-thread)
class Object
{
int* data = new int[32];
void move_into( Object& object )
{
object.data = data;
data = nullptr;
}
~Object()
{
delete[] data;
}
}
Object a;
Object b;
a.move_into( b );
I think much of the confusion arises from wondering where is the allocation for int* data located? It's somewhere on the heap. What if the data member were int data[32] instead? What would the class look like? What would 'a' look like afterwards?But maybe with int data[32] as a data member this is harder to reason about.
This is a way to illustrate move semantics and confusion behind what happens to the 'moved from' object.
std::move() is there to allow you to explicitly say, "this is okay to move from, even though it can still be accessed after the move".
And yet programmers are still fucking around with move constructors, copy semantics, and host of other horseshit foisted upon us by people who think that saving a copy of a couple words of memory here and there should be absolutely foremost in programming. And then one of those designers has the gall to tell us we are all stupid for not understanding this and blowing our legs off every day.
C++ is such a waste of everyone's time. Don't even get me started on how much computational power its build system wastes.
sigh
Most certainly written in c++
> and AIs that can slaughter humans at almost any strategy game.
Guess in what language is trnsorflow written
> Bitcoin
Ditto
Totally beside the point!
Actually, FORTRAN mops the floor with C++ when it comes to bigtime numerical computations on supercomputers. And they're all running kernels written in good ole C.
Most of the FAANGs are running absolute shittons of Java, Javascript, PHP, and Python. Also running kernels written in C.
> Guess in what language is trnsorflow written
Derp, tensorflow has to interact with GPU drivers which are all C++ interfaces. Btw it generates shader code which is definitely not C++, but some weird vendor languages, which don't have any of this move constructor related garbage.
> Bitcoin
Ditto
Actually the vast majority of bitcoin's computational load is done by ASICs.
Congratulations, you missed the whole point. My point wasn't what languages those things are written in (your Red Herring, not mine), but the fact that we spend an absolute metric shitton of computation on other things, but still expect slow, dumb programmers to fret over niggling details, usually screwing it up in the process, when this task would be far better served by throwing some god damn computational resources at it--you know the ones we waste on O(N^2) compilation tasks and parsing header files over and over and over again....
sigH
Perhaps no other _popular_ alternatives but still there are alternatives.
> a "moved-from" C++ object is NOT in any special state, like it is in some other popular languages.
In practice it is; it's in a special state of "satisfies class invariants but otherwise undefined". From the developer's point of view this is very little different from the destructive-move "you can't touch this anymore" state.
No different than when they added move the first time in C++11 and everybody had to learn the difference between copy and move. No different than Rust programmers learning the difference between Rust's move and C++'s move.
Stuff changes over time. In this case, I think it is warranted. The C++11 move is useful in many cases, but not useful enough for how many programmers use unique_ptr<> stuff. Adding that little bit of extra state for the compiler to track might be helpful.
> In practice it is;
That's the mismatch between C++ Programmers and C++ Language. In effect, C++ Programmers want destructive moves and are currently coding their move statements AS IF they were destructive moves.
Herb Sutter is careful to choose the auto_ptr example. Because he's basically saying that unique_ptr is making the same mistake auto_ptr made years ago... the move semantics are subtly different and need to be changed and/or updated.
That's what rust does - rust isn't literally freeing/clearing memory when a struct is moved.
Initializing the ‘source’ int to zero seems the only reasonable option, but it would hurt performance.
Also, what about std::move<T> in general? If T has a destructor, should that call it on the original? Currently, deciding whether that (or doing its equivalent) is a good idea is up to the implementer of std::move<T>. I don’t see how the compiler could make that decision without sacrificing performance in some cases.
I think that, if you want the compiler to enforce destruction of moved-from values, you also should want the compiler to zero out (or equivalent) all destructed values.
To satisfy the programmer, there probably should be a std::dmove(x), which would be a compiler-enforced notation that would make use of x illegal after the use of std::dmove.
std::dmove(x) would be implemented as std::move, followed by a destructor, and then the compiler removes variable x from the scope. Pointers and references to that variable will have undefined behavior if they are used (compiler may find it reasonable to recycle x's memory location to another variable)
------
std::move<int> remains the same. There's no performance benefit, or even code-logic benefit, to zeroing out integers. For example, an integer may be a denominator, dividand, modulus, or multiplier. In these cases, "zeroing out" to "x = 1" makes more sense than "x=0".
Having a compiler enforced "zero" of "x=0" is arbitrary and doesn't help anybody. Its better to let the integers remain the same value they used to be.
Just copy Rust. Trivially copyable types aren't invalidated by moves, and "move" just aliases copy.
What is "partially configured" after all? IMHO this dichotomy is just a headache brought to you by yours truly, OOP (or rather, syntactically enforced OOP). Personally I don't want to spend the rest of my life pondering such philosophical questions. Or dealing with all the consequences, like constructor exceptions, forced dynamic allocation because static doesn't work without initialization, etc, pp.
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-..., which will detect any such silliness.
So the fact that moves are not destructive is indeed very limiting.
- 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++.
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).
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.
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.BTW, I have updated my original post to mention swapping source and destination, another common idiom for satisfying the requirement.
The standard example is
template<typename T>
void swap(T& x, T& y)
{
T z = std::move(x);
x = std::move(y);
y = std::move(z);
}
...
std::string a[1000];
...
std::swap(&a[2], &a[7])
If std::move copy-constructs and destroys, that last line would do three allocations and three frees (recoverable by a significantly advanced compiler, but who has that?). The way std::move is specced, it can do zero allocations and zero frees.So clearly the way std::move works now is great for that situation. But there's a 2nd situation, with Herb Sutter's IndirectInt, which calls for a different behavior.
Yes, it's not the first time the cognitive load of C++ has increased.
> No different than Rust programmers learning the difference between Rust's move and C++'s move.
Learning concepts in a new language doesn't increase cognitive load in the same way. In C++ you will have to decide which kind of move to use in any given situation, and deal with code using multiple kinds of moves. Learning Rust doesn't create such issues.
> Adding that little bit of extra state for the compiler to track might be helpful.
It'll be interesting to see how that goes. In Rust the compiler ensures safe code can't access a moved-out-of object, but that depends on Rust's strong aliasing guarantees. In C++ I guess accessing a moved-out-of object will have to be a new kind of undefined behavior, creating new classes of bugs and requiring new sanitizers and approximate static checkers to be implemented.
E.g. no more "it requires work to say what happens" UB cop outs - if something can be defined it should be.
In this case I'd say that a value cannot be used after it is destructively moved. There is no reason for that to be UB, when the language can just refuse to compile it.
I.e. to have the compiler prove absence of use-after-move requires the same sort of lifetime analysis it would need to prove absence of use-after-free, and that is simply not realistic for C++.
What Ollie said is true for local variables, and collections generally support some kind of "move element(s) out of the collection" API (e.g. Vec::pop(), T::into_iter()).
Neither is correct vs the other, rather they reflect different choice points in the language possibility space.
The C++ version would need the existing (unchangeable) move semantics.
What if you move a unique_ptr from a std::vector? You don't know which elements of the vector need to have their destructors run.
I think Rust unconditionally doesn't run the destructor of a moved-from Box, but uses drop flags for "maybe-moved-from" local variables, and doesn't allow maybe-moved-from Vec elements.
But that difference actually gets to your point here. The only difference between copy and move is that a move allows for the new data to overlap the previous. So if you think about what moving from a vector of unique ptr actually means, it must invalidate the vector or remove the element that was moved from the vector. If you want to get the element from the vector without those effects, you have to create a deep copy.
Basically moving without invalidating or mutating such that the moved-from alias can no longer be accessed is necessary.
In Rust, the only thing that has to happen is a transfer of ownership, which does not always mean moving bytes. This greatly simplifies the whole “rvalue reference parameter” thing, because ownership abstracts over wherever you might want to move to. So if your new owner wants it in part of its new stack frame, the compiler is free to just put it there before calling or simply call a destructor on the parent stack frame’s memory in the child. (I don’t know exactly what it likes to do, but it’s all fine.) New owners like Box need to move memory, and that’s cool too, and the compiler is free to reorder the steps to avoid initialising and moving when “placement new” is better. This is also something I believe the compiler is getting better at slowly. The compiler won’t let you safely write moves out of a vector without mutating or taking ownership of the vector, each with moving bytes — otherwise it has access to something it no longer owns.
The easiest example is `let a = A::new(); let b = a;`, which only needs stack space for one A, and for which line 2 is a noop. Its only effect is in when drop is called, if b is in a smaller scope. Does C++ do this too? I would hope so, but it looks like no.
Edit: turns out neither do it.
- https://rust.godbolt.org/z/xjA9v4 - https://godbolt.org/z/7Wh_AZ
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.
I'd say that it's the "pointer must not be null, even for a moved-out-of object" that's the "contrived" part.
But to address your point: I'm a muppet and didn't think about conditional move in a loop.
I had been thinking that you require that any conditional block has to end with a moved value int he same state - e.g.
if (a) {move(x)} else {}
The compiler would say both blocks must end with x being dead, so the else block would be required to call x.~X();
But yeah, that fails completely if you have
for (...i...) { if (i) move(x) }
because my clever rule means that x would have to die at the end of the first loop.
But the general problem I have with std::move currently is that there's no guaranteed death - the fact that post std::move a destructor will still run, or the object can just continue to be used in general means that your code has to be made defensive against already being logically dead - even if just the destructor.
> Use after free (through references) is a standard problem in C++ through anything. But you can say post-move a value is dead, and use of existing references would be UB (sigh, but that isn't new).
OK, arguably aliased-use-after-move could be treated as an existing form of UB. Sanitizers and static checkers would still need to be updated, however.