The copy and swap idiom in C++
sandordargo.com
sandordargo.com
But after some time you realise that nobody is smart enough to keep all of these idioms and arbitrary C++ rules in their head. The only reason I could do it was because I put so much time into preparing my courses. So all of your collegues will keep writing terrible C++ code. And you will be left frustrated that nobody is "doing C++ right".
I guess one could chill down and accept the chaos. I chose to leave C++ behind.
I recently had cause to start using C++ again and went over the cards after about a year break. It was depressing how few I could answer correctly.
C++ is great if you're great with it, but that takes a __lot__ of effort, and it can be a minefield otherwise.
The issue with C++ is that the 'proper' way to do something almost always requires an encyclopedic knowledge of the language; any other solution you come up with will likely be wrong or at the very least suboptimal for esoteric reasons. It's not that people aren't smart enough, it's that there's not enough time to really learn the language. I know I've sacrificed sleep and social life just doing deep dives into the language to really understand it. The reason I don't know C++20 yet is because I'm not willing to make those personal sacrifices anymore, and even if we could use C++20 where I work we don't have infinite time to learn it before being productive.
For those that are weathering the storm in C++, I commend you, but you’re going to need therapy. (joke but the effects of stress are real).
For C++ I settle down with c++17|c++20 and they're still complex, but not that bad though.
Go might be simpler but C++ can do whatever Go does and I tried to master both, it did not work, so I had to pick one and that one has to be c++ for my use cases.
&mut Arc<Mutex<Option<Box<MyStruct>>>>
It's a mutable reference to an Arc of a Mutex of an Option of a Box of MyStruct.
You presumably could build yourself one of these in C++ although since the standard mutex in C++ can't actually protect anything (you're supposed to just remember to lock it anywhere you need to, you know, because C++ is a language for people who never make mistakes) you would need to build that part yourself.
And if you did build that yourself in C++ the syntax for the type would look rather similar, although I guess C++ allows you to just say auto meaning, "I dunno, guess" for the return type in a function signature whereas Rust insists you must actually write return types and don't leave them for the compiler to figure out.
Inferring return types is convenient for the person writing the function, but adds cognitive overhead for people using the function and makes it easier to introduce incompatible changes without realising (the signature you wrote is unchanged but a different signature is now inferred)
I’m saying, to a person with less skills than I, it’s an almost insurmountable undertaking to learn Rust while Go at least seems somewhat familiar. They aren’t teaching these in universities I visit so new engineers might have a tough time grokking the syntax.
I prefer Go for two reasons. 1) It’s faster for me to get my idea across (this is my personal hurtle) and 2) Go’s concurrency model.
What I would like to try in Rust: rewriting my game engine. Building a hobby OS. Building a hobby programming language. These are areas where Go isn’t the best tool.
In Rust that can be made impossible.
Every new release of C++ tends to reduce, not increase, my stress. Many of these patterns, including this one, just largely stop being things entirely. Rule of zero and all that.
It can be messy, because backward compatibility, but a lot of newer code bases start time with idiomatic C++17. The standard library is the primary place you'll still run into old style C++.
The libraries we get to plug into our infrastructures show another reality.
Or in a better way, see the usual talks about the state of teaching C++, and the quixotic endevours to improve it.
I really wanted them to be worth it, and spent years trying to make it worth it. I guess in the long run it was worth the time spent, because it caused me to explore other possibilities that I might not otherwise have considered (in particular, Lisp and Scheme-like languages, which I doubt I would have otherwise given a second look -- they were just "weird languages that we spent a couple of weeks looking at in college - and oh, yeah what's up with all those freaking parentheses?").
Sometimes the point is just having it possible. Sometimes I need to get that last bit of performance out of some code.
> (in particular, Lisp and Scheme-like languages
If your performance requirements are more in the scripted range then C++ will seem like overkill. I often use python for quick and dirty tools, however every other time I end up rewriting things in C++ because something that should finish almost instantly takes half an hour.
I'm talking about complexity beyond using a small subset of C++ features, things like template metaprogramming (but not limited to that).
> If your performance requirements are more in the scripted range then C++ will seem like overkill. I often use python for quick and dirty tools, however every other time I end up rewriting things in C++ because something that should finish almost instantly takes half an hour.
I have a wide range of requirements depending on what I am working on. I can still drop down to a C or C++ level if I need to, but if I do use C++ it will only be a limited subset that minimizes complexity.
Also, you imply that Lisp and Scheme-like languages are limited to performance on par with Python or other so-called "scripting languages". There are languages in that vein that can perform very close to C's performance (Gambit Scheme being one such example). Often instead of rewriting in a lower-level language, you can use optimizations in the language to improve performance. Of course you can also just rewrite smaller portions of code in C or C++ and call that code from another language.
I'm not someone who just dabbled in C++ for a little while and gave up. I have decades of experience with it. I wanted to use it beyond "C plus a little extra", but I found that the further from a C-like subset I went the more incidental complexity I had to deal with, and in the end the benefits I was looking for just never materialized.
So there's just no way K&R C gets anything like the STL. Maybe you get stuff like a char* (these days you would write void * but that's not a thing yet) linked list? That does not sound like a better world.
And then you just abuse arrays as much as possible. For a small number of items, e.g. <100 on a modern laptop, that linear and cache-friendly search is going to beat std::map and std::unordered_map and friends.
- Use simple POD-like structs as much as possible.
- Don’t be afraid of creating simple functions. Not everything has to be a method.
- Only use virtual functions to create pure interfaces, don’t use them for anything more complex
- Don’t use constructors for things more complex than simply initializing fields. For that, use factory functions or separate Init() methods
- Make use of RAII as minimal as possible. Opt for batch construction/deletion instead of RAII-like “single” construction/deletion.
- Turn off exceptions, exit early on error or return an error code/error object if recovery needs to happen. (Related to why you shouldn’t use constructors)
- As a bonus, turn off RTTI. Don’t rely on dynamic_cast, you’re doing something wrong.
- Allocate arrays of objects instead of unique_ptrs/shared_ptrs. Use indices instead of pointers as object handles. When objects are frequently created/deleted and indices frequently get invalidated, use generational indices.
- Ownership should be centralized instead of distributed. A central store should contain arrays of various kinds of objects, and it should take care of most of the memory management (as opposed to the “modern C++” way of scattering objects on heap memory via unique_ptr/shared_ptr as much as possible).
It's generally better to simply not implement operator= and take the compiler generated default implementation for both copy and move assignment. Then design your class to have semantics such that member-wise assignment does the Right Thing (tm). C style. If you want an idiom for this it's generally called the 'Rule of Zero'
You get swap() for free since std::swap will do the optimal thing.
Move, and thus swap (since it's 3 moves), should essentially always be noexcept (never throw)
This way I know I don't need to learn the whole language at this particular time; only the necessary parts I can use to produce the desired output, which really helps me control my stress.
MyClass& operator=(const MyClass& other) {
return (*this) = MyClass(other);
}1. A class that's responsible for managing exactly one instance of a resource (e.g. FileDescriptor). This class defines whether or not a resource is copyable and/or movable.
2. A dynamically sized container that needs to offer certain kinds of guarantees.
3. A composite class that may contain zero or more instances of 1, 2, and/or 3.
The vast majority of classes you write are always going to be 3. Option 3 should NEVER specify move/copy constructors/assignment operators and instead should inherit that from whatever is being stored.
Don't do #2 unless you really know what you're doing. Prefer to use existing well-behaved containers from STL. If you need 3p ones, Folly and Boost are probably good ones. I don't know how ABSEIL fares around exception safety when exceptions are enabled so YMMV there if you're doing things in an exception context. It can be hard to properly make sure that you inherit the copyability / movability from the underlying type (& also noexcept inheritance).
For #1, KISS is a very good principle. Just make sure you always leave resources in a consistent state. Move helps here a lot but can be emulated with copy + swap pre-C++11. Here's an example of what a copyable and movable resource might look like:
// Terrible idea - don't actually do this in practice.
class AutoDupOnCopyFd {
public:
AutoDupOnCopyFd(const AutoDupOnCopyFd& copy): _fd(dup(copy._fd)) {
if (_fd == -1) { throw ... }
}
AutoDupOnCopyFd(AutoDupOnCopyFd&& owned) noexcept: _fd(std::exchange(copy._fd, -1)) {}
~AutoDupOnCopyFd() noexcept { close(_fd); }
AutoDupOnCopyFd& operator=(const AutoDupOnCopyFd& other) {
// No need to check for assignment to self but you can if you think you're likely to have this happen
// to avoid the syscall (copy to self is typically rare)
AutoDupOnCopyFd copy(other);
*this = *copy;
return *this;
}
AutoDupOnCopyFd& operator=(AutoDupOnCopyFd&& other) noexcept {
using std::swap;
swap(_fd, other._fd);
return *this;
}
private:
int _fd;
};
Now `AutoDupOnCopyFd` can be nested in any composite class & the composite class will automatically inherit the right set of copy / move semantics with correct exception safety. The rule of 0 really is a powerful concept. This breaks down if you start trying to make 1 class responsible for multiple resources. Don't do that. Use resource classes + containers/composite classes to do that.You can also (ab)use unique_ptr for unique ownership although I still prefer explicitly named classes (no confusion with `->` vs `.`, easier to understand for the vast majority of coders).
It's a powerful concept to internalize to level up your C++ game but it's only applicable to C++'s ownership model. Rust went a more teachable path that doesn't have these foot guns.
OMG this is my time to shine. I wrote the library for testing Abseil containers for exception-safety. I don't work there anymore but I know they've implemented exception safety tests for a bunch of containers in there, and I remember when I was writing the library we even found a bug in GNU `std::optional`.
https://www.youtube.com/watch?v=XPzHNSUnTc4 is the talk I gave at cppCon about this.
As a side story, that talk was the first one in the morning and I showed up just before it started on account of my Uber driver missing the highway exit. I had planned to grab some food at the conference but didn't have time so that whole talk I was overstimulated on coffee on an empty stomach trying to keep my composure early in the morning.
i.e. it might be fine at the app-level since you might know all your callers in that case, but you shouldn't do it in a library that others might want to use, or your code will behave erratically.