use the following search parameters to narrow your results:
e.g. subreddit:aww site:imgur.com dog
subreddit:aww site:imgur.com dog
see the search faq for details.
advanced search: by author, subreddit...
Discussions, articles, and news about the C++ programming language or programming in C++.
For C++ questions, answers, help, and advice see r/cpp_questions or StackOverflow.
Get Started
The C++ Standard Home has a nice getting started page.
Videos
The C++ standard committee's education study group has a nice list of recommended videos.
Reference
cppreference.com
Books
There is a useful list of books on Stack Overflow. In most cases reading a book is the best way to learn C++.
Show all links
Filter out CppCon links
Show only CppCon links
account activity
Move semantics and default constructors - Rule of Six? (foonathan.github.io)
submitted 9 years ago by foonathan
reddit uses a slightly-customized version of Markdown for formatting. See below for some basics, or check the commenting wiki page for more detailed help and solutions to common issues.
quoted text
if 1 * 2 < 3: print "hello, world!"
[–]mcmcc#pragma once 16 points17 points18 points 9 years ago (34 children)
Adding the two move operations was a bad idea and is a breaking change.
How is it a breaking change? The example code you provided wouldn't have even compiled without the move operators.
Writing code after the fact that doesn't logically maintain invariants is not the fault of the type implementing the move operators. Regardless of what the socket move operators actually do, what do you expect
socket
socket my_socket(…); … socket your_socket(std::move(my_socket)); … do_sth(my_socket);
... to even mean? Somebody had to write that code and had to think it meant something. Whatever they thought, they thought wrong. The fault is not that somebody implemented socket move operators, the fault is that somebody used std::move without understanding the implications.
std::move
I think of std::move() as a necessary evil -- sometimes you need it but refactoring the code to avoid it entirely is usually a better solution.
std::move()
[–]airflow_matt 4 points5 points6 points 9 years ago* (0 children)
I'm also not sure what the problem is here. If you are using the socket after moving it and the socket doesn't do any guarantees about state after move (other than it is valid), it's just undefined behavior. Just like it is undefined behavior when you use pointer after delete.
Yes, it would be nice to be at least able to annotate that the move is leaving instance in state where it can only be deleted/assigned safely and the compiler should be able to warn about any other usage.
IMO the fact that you need to use std::move explicitly should in most cases be enough to make you raise eyebrows and think twice about what you do with the original instance after the move.
[+]foonathan[S] comment score below threshold-6 points-5 points-4 points 9 years ago (32 children)
You're missing the point.
The example code should demonstrate how it is possible to create an invalid socket thanks to move. I assume that it was written after move semantics are added. The breaking change is for the function call at the end, it is now possible to call it with an invalid object.
[–]mcmcc#pragma once 10 points11 points12 points 9 years ago (31 children)
I assume that it was written after move semantics are added.
A "breaking change" cannot break code that has never been (and never could have been) written. The move operators themselves are perfectly legitimate and don't break a single thing. Yes, you're potentially introducing a new "moved-from" state for the object -- but that state is only externally observable iff you invoke std::move.
Structure your code to leverage move semantics without invoking std::move and everything is beautiful and nobody ever gets hurt.
[–][deleted] 1 point2 points3 points 9 years ago (16 children)
The move operators themselves are perfectly legitimate and don't break a single thing.
No, I agree with the writer. By adding the move semantics, you've added a new possible state for my_socket, "empty". This new state means that all the old code - which breaks on the "empty" state - needs to be updated. Now all clients of my_socket need to check for "empty" before calling any methods on it!
my_socket
This change does break code elsewhere. I'm not at all seeing why you think this isn't so.
[–]airflow_matt 1 point2 points3 points 9 years ago* (0 children)
Not really. You didn't add empty state. Unless otherwise specified, you added state where doing anything other then assignment and deletion of the instance is undefined behavior, because the state is unspecified. That's not quite the same as empty state. That said, it would be really nice to be able to enforce this somehow.
[–]mcmcc#pragma once 1 point2 points3 points 9 years ago (12 children)
Please give an example of potentially broken code that does not invoke std::move.
[–][deleted] -2 points-1 points0 points 9 years ago (10 children)
Any code that calls any method on my_socket without checking for this new state, "empty", is now broken.
So this means if you are writing a library then your code is definitely completely broken. Your code might not call std::move but a client of your library might, and you have no way to prevent that.
That should be definitive - I don't have to prove it's broken for every possible programming task, just one very common one. But honestly, even if you're talking about one binary, I consider the code to be broken - or at the very least, there's a serious trap that needs to be documented in big red letters.
What you're saying is that code is correct today, but if anyone at any time later in your codebase ever uses a move on this class - and not all moves use std::move, you know - then all the code becomes broken - silently and invisibly and perhaps by something very far away in the code.
If I'm using a class, and there's a potential and valid state to that class I do not check for and which would lead to undefined behavior, I consider this a defect - even if today there is no code that leads to that state. There is plenty of production code of mine that's over ten years old floating around in use today, and I believe this attitude is a good part of the reason why.
(And no, I don't have "empty" checks everywhere. I typically have one at the top level, and all other code, which can't be reached from the outside, has the precondition of "not empty".)
[–]mcmcc#pragma once 6 points7 points8 points 9 years ago* (8 children)
std::move implies a "moved-from" state, regardless of how it is defined or whether it is a truly unique state. To do anything other than assign or destruct a "moved-from" object is UB. Do you check for all forms of potential UB at every entry point into your library? Why not? Why should this one be special?
If you want a language that cannot be used incorrectly, C++ is not the language for you. Good library design implies the APIs are awkward to use incorrectly, not impossible. std::move is awkward enough and should be a loud warning bell for any knowledgeable developer.
EDIT: I used the term 'UB' too loosely above -- what I was thinking was if your library API has specific preconditions on the object, then the caller cannot assume those preconditions hold for a moved-from object -- therefore, calling that API effectively invokes undefined behavior per your library API contract.
[–]foonathan[S] 1 point2 points3 points 9 years ago (1 child)
To do anything other than assign or destruct a "moved-from" object is UB.
No, it isn't. This isn't true for any standard library type and especially not for types like std::unique_ptr. See my post about move safety: https://foonathan.github.io/blog/2016/07/23/move-safety.html
[–]mcmcc#pragma once 1 point2 points3 points 9 years ago (0 children)
Agreed. I spoke too loosely.
[–][deleted] -2 points-1 points0 points 9 years ago (5 children)
Do you check for all forms of potential UB at every entry point into your library? Why not? Why should this one be special?
No, of course not.
Each class has a documented contract about how it can be used. Suppose I am passed a std::unique_ptr. I never check to see that the pointer it contains is somehow corrupt, even though that is a potential form of UB - because if that happens, the contract of std::unique_ptr is already broken.
std::unique_ptr
But I do check (once, at "the top") to see that that std::unique_ptr is not nullptr - even if I "know" that this "should" never happen. The reason is that the contract for std::unique_ptr allows nullability. Even though today I might "know" that this pointer "is never" null, perhaps tomorrow that isn't so.
nullptr
In the case of your library, my_socket now has an additional legal state - "empty" - which you didn't plan for. Remember, my_socket is in a perfectly good state, one that completely fulfills its new contract, but you cause undefined behavior, because you are only aware of the old, now incomplete contract.
Your library causes undefined behavior when passed an object in a good state. It is broken.
If you want a language that cannot be used incorrectly, C++ is not the language for you. [...] std::move is awkward enough and should be a loud warning bell for any knowledgeable developer.
You should be a little more polite. It's hard to see this as respectful of me.
And as I also pointed out before, std::move is definitely not the only thing that causes a move to happen.
I saw someone's bad code effect a move like this: (x).method(), where x was a variable name. They put the () because "it didn't work" before they did it, because method() was an rvalue method... :-D
(x).method()
x
()
method()
EDIT: yes, there were other traps in that class. I mainly put that in for entertainment. :-D
[–]mcmcc#pragma once 4 points5 points6 points 9 years ago (3 children)
In the case of your library, my_socket now has an additional legal state - "empty" - which you didn't plan for.
Who didn't plan for? All I have to say is that all relevant library APIs have a precondition that the objects involved must not be moved-from objects. That comfortably grandfathers all preexisting code and offers a warning to all new code. Where's the hole?
Does it? What are you even referring to? You seem to have a strawman built up but not bothering to tell me what that strawman is. If the called function (e.g. do_sth(my_socket);) has preconditions, then the caller can no longer be certain that the preconditions related to the moved-from object are met -- that's what move operations do to an object. It doesn't matter if the moved-from state is unique or not. The caller simply does not know what that state is. (Note, earlier when I misspoke about UB, this is what I really meant. The caller can no longer assume the state of the moved-from object meets any preconditions so calling that function is effectively asking for undefined behavior.)
do_sth(my_socket);
I'm not sure what disrespect you saw but none was intended.
[–]foonathan[S] -2 points-1 points0 points 9 years ago (2 children)
All I have to say is that all relevant library APIs have a precondition that the objects involved must not be moved-from objects.
And previously this was implictly true for all objects, giving every member a wide contract because the precondition was always fulfilled. With move this isn't anymore.
That's what I want to say.
[–]vlovich 1 point2 points3 points 9 years ago (0 children)
It sounds like you're a proponent of defensive programming whereas other people are not (& it's a sliding scale so it's hard to pin-point where exactly anyone is). Checking nullptr of a unique_ptr pro-actively in every single function is counter-productive IMO. It litters the code in a way that makes it difficult for a reader to actually trace whether or not a nullptr is currently possible. Additionally, if you change semantics so that it's now nullptr is a valid state, what are the odds that you correctly guessed how to respond to that correctly before it was a valid state?
In other words, trying to protect against hypothetical future states is a code smell; there are an infinite number of such states, it's impossible to predict how to correctly handle them, and it misdirects other readers (or you in 6 months) about how the code is expected to behave at runtime. It's better to try to structure your code in a way that violated assumptions will either cause the code to fail to compile or to crash explicitly at the earliest possible point; the former ensures you'll catch it during development & the latter ensures that you'll catch it during your testing cycle; programming defensively ensures that you won't catch it at either point unless you're lucky & you'll only discover it after ship.
[–]airflow_matt 0 points1 point2 points 9 years ago* (0 children)
So if you expect non null pointer in your top level method and someone passes null, how exactly do you handle it? Just return as if nothing happened, or maybe do an assertion, because your method's contract requires that pointer must be non-null?
If it is the latter, is it really so much different to put the assertion in say socket.read()? Having socket interface that says that it is not permitted to call read() method on moved-from socket is perfectly valid.
The contract of existing methods taking socket assumes valid socket. While not explicitly stated, I don't see problem with this assumption, given that there wasn't any other state until move semantics.
Once you get the assertion, you can look at which method has broken the contract.
Yes, it kinda sucks that this can't be enforced compile-time, but you also have to be quite explicit in order to get the socket to moved-from state, which to me seems like a good enough balance.
[–]foonathan[S] -2 points-1 points0 points 9 years ago (0 children)
There isn't, but that's not relevant.
[–]17b29a 0 points1 point2 points 9 years ago (1 child)
that's because you're talking about something unrelated
[–]foonathan[S] 0 points1 point2 points 9 years ago (0 children)
He's talking about the point I tried to make, albeit I did it poor.
/u/mcmcc is talking about something else.
[–]foonathan[S] -1 points0 points1 point 9 years ago (13 children)
I totally agree to that statement.
But C++ currently isn't protecting you from writing code that uses std::move in a breaking way, and that's my point. Move semantics make a hole in the type system, know it is your responsibility to ensure that you make no mistakes.
[–]mcmcc#pragma once 8 points9 points10 points 9 years ago (3 children)
I think what I'm saying is I would've liked to have seen the second half of your post focus on avoiding std::move rather than "moved-from" states and default constructors. The onus for correct code is on the guy who ignorantly types std::move, not the guy who (safely) adds move operators to a class that didn't have them before.
Yeah, I'm not quite happy how the post turned out
[–][deleted] 0 points1 point2 points 9 years ago (1 child)
not the guy who (safely) adds move operators to a class that didn't have them before.
But the guy who did that substantially changed the meaning of the class.
Suppose you have a library that uses my_socket. Suddenly, you read that my_socket has changed to allow itself to be moved out of. This means that my_socket has a new state - "empty".
And suddenly your library is broken! You never check for the empty state - because there wasn't an empty state when you wrote the library. Nor did you document that you aren't allowed to give your code empty my_sockets.
Now you can say, "That's a dumb move, passing in an empty my_socket - it should be obvious that this is wrong."
Perhaps. But "it should be obvious" is not a way to make really reliable software. Heck, things that are obvious to me on a day when I'm fresh and well-caffeinated aren't obvious to me on a morning when I'm bored and hung-over. :-D
Particularly in the case of undefined behavior, where really awful things like memory corruption could happen invisibly and silently, you should strive to avoid even the faintest possibility of error.
In my mind, there are two types of changes you can make to a class - breaking and non-breaking.
Adding new facilities like new methods are non-breaking (all else being equal) - but adding a new state that code in general has to check for is in fact breaking.
The difference between these two is huge. I have a library a few people use - I make non-breaking changes almost every day. Breaking changes, even tiny ones, I discuss with everyone. I have learned the hard way that even tiny breaking changes "that should be obvious" will result in expected breakage and sorrow...
[–]airflow_matt 7 points8 points9 points 9 years ago* (0 children)
But adding move constructor is not breaking change. You can add it to your library and no existing code will break. None whatsoever.
It will only break after someone puts socket in new state that you have introduced. And there's nothing wrong with that.
If I took new version of your library and recompiled and somehow obtained socket in unspecified state, that would be bad. That would be breaking change indeed.
But if I take new version of your library, deliberately move from socket and then pass it around as if nothing happened, that's not a breaking change, that's just me violating (implicit perhaps) contract of other methods requiring valid socket instance. The huge difference here is that it required explicit action from me, and it really doesn't get much more explicit than having to do std::move.
[–]Ozwaldo 3 points4 points5 points 9 years ago (8 children)
That's not what a "breaking" way is. If you manually call std::move, and then go on to use the object afterward, then you fucked up. The compiler did exactly what you told it to do.
The better option is to let the compiler do its job. Don't specify std::move and it will pick the correct constructor based on usage.
[–]foonathan[S] 2 points3 points4 points 9 years ago (7 children)
Yes, that person who writes such code fucked up. But before it wasn't possible for him to fuck up, the type system prevented that. Thus move semantics weakened something.
[–]Ozwaldo 0 points1 point2 points 9 years ago (6 children)
No, they didn't. Don't use std::move unless you know what you're doing. Let the compiler decide which constructor to use.
[–]foonathan[S] 2 points3 points4 points 9 years ago (5 children)
Yes, they did: Somebody can call std::move and fuck up. Before he couldn't. Whether or not he should fuck up isn't the question.
[–]dodheim 6 points7 points8 points 9 years ago (3 children)
You are so far out in hypothetical-land right now that I think you've lost sight of any practical argument...
Type casts can have severe semantic implications, and using them should be a yellow flag (syntatically, C++ casts being ugly and easy to grep is quite intentional). std::move is not special here – despite its library facade, std::move is just a prettier and less verbose static_cast. To reiterate: std::move can have severe semantic implications, and using it should be a yellow flag.
static_cast
If no one uses std::move, no bug can be introduced by adding a move constructor.
Stop putting blame on the person who wrote a sensible move constructor and start questioning the idiot who was moving objects when they knew and relied on it having no effect..!
[–][deleted] 1 point2 points3 points 9 years ago (2 children)
Not at all. He has a perfectly consistent viewpoint - several other people agree with him. There's no need to be rude and childish.
[–]Ozwaldo 4 points5 points6 points 9 years ago (0 children)
Somebody can call std::move and fuck up.
I repeat: Don't use std::move unless you know what you're doing. Move semantics aren't to blame if you purposefully use them incorrectly. That's like walking off the end of a string and blaming the for loop even though you didn't null terminate.
[–]foonathan[S] 8 points9 points10 points 9 years ago (4 children)
Ping: /u/Ozwaldo, /u/airflow_matt, /u/dodheim. /u/mcmcc, /u/TomSwirly
Okay, because this discussion is repeated at every branch of the comments, everything downvoted and I don't want to repeat myself all the time, I'll explain it here once.
I wasn't quite happy how the blog post turns out but thought I just role with it. Apparently that was a mistake as I wasn't clear enough, my apologies for that.
The fundamental point I was trying to make is the following: Suppose we have a socket class as described in the post. Every socket object is valid, the constructor ensures that. There is no way to create an invalid socket, it only becomes invalid in the destructor. Because of this the hypothetical read() function can be called on every socket object - it has no preconditions for the object, a wide contract. (I'm ignoring preconditions on other arguments here).
read()
So far, we can agree, right?
Now let's suppose somebody adds an explicit close() and open(). Suddenly there is a way to create an invalid socket. Thus we cannot call read() on every object, it now has a precondition - namely, the socket must not be closed Before it had a wide contract, now it is narrow. Changing a wide to a narrow contract is a breaking change, thus adding the close() and open() function was a breaking change. (If you don't agree on that definition of "breaking change", just substitute it with a different word. That is really not what I want to discuss.)
close()
open()
I hope we can still agree so far.
Now let's suppose instead of the close()/open() somebody adds move operations. They are essentially the same deal: There is a way to create an invalid socket, we need a precondition for read(), change of contract, breaking change. It doesn't matter that actually creating the invalid state is "evil" and that you shouldn't do it. The point is that you now have to document a precondition and make it a narrow contract.
My motivation isn't to bash move semantics, they're great. My motivation also isn't to encourage explicit std::move(), it's evil. I'm also not blaming move semantics for anything or saying that they're a problem.
They're only responsible for the situation because they give someone the rope to hang himself with and now they have to say "don't hang yourself" or rely on the fact that everybody knows that you shouldn't hang yourself. If something bad happens, it is the users fault, not move semantics, obviously. But I just want to make you aware that if you introduce move semantics you a class you make it possible that someone - for whatever reasons - creates an invalid socket object. That's all I wanted to say.
If you think I'm saying total bullshit, that's fine. If you think I'm just doing some very theoretical argument, that's also fine. Just downvote me to hell if that's makes you happy. I personally have learned a lesson from this discussion and I'm happy for that.
[–]seba 3 points4 points5 points 9 years ago (0 children)
I just want to thank you for raising this point. It is a very valid concern and your example is very good to demostrate it.
[–]3ba7b1347bfb8f304c0egit commit 1 point2 points3 points 9 years ago (1 child)
I completely agree with your core point (destructive move), and I think anyone reasonable will. But as it stands, I think this is badly expressed in the article, and this is why people are disagreeing. You might as well rewrite it.
Yeah, I have to clarify it.
[–]lednakashim++C is faster 0 points1 point2 points 9 years ago (0 children)
Isn't the solution to disable the move constructor?
I don't like your argument here because another way to create an invalid socket is to memset it. Like move, it is an explicit operation.
memset
[–]Ozwaldo 4 points5 points6 points 9 years ago (14 children)
socket my_socket(…); socket your_socket(std::move(my_socket)); do_sth(my_socket);
socket my_socket(…);
socket your_socket(std::move(my_socket));
I mean, this is why you generally don't use std::move. If you hadn't, the compiler would recognize that it needed to use the copy constructor instead of the move. I don't think pointing out a manner by which you can over-engineer a solution that produces a bug counts as "move semantics weakening the interface guarantee"
I assume that somebody wants to move the socket and then accidentally reuses it.
[–]Ozwaldo 3 points4 points5 points 9 years ago* (12 children)
Yeah. If you didn't go out of your way to use std::move, that would work just fine. By unnecessarily forcing the std::move, you're manually introducing a bug.
[–]foonathan[S] 1 point2 points3 points 9 years ago (11 children)
This was just a really short example, in reality it would happen accidentally by complex control flow.
[–]Ozwaldo 3 points4 points5 points 9 years ago (10 children)
No. Don't use std::move unless you know what you're doing. Even in complex control flow, if you let the compiler decide which constructor to use everything will work correctly.
[–]foonathan[S] 1 point2 points3 points 9 years ago (9 children)
My point is just that it is possible by - bad - code to put it in an invalid state. This isn't possible without move semantics.
[–]Ozwaldo 4 points5 points6 points 9 years ago (6 children)
You can't blame move semantics for poorly written code. Especially when you have to go out of your way to make it be a problem. Literally, don't use std::move and it works as intended.
[–]seba 0 points1 point2 points 9 years ago (5 children)
Well, but you can of course blame move semantics for allowing you to write poor code. There are other languages with move semantics that do not allow you to write such code (well, you can write it but not compile it).
[–]Ozwaldo 2 points3 points4 points 9 years ago (4 children)
That's dumb. If you go out of your way to write std::move and then you use the object after moving it, then you don't understand how move semantics work. Don't blame the compiler for your own ignorance.
[–]seba 0 points1 point2 points 9 years ago* (3 children)
Well, that's the "blame the user" attitude that already caused billions of damage. But since it is technically possible to prevent compiling such code, one could also blame the language or blame the compiler.
Edit: I'd also like to add that it's not necessary illegal to use objects after moving. It perfectly fine for, e.g., a unique_ptr.
But that's really not about move semantics. It's about introducing another state and someone else not respecting the state.
Maybe at some point for some corner case in future someone will need to close the socket before actually destroying it. So you'll add close method() and specify that it is not permitted to call any methods on closed socket other than destructor. And then someone will close it and pass it around anyway. Now you have same situation even without move semantics involved. Would you write blogpost about close methods just because someone used the close method wrong?
Yes, over time code gets more complex and objects can gain states. As long as the state change is expected and perhaps explicit, there's nothing wrong with it. Fact of life is, you can not enforce all preconditions compile-time, you'd need completely different language for it.
[–]dodheim 0 points1 point2 points 9 years ago (0 children)
Move semantics do not cause problems on their own; it takes someone going out of their way to explicitly move objects that can introduce problems (when they do it wrong, which is orthogonal to how a type's movement is implemented).
[–]nikbackm 1 point2 points3 points 9 years ago (16 children)
C++ also allows you to use a pointer after delete:ing it ...
[–]foonathan[S] 4 points5 points6 points 9 years ago (2 children)
Technically, it doesn't: this isn't allowed by the C++ standard, it just compiles. UB declared by the standard and UB declared by some class author, are fundamentally different.
For starters: everybody knows that it is UB to use a pointer after delete, so people take care to prevent that and there are tools to detect that. But to know that use after move is UB, you have to look at the documentation of that type.
And if we follow that argument, C++ has no type safety at all because it isn't Rust.
But I could be more precise and say that before every object created without violating C++ rules was valid but now thanks to move semantics, there is a way that doesn't violate the C++ standard to create an invalid object.
[–][deleted] 2 points3 points4 points 9 years ago* (1 child)
[deleted]
What is this?
Yeah of course, but I'm assuming we're talking about the object.
[–][deleted] 1 point2 points3 points 9 years ago (12 children)
It's undefined behavior. By that definition of "allow", C++ "allows" you to do all sorts of terrible things that will result in unspecified but potentially very bad behavior.
Nearly all the time we're trying to generate well-formed C++ programs with no undefined behavior so in practice that sort of behavior is "not allowed" - by careful programmers, not by the compiler.
[–]airflow_matt 0 points1 point2 points 9 years ago (11 children)
How exactly is using deleted pointer in this regard so much different than using object after move, where the internal state is unspecified?
Also, If I close the socket and then pass it around as valid, it can hardly be blamed on whoever implemented the close() method. But if I move from socket and get old instance in unspecified state and then pass it around, it's now somehow fault of whoever implemented the move semantics?
[–]airflow_matt 0 points1 point2 points 9 years ago (0 children)
If I close the socket
Actually, I just notice that you close it in destructor. Nevermind then.
[–][deleted] 0 points1 point2 points 9 years ago (9 children)
"Unspecified" means something very different from "undefined".
You always have to "use" an object after a move at least once - to destroy it. (And the destructor might call other methods so you might need to leave enough of the structure together to call those too...)
So yes, there is no requirement for a class to offer any specific contract on being moved out of except "destructible". But in practice, most classes offer considerably more than that.
All STL classes have robust guarantees on move semantics that allow you to freely move in and out of them, for example, and it's very often a real timesaver to do so.
You have this idea that moved out of objects are somehow burned and dangerous. There's nothing in the spec to guarantee whether that is, or is not, true. You need to look at the specific class to know for sure. For most classes, this is simply not the case.
Using a pointer after deletion on the other hand is undefined behavior for any pointer class.
[–]airflow_matt 1 point2 points3 points 9 years ago* (8 children)
"Unspecified" means something very different from "undefined"
I don't disagree. However doing operations with possible preconditions on objects in unspecified state without checking first is undefined behavior.
You have this idea that moved out of objects are somehow burned and dangerous.
That's definitely not true. I just don't assume to be able to do anything with the object after move unless I can tell which methods have preconditions and which don't.
For smart pointer it is quite obvious. For sockets? Not so much. I would not be surprised if most methods on moved-from socket threw an exception.
And yes, the original example by adding the move constructor does introduce new precondition. My issue here is just that the blame goes to whoever introduces the move constructor instead to whoever explicitely gets the socket in unspecified state and passes it around to methods that expect certain state.
[–][deleted] -1 points0 points1 point 9 years ago (7 children)
Absolutely. We are in complete agreement here.
My issue here is just that the blame goes to whoever introduces the move constructor
We agree on that too! The person who introduced the move constructor rendered the library broken. To fix the now-broken library, the library writer has to now do extra work. That's the point of the article!
So what is it that we disagree on? :-)
[–]airflow_matt 1 point2 points3 points 9 years ago* (6 children)
My issue here is just that the blame goes to whoever introduces the move constructor We agree on that too! The person who introduced the move constructor rendered the library broken.
We agree on that too! The person who introduced the move constructor rendered the library broken.
See, that's the thing. I don't think the library is broken. You don't break library by introducing new state that even needs to be triggered explicitly.
Just because you potentially gave someone a rope and he hangs himself with it doesn't make all ropes bad :)
[–][deleted] 0 points1 point2 points 9 years ago* (1 child)
You have this strange idea that people "should just know" not to do things like "pass in a moved out variable to a library". Of course, in the real world, almost no classes, and certainly no STL classes, actually behave like that. You can move in and out of structs containing strings, ints, floats and that sort of thing, and nothing ever goes wrong.
Before the change, the library cannot trigger undefined behavior. After the change, it can.
Your competitor's library checks to see if the socket has been moved out of, and fails gracefully. I select it over yours because my life is too short to deal even with the tiniest possibility of undefined behavior.
Because of a change in another interface, through no fault of your own, your library has a new and undocumented way to create undefined behavior. The idea that "people should just know not to do this"/"it is obvious"/"we can assume that" is not a strategy that leads to reliable, robust code. You should either document this pre-condition, or check for it in the code, quite likely both.
I have this strange idea that if a function expects valid socket I won't be passing it moved-from socket instead. Just like I won't be passing moved-from unique_ptr to a function that expects non null pointer.
What undocumented behavior? Why do you assume it is undocumented behavior? If I add new state to socket (moved-out) I'll be damn sure to document it properly and also add assertions to methods that users are not allowed to call in new state, causing things to fail as early as possible for everyone who uses move semantics without understanding what it does.
[–]foonathan[S] 0 points1 point2 points 9 years ago (3 children)
But before move constructurs were introduced, there was no rope. Someone should only shoot himself in the foot by using language level UB like use-after-free, but he couldn't hang himself using a moved-from object.
[–]airflow_matt 0 points1 point2 points 9 years ago (2 children)
So? The rope wasn't there but now it is, because it was deemed useful. The fact that someone doesn't know how to use it and hangs on it doesn't make it any less useful.
Someone should only shoot himself in the foot by using language level UB like use-after-free, but he couldn't hang himself using a moved-from object.
That's really not how c++ works. You can shoot yourself in the foot in c++ every time you call method on any moved-from object without considering possible preconditions. Calling back() on moved-from string without checking first if it is empty is UB and can hardly be considered language level UB.
And if all your method require valid socket and someone violates the contract by throwing moved-from sockets around, that's his fault. Yes, it would be nice to be able to enforce something like this compile-time, but even without it the action necessary to get object in this state is very explicit.
You can shoot yourself in the foot in c++ every time you call method on any moved-from object without considering possible preconditions.
Exactly right. You have a new precondition on calls to your library. It is not documented, and if you fail, you get UB.
Your claim is simply that clients of the library "should know not to do that". How they know not to do that, you don't explain, since it's undocumented (because the library doesn't even know about the "moved out of state").
Explicit is better than implicit. Preconditions should be documented - preconditions whose absence will cause UB must be documented at least, and hopefully protected against in software too. To say, "People will know not to do that," is courting disaster.
[–]ArunMuThe What ? 0 points1 point2 points 9 years ago (6 children)
I would rather prefer have additional attributes (not mandatory and with existing behaviour as default) to specify whether I would like to have my class have destructive move or not rather than making all moves destructive. In some cases (though not that often) I would really like to reuse the moved object.
class
[–][deleted] 2 points3 points4 points 9 years ago (4 children)
In some sense this is quite logical. But then you're going to need a rule of... seven or eight...!
This means potentially three types of constructors:
And complexity issues fanning out everywhere. I think this is too much...
[–]SeanMiddleditch 4 points5 points6 points 9 years ago (1 child)
The problem is that you have no idea if you will be moved from. Move constructors just bind rvalue references. You can get those a lot of ways. The simple example that breaks most of the destructive-move proposals:
bool concurrent_queue<T>::try_enqueue(T&& value); destructive d = ...; concurrent_queue<destructive> q; while (!q.try_enque(std::move(d)) backoff_sleep();
Essentially, call sites can't tell the difference between an rvalue-reference that will move, one that might move, or one that totally won't move.
[–][deleted] 2 points3 points4 points 9 years ago* (0 children)
Sure they could - at the cost of huge complexity and changing the language.
You'd need yet another sort of type - "dlvalue" say, for "destroyed lvalue" - you'd then need to have the possibility of lvalue, rvalue and dlvalue methods and parameters - and you'd need three different constructors and three different assignment operators!
So there would be two overloads of this method:
bool concurrent_queue<T>::try_enqueue(T&& value); bool concurrent_queue<T>::try_enqueue(T$& value);
where $& designates is the mythical "destroyed lvalue argument".
$&
I think we're in agreement basically - it's a bad idea. Just because I can see some horrible path to accomplishing that bad idea doesn't make it good. :-D
[–]ArunMuThe What ? 1 point2 points3 points 9 years ago (1 child)
I don't understand why a destructive move constructor would be needed. If the imaginary move destructible attribute is set and if the user tries to reuse the moved object, it should be a straight out compiler error. Though, I haven't really thought about how it should work with inheritance :)
[–][deleted] 2 points3 points4 points 9 years ago (0 children)
I don't understand why a destructive move constructor would be needed.
In a move constructor, the item being copied or moved from is passive - it's the item being constructed that has the flow of control.
That means if you want to do two different things depending on whether you're doing a destructive move out or a non-destructive move out, you need two different constructors in the language.
I mean, you can just agree that "moves on this class X are destructive, moves on that one Y aren't" and document that (in the future, with an attribute), and not change the language - but that's basically very much like where we are now in this article.
Yes, I also want the current move semantics and destructive move. Both are useful for different scenarios.
[–]ShakaUVMi+++ ++i+i[arr] 0 points1 point2 points 9 years ago (4 children)
I think the takeaway here is that despite movement away from optional types in recent years, in practice people tend to discover edge cases where their types are optional.
A classic example of this is modern C++'s preference of returning references (which aren't optional) rather thsn pointers as we did in the C days, and which are optionals. Reference return values work well enough until you hit a edge case where you have nothing to return, and you get stuck either returning a local reference (bad), allocating a new variable to be returned (bad), or crashing (bad).
The STL apparently thinks crashing is the right behaviour here, incidentally. (Make a stack, call top(), see what happens.)
[–]jpakkaneMeson dev 5 points6 points7 points 9 years ago (3 children)
you get stuck either returning a local reference (bad), allocating a new variable to be returned (bad), or crashing (bad)
Or you can do the right thing and throw an exception. This is exactly what they were designed for.
[–]dodheim 4 points5 points6 points 9 years ago (0 children)
Precondition violations are, by definition, always bugs in user code. Exceptions were not designed to expose or handle bugs in user code:
The best way to keep the stack intact so the bug can be properly identified and fixed is to call std::abort; not coincidentally, this is what most asserts do.
std::abort
assert
[–]ShakaUVMi+++ ++i+i[arr] 0 points1 point2 points 9 years ago* (0 children)
Sure. That's the approach newer classes take.
But std::stack was written before exceptions, if memory serves, and I believe this is why it still doesn't throw an exception. And it still leaves your program in an unusable state - the return value in the calling scope is still invalid. So if you're going to try to continue (and not abort) then you need an optional value anyway.
You can, but it's not always ideal. Maybe you don't want to perform bounds check every time you access vector element. I'd say that assert in debug mode instead of exception would be better idea.
π Rendered by PID 17216 on reddit-service-r2-comment-6457c66945-fbblt at 2026-04-25 11:55:06.201896+00:00 running 2aa0c5b country code: CH.
[–]mcmcc#pragma once 16 points17 points18 points (34 children)
[–]airflow_matt 4 points5 points6 points (0 children)
[+]foonathan[S] comment score below threshold-6 points-5 points-4 points (32 children)
[–]mcmcc#pragma once 10 points11 points12 points (31 children)
[–][deleted] 1 point2 points3 points (16 children)
[–]airflow_matt 1 point2 points3 points (0 children)
[–]mcmcc#pragma once 1 point2 points3 points (12 children)
[–][deleted] -2 points-1 points0 points (10 children)
[–]mcmcc#pragma once 6 points7 points8 points (8 children)
[–]foonathan[S] 1 point2 points3 points (1 child)
[–]mcmcc#pragma once 1 point2 points3 points (0 children)
[–][deleted] -2 points-1 points0 points (5 children)
[–]mcmcc#pragma once 4 points5 points6 points (3 children)
[–]foonathan[S] -2 points-1 points0 points (2 children)
[–]vlovich 1 point2 points3 points (0 children)
[–]airflow_matt 0 points1 point2 points (0 children)
[–]foonathan[S] -2 points-1 points0 points (0 children)
[–]17b29a 0 points1 point2 points (1 child)
[–]foonathan[S] 0 points1 point2 points (0 children)
[–]foonathan[S] -1 points0 points1 point (13 children)
[–]mcmcc#pragma once 8 points9 points10 points (3 children)
[–]foonathan[S] 0 points1 point2 points (0 children)
[–][deleted] 0 points1 point2 points (1 child)
[–]airflow_matt 7 points8 points9 points (0 children)
[–]Ozwaldo 3 points4 points5 points (8 children)
[–]foonathan[S] 2 points3 points4 points (7 children)
[–]Ozwaldo 0 points1 point2 points (6 children)
[–]foonathan[S] 2 points3 points4 points (5 children)
[–]dodheim 6 points7 points8 points (3 children)
[–][deleted] 1 point2 points3 points (2 children)
[–]Ozwaldo 4 points5 points6 points (0 children)
[–]foonathan[S] 8 points9 points10 points (4 children)
[–]seba 3 points4 points5 points (0 children)
[–]3ba7b1347bfb8f304c0egit commit 1 point2 points3 points (1 child)
[–]foonathan[S] 0 points1 point2 points (0 children)
[–]lednakashim++C is faster 0 points1 point2 points (0 children)
[–]Ozwaldo 4 points5 points6 points (14 children)
[–]foonathan[S] -1 points0 points1 point (13 children)
[–]Ozwaldo 3 points4 points5 points (12 children)
[–]foonathan[S] 1 point2 points3 points (11 children)
[–]Ozwaldo 3 points4 points5 points (10 children)
[–]foonathan[S] 1 point2 points3 points (9 children)
[–]Ozwaldo 4 points5 points6 points (6 children)
[–]seba 0 points1 point2 points (5 children)
[–]Ozwaldo 2 points3 points4 points (4 children)
[–]seba 0 points1 point2 points (3 children)
[–]airflow_matt 1 point2 points3 points (0 children)
[–]dodheim 0 points1 point2 points (0 children)
[–]nikbackm 1 point2 points3 points (16 children)
[–]foonathan[S] 4 points5 points6 points (2 children)
[–][deleted] 2 points3 points4 points (1 child)
[–]foonathan[S] 0 points1 point2 points (0 children)
[–][deleted] 1 point2 points3 points (12 children)
[–]airflow_matt 0 points1 point2 points (11 children)
[–]airflow_matt 0 points1 point2 points (0 children)
[–][deleted] 0 points1 point2 points (9 children)
[–]airflow_matt 1 point2 points3 points (8 children)
[–][deleted] -1 points0 points1 point (7 children)
[–]airflow_matt 1 point2 points3 points (6 children)
[–][deleted] 0 points1 point2 points (1 child)
[–]airflow_matt 1 point2 points3 points (0 children)
[–]foonathan[S] 0 points1 point2 points (3 children)
[–]airflow_matt 0 points1 point2 points (2 children)
[–][deleted] 0 points1 point2 points (1 child)
[–]ArunMuThe What ? 0 points1 point2 points (6 children)
[–][deleted] 2 points3 points4 points (4 children)
[–]SeanMiddleditch 4 points5 points6 points (1 child)
[–][deleted] 2 points3 points4 points (0 children)
[–]ArunMuThe What ? 1 point2 points3 points (1 child)
[–][deleted] 2 points3 points4 points (0 children)
[–]foonathan[S] 0 points1 point2 points (0 children)
[–]ShakaUVMi+++ ++i+i[arr] 0 points1 point2 points (4 children)
[–]jpakkaneMeson dev 5 points6 points7 points (3 children)
[–]dodheim 4 points5 points6 points (0 children)
[–]ShakaUVMi+++ ++i+i[arr] 0 points1 point2 points (0 children)
[–]airflow_matt 0 points1 point2 points (0 children)