Obviously this behavior is a little weird when you first encounter it. A lot of C++ developers find it weird as well, and I'm sure a lot of C++ developers have made mistakes around this at one time or another. However, it's not nearly as bad as the article makes out.
For one thing, clangd/clang-tidy will emit warnings if you use a moved-from object in an unsafe way, e.g.:
some_vector.emplace_back(std::move(foo));
foo.whatever(); // <-- clang-tidy will warn about this
Using these kinds of static analysis tools is pretty much essential in any professional C++ role. Could you argue that it would be better if this was a language feature instead of something that you need a static analyzer for? Of course, but in practice everyone is using these tools anyway so it's not a big deal.Additionally the C++ semantics are sometimes really useful. As a simplified example, I have some code at work that looks something like this (simplified here, but this shows the general idea):
struct CounterData {
// Threads call this method very frequently, say thousands of times per second
void AddCounter(std::string_view key, int val) {
absl::MutexLock lk(&mut); // ALMOST NEVER CONTENDED
counters[key] += val;
}
absl::flat_hash_map<std::string, int> counters;
absl::Mutex mut;
};
// Thread-local instance of counter_data
static thread_local CounterData counter_data;
// Main thread calls this infrequently, say once a second
absl::flat_hash_map<std::string, int> AggregateCounters() {
std::vector<absl::flat_hash_map<std::string, int>> all_counters;
// Assume it's possible to enumerate threads and iterate over thread local counters,
// this is not a difficult abstraction to build.
all_counters.reserve(num_threads);
for (CounterData &data : all_thread_local_counters) {
absl::MutexLock lk(&data.mut);
all_counters.emplace_back(std::move(data.counters));
}
absl::flat_hash_map<std::string, int> aggregate;
for (const auto &map : all_counters) {
for (const auto &[k, v] : map) {
aggregate[k] += v;
}
}
return aggregate;
}
Again, this is a simplified example. But the point of this code is that it minimizes the time spent in the critical section where each per-thread CounterData is locked by the main thread. Normally calls to AddCounter() will be uncontended and the lock acquisition will be nearly free. The only time the lock is contended is if an AddCounter() call happens at the same time as a call by the main thread to AggregateCounters(). However, AggregateCounters() holds these locks for really short periods of time, because it can acquire the lock, move the absl::flat_hash_map, and then release the lock. The move just updates a few pointers and afterwards the map is in a default initialized state, so it's perfectly OK for the threads to call AddCounter() immediately afterwards, even though the object is in a moved-from state. And AggregateCounters() does all the expensive work (iteration and summing) without needing to hold any locks.You might object: how do you know the state of an absl::flat_hash_map after moving it? It's a reasonable thing to ask but in practice it's really easy to understand. Pretty much anyone writing a custom move constructor is going to do something reasonable, like move pointers to the new object and probably reset the original pointers to nullptr. Since the object needs to be in a valid state after move, this means that things like integer length/capacity fields and whatnot will also need to be reset to zero afterwards, since if they weren't the object wouldn't be in a usable state. And if you have any questions about this, you just look at the code. But really 99.99% of the time the author is going to write their move constructor in a sane way where you can guess the behavior from a few basic principles.
The original post also specifically complains that std::string is weird because if the string is large enough that it allocates then the string will probably be in an empty state after the call to std::move, and if the string is small enough that SSO applies it will probably be in the same state afterwards, and the standard doesn't guarantee anything. Fair enough, but this just means you need to assume that you don't know the string state after a move, and you can't read from a moved-from string. Again, clang-tidy and clangd will warn you if you try to use a string this way.