This is a good post. The C++ question of what remains in the “moved from” object is a big, nasty can of worms. Sometimes it’s well-defined, like e.g. std::unique_ptr are nulled when they are moved from (at the cost of performance). Other classes are less clear on the specifics, so you have to consider it on a case-by-case basis. Ideally, of course, you never use “moved from” objects, and that is how Rust does it, enforced by the compiler. This is faster and safer. In C++, the entire thing makes my head spin, so I usually just never use “moved from” objects again, unless I know the guaranteed behaviour off the top of my head.
It’s a difficult problem in general once you start to have structure fields. The example of the string in Rust is fine for a local variable, but presumably this means that you can’t move a string out of a field in an object in Rust, because doing so would require the field to become invalid. C++ works around this by effectively making it a sum type of string and invalid string (which, as the author points out, is deeply unsatisfying and error prone). Pony does this a lot better by effectively making = a swap operator, so you have syntax for cheaply replacing the string with an empty string or other canonical marker.
The other problem with C++ that the author hints at is that move isn’t really part of the type system. R-value references are part of the type system, move is a standard library idiom. This means that you can’t special case this in useful places. For example, it would be nice to allow destructive moves out of fields when implementing the destructive move of an object. A language with move in the type system could implement a consume and destructure operation that took a unique reference to an object and returned its fields in a single operation. This would let you safely move things out of all fields, as long as you could statically prove that the object was not aliased.
The example of the string in Rust is fine for a local variable, but presumably this means that you can’t move a string out of a field in an object in Rust, because doing so would require the field to become invalid. C++ works around this by effectively making it a sum type of string and invalid string (which, as the author points out, is deeply unsatisfying and error prone).
I’m not a rust expert by any means, but I think in rust you could achieve this pretty easily (and safely and explicitly) by having the struct field be an Option<T> and using Option::take().
The problem then is that it’s now a union type and you need an explicit check for validity on every single field access. This is safer than C++ where you need the check but the type system doesn’t enforce it, it’s just undefined behaviour if it’s omitted, but it’s incredibly clunky for the programmer.
Well sure, but it sounds like the alternative is “deeply unsatisfying and error prone”? If you want the Pony approach (by my understanding of your description; I’m not familiar with the language) of using some special sentinel value instead of an Option<T>, you could also use std::mem::replace(), or more concisely std::mem::take() if the type has a meaningful default value like "" for String.
It has a good observation that Types must have specific, clearly expressed ‘capabilities’ (or traits) that make them usable or non-usable for a specific algorithmic or technical operation.
In a way, C++ templates system is a mechanism to express if a given type is ‘fitting’ to a specific ‘operation’ or algorithm.
And if it is not – there was (often difficult to read), compilation error. But still there was a compilation error.
C++ allowed to expressed fitment of user types to algorithms, but did not allow to express the ‘fitment’ to the technical operations (like Move, parallel access, allocation model, exception model, etc).
And there is a impedance mismatch between built-in types and user-defined types, in the area of how these capabilities are expressed.
Now as the number and complexity of these technical operations grows, the inconsistency in expression of the capabilities through type system, is causing higher and higher cognitive load on developers (and I am sure compiler designers).
The problem with language now, is that it cannot evolve in a way where the complexity is constrained, but downward compatibility is maintained. It seems that we have to start choosing one over the other.
As our cognitive faculties cannot just keep up with the complexity caused by the desire to maintain the previously-written-code compatibility of the language.
When moving a variable means ending its existence, use-after-move API misuse becomes illegal, and this illegality is exploitable as an API design tool to statically enforce that functions are called in a correct order.
On the requirement of having a moved-from state (as a note to the destructor that the object is already destroyed):
Guard types, as an example, may not inherently have a state to represent this with, so adding a bool alive state to make it movable is a nonzero cost abstraction – a dilemma because you don’t know if the user needs it to be movable. As a symptom (there could be other reasons), C++ makes you choose between lock_guard and unique_lock.
Well, the lengthy treatise can be summarized that C++ is a language which has quirks and with which you can do potentially dangerous things. This is also true for any kitchen knife or car. The way move semantics has been solved in C++11 may not seem very elegant to many. What is often overlooked (including in the article) is that it was possible to do something similar already with previous C++ versions. Rust has advantages in terms of performance, if one compares e.g. with shared and weak pointers. However, Rust also has various disadvantages or requires restrictions in terms of architecture, and certain things are very difficult to do or not feasible. “There is no silver bullet”.
The way move semantics has been solved in C++11 may not seem very elegant to many.
It is elegant, given the constraint of backwards compatibility to C++98. It’s not elegant compared to what modern languages can do.
What is often overlooked (including in the article) is that it was possible to do something similar already with previous C++ versions.
This is not entirely true. Move semantics brought things to the table that could not be done in C++98. Especially std::unique_ptr is literally impossible to implement in C++98. Despite all its shortcomings, C++11 was a huge improvement.
It is elegant, given the constraint of backwards compatibility to C++98. It’s not elegant compared to what modern languages can do.
Maintaining backwards compatibility is a challenge indeed. I’m glad they’re doing it; it’s an essential feature to protect investments made. Regarding modern languages: C++20 is obviously “modern”, but certainly not an example of beauty.
Move semantics brought things to the table that could not be done in C++98
I can’t explain why this rumor is so persistent; someone has apparently invested a lot of money in marketing. It is well possible to implement move semantics and even unique pointers with pre-2011 C++, and there is even a Boost library for it.
This rumour is so persistent because it is true. The std::auto_ptr in C++98 has entirely different semantics. You cannot implement a std::unique_ptr in C++98. And the smart pointer collection that boost provided back then was also just a band-aid at best. Trust me, I develop C++ software in my spare time and professionally for many years and I maintain codebases that are locked into C++98. This is my daily life, I feel the pain every day.
C++20 is obviously “modern”, but certainly not an example of beauty.
I don’t think C++20 is modern. You still have all the legacy burden, unsafe memory management, insane integer promotion rules, strings without encoding, crazy locale library, the list goes on. C++20 is as modern as a cassette player with a Bluetooth 5.2 module soldered on.
It’s modern by definition; there are a lot of very important people in the commitee who are pushing for the latest craze with every release; even C++98 was officially called “modern C++”; the term is arbitrarily stretchable; personally I don’t care if it’s modern; most features we have in today’s languages were invented 40 to 60 years ago.
Just like the boost pointers, this is a band-aid at best. First of all, you now have to put macros everywhere into your code (also, have fun debugging and stepping through that). And then you still don’t have support for e.g. move-appending a std::vector element and things like that. As I said, many aspects of move semantics are impossible without actually using C++11. At best, you’re emulating C++11 behaviour with macros and elaborate C++98 incantations, while fighting tooth and nail against both the standard library and the language itself. Did you have good experiences with Boost.Move?
We had many libraries in the nineties, and there still are some today, where a lot of macros are used all over the code, some even with pre-processor or code generators. That’s absolutely no problem; on the contrary it makes the code clearer. The so-called new features of C++11 are primarily syntax sugar. Only in C++14 or 17 there started to appear a few things you couldn’t do already with C++98.
I remember well the time when trees were wasted on programming journals, and where there were regular puzzle columns on C++ that demonstrated its surprising capabilities. This was before Alexandrescu’s famous book and anticipated much of what we later had in Boost. Yes, Boost is an amazing and qualitatively impressive library in every respect.
This is a good post. The C++ question of what remains in the “moved from” object is a big, nasty can of worms. Sometimes it’s well-defined, like e.g.
std::unique_ptr
are nulled when they are moved from (at the cost of performance). Other classes are less clear on the specifics, so you have to consider it on a case-by-case basis. Ideally, of course, you never use “moved from” objects, and that is how Rust does it, enforced by the compiler. This is faster and safer. In C++, the entire thing makes my head spin, so I usually just never use “moved from” objects again, unless I know the guaranteed behaviour off the top of my head.It’s a difficult problem in general once you start to have structure fields. The example of the string in Rust is fine for a local variable, but presumably this means that you can’t move a string out of a field in an object in Rust, because doing so would require the field to become invalid. C++ works around this by effectively making it a sum type of string and invalid string (which, as the author points out, is deeply unsatisfying and error prone). Pony does this a lot better by effectively making = a swap operator, so you have syntax for cheaply replacing the string with an empty string or other canonical marker.
The other problem with C++ that the author hints at is that move isn’t really part of the type system. R-value references are part of the type system, move is a standard library idiom. This means that you can’t special case this in useful places. For example, it would be nice to allow destructive moves out of fields when implementing the destructive move of an object. A language with move in the type system could implement a consume and destructure operation that took a unique reference to an object and returned its fields in a single operation. This would let you safely move things out of all fields, as long as you could statically prove that the object was not aliased.
I’m not a rust expert by any means, but I think in rust you could achieve this pretty easily (and safely and explicitly) by having the struct field be an
Option<T>
and usingOption::take()
.The problem then is that it’s now a union type and you need an explicit check for validity on every single field access. This is safer than C++ where you need the check but the type system doesn’t enforce it, it’s just undefined behaviour if it’s omitted, but it’s incredibly clunky for the programmer.
Well sure, but it sounds like the alternative is “deeply unsatisfying and error prone”? If you want the Pony approach (by my understanding of your description; I’m not familiar with the language) of using some special sentinel value instead of an
Option<T>
, you could also usestd::mem::replace()
, or more conciselystd::mem::take()
if the type has a meaningful default value like""
for String.Yep a good post, bad title :-).
It has a good observation that Types must have specific, clearly expressed ‘capabilities’ (or traits) that make them usable or non-usable for a specific algorithmic or technical operation.
In a way, C++ templates system is a mechanism to express if a given type is ‘fitting’ to a specific ‘operation’ or algorithm. And if it is not – there was (often difficult to read), compilation error. But still there was a compilation error.
C++ allowed to expressed fitment of user types to algorithms, but did not allow to express the ‘fitment’ to the technical operations (like Move, parallel access, allocation model, exception model, etc).
And there is a impedance mismatch between built-in types and user-defined types, in the area of how these capabilities are expressed.
Now as the number and complexity of these technical operations grows, the inconsistency in expression of the capabilities through type system, is causing higher and higher cognitive load on developers (and I am sure compiler designers).
The problem with language now, is that it cannot evolve in a way where the complexity is constrained, but downward compatibility is maintained. It seems that we have to start choosing one over the other. As our cognitive faculties cannot just keep up with the complexity caused by the desire to maintain the previously-written-code compatibility of the language.
I felt obliged to reproduce the clickbaity title, but IMO the body of the article is very interesting and not antagonistic.
Typestate programming is also a thing you get with destructive move semantics:
When moving a variable means ending its existence, use-after-move API misuse becomes illegal, and this illegality is exploitable as an API design tool to statically enforce that functions are called in a correct order.
On the requirement of having a moved-from state (as a note to the destructor that the object is already destroyed):
Guard types, as an example, may not inherently have a state to represent this with, so adding a
bool alive
state to make it movable is a nonzero cost abstraction – a dilemma because you don’t know if the user needs it to be movable. As a symptom (there could be other reasons), C++ makes you choose between lock_guard and unique_lock.Well, the lengthy treatise can be summarized that C++ is a language which has quirks and with which you can do potentially dangerous things. This is also true for any kitchen knife or car. The way move semantics has been solved in C++11 may not seem very elegant to many. What is often overlooked (including in the article) is that it was possible to do something similar already with previous C++ versions. Rust has advantages in terms of performance, if one compares e.g. with shared and weak pointers. However, Rust also has various disadvantages or requires restrictions in terms of architecture, and certain things are very difficult to do or not feasible. “There is no silver bullet”.
It is elegant, given the constraint of backwards compatibility to C++98. It’s not elegant compared to what modern languages can do.
This is not entirely true. Move semantics brought things to the table that could not be done in C++98. Especially
std::unique_ptr
is literally impossible to implement in C++98. Despite all its shortcomings, C++11 was a huge improvement.Maintaining backwards compatibility is a challenge indeed. I’m glad they’re doing it; it’s an essential feature to protect investments made. Regarding modern languages: C++20 is obviously “modern”, but certainly not an example of beauty.
I can’t explain why this rumor is so persistent; someone has apparently invested a lot of money in marketing. It is well possible to implement move semantics and even unique pointers with pre-2011 C++, and there is even a Boost library for it.
This rumour is so persistent because it is true. The
std::auto_ptr
in C++98 has entirely different semantics. You cannot implement astd::unique_ptr
in C++98. And the smart pointer collection that boost provided back then was also just a band-aid at best. Trust me, I develop C++ software in my spare time and professionally for many years and I maintain codebases that are locked into C++98. This is my daily life, I feel the pain every day.I don’t think C++20 is modern. You still have all the legacy burden, unsafe memory management, insane integer promotion rules, strings without encoding, crazy locale library, the list goes on. C++20 is as modern as a cassette player with a Bluetooth 5.2 module soldered on.
No. I’m not talking about auto_ptr. Have e.g. a look at https://www.boost.org/doc/libs/1_63_0/doc/html/move.html. Every C++ programmer likely uses or at least knows Boost.
It’s modern by definition; there are a lot of very important people in the commitee who are pushing for the latest craze with every release; even C++98 was officially called “modern C++”; the term is arbitrarily stretchable; personally I don’t care if it’s modern; most features we have in today’s languages were invented 40 to 60 years ago.
Just like the boost pointers, this is a band-aid at best. First of all, you now have to put macros everywhere into your code (also, have fun debugging and stepping through that). And then you still don’t have support for e.g. move-appending a
std::vector
element and things like that. As I said, many aspects of move semantics are impossible without actually using C++11. At best, you’re emulating C++11 behaviour with macros and elaborate C++98 incantations, while fighting tooth and nail against both the standard library and the language itself. Did you have good experiences with Boost.Move?We had many libraries in the nineties, and there still are some today, where a lot of macros are used all over the code, some even with pre-processor or code generators. That’s absolutely no problem; on the contrary it makes the code clearer. The so-called new features of C++11 are primarily syntax sugar. Only in C++14 or 17 there started to appear a few things you couldn’t do already with C++98.
I remember well the time when trees were wasted on programming journals, and where there were regular puzzle columns on C++ that demonstrated its surprising capabilities. This was before Alexandrescu’s famous book and anticipated much of what we later had in Boost. Yes, Boost is an amazing and qualitatively impressive library in every respect.