For instance, if you want to avoid unnecessary copy operations when returning a string, just return it in variable that you pass by reference (eg. void doSomething(string& str);) likewise avoid the vector class making unnecessary copies, simply by creating the objects on the heap and use a vector of pointers instead of values. It's a bit more ugly, but it works, and you don't need to read a 24 page blog to understand all the corner cases where it can go wrong. modern c++ is all about syntactic suger.
Like if I have a vector<std::string>, in c++03 when it resizes it must copy every string from the old storage to the new storage. For a vector of size N, that's up to N+1 allocations (allowing for the possibility that std::string uses the small string optimization).
Granted, std::string doesn't have to allocate when copied if it's a "copy on write" implementation. IIRC, there were some implementations that used that technique when c++03 was the latest, but I don't think there are any that still do, due to other problems with COW.
In modern c++, that same vector resizing operation requires exactly one allocation (for the new vector storage), because all the strings can be moved from the old storage to the new.
Yes, you could have a vector of pointers to std::string, but now you've got yet another allocation (and indirection on access) for every string. In practice that tradeoff almost never makes sense, unless perhaps the strings have shared ownership (e.g. vector<shared_ptr<string>>).
Ultimately, I think there's really no question that the vector resizing optimization described above is useful in certain scenarios. Having said that, I do agree that the associated complexity is annoying. Therefore, the real question is whether it's possible to have these benefits with less complexity, and I personally don't know the answer to that.
At that point you are slowly rewriting the universe. So you can also do it in Rust tbh (which provides seamless updates and integration between epochs/editions).
Even languages that have tried to fast-follow and disrupt C++ end up looking a lot like C++. There is an irreducible complexity.
Like, I can understand how systems programming requiring programmers to think about questions like “how can I proceed if allocation fails? How does this code work in an embedded context with no heap?” is hard and irreducible.
But I can’t understand why a language’s choice to impose complex rules like C++ move constructor hell is an inevitable outcome of irreducible complexity in systems programming. Put another way: C is also a systems programming language that works for many people, and it doesn’t have any of these Byzantine rules (unless you build them yourself). That’s not to say C is better/preferable, but it swims in the same “official Big Gun systems language” pond as C++, which seems to indicate that revalue semantics as complex as C++’s are a choice, not an inevitability.
No, you should only use the heap if necessary.
The bigger issue in C is there is no concept of references, so if you want to modify memory, the only recourse is return-by-value or a pointer. Usually you see the latter, before return value optimization it was considered a waste of cycles to copy structs.
In the embedded world, its often the case you won't see a single malloc/free anywhere. Because sizes of inputs were often fixed and known at compile time for a particular configuration.
Maybe if move semantics, and noexcept, had been designed into C++ from the beginning then the designers might have chosen to insist that move constructors be noexcept, but since these were added later there is code out there with move constructors that do throw exceptions...
Note by the way that the issue being described isn't strictly about std::move or move semantics in general, but more about the STL and containers like std::vector that have chosen to define behavior that makes noexcept move constructors necessary to be used when reallocating.
Programmer here for 30 years in C/C++. It is true that C++ has become a more complex language after rvalue references were introduced, but you have to understand the rationale behind C++: a language suitable for large scale systems programming with *ZERO OVERHEAD*.
The language complexity especially rvalue references was to reduce overhead. Pre-C++-11, there were many code patterns that involved constructing temporaries and destroying them immediately.
C is not suitable as a large scale programming language. Just look at the number of defects in the Linux kernel and their attempt at extending the language through custom compiler attributes to overcome the limitations of C.
Is this the reason why C++ was created, or the last remaining niche that C++ is holding onto?
I remember the early 90's, and it very much seemed like C++ was being pushed as both a general-purpose language and the logical successor to C, insert Linus Torvalds rant here. On top of that, C++ made the decision to privilege a form of polymorphism that had pointer-chasing baked into its internal design, as well as having a good chunk of the standard library being considered a footgun best to avoid due to how much it blew up compile-times.
I think that C++ is a zero-overhead language now because a series of general purpose languages that came afterwards took the other niches away from it, plus the benefit of 30+ years worth of compiler optimizations that were originally largely aimed at the mountain of C code that was out there.
EDIT: Almost forgot about exceptions, the other enormous performance footgun that was an early pre-standard C++ feature.
Zero overhead is a fiction the committee likes to tell themselves, but it's not true.
The best programmers I know of have basically all abandoned C++ in favor of either languages they made, or just use plain C
It's not about irreducible complexity in systems programming, it's about irreducible complexity in the creation of higher level abstractions.
You could certainly implement something functionally equivalent to std::vector<std::string> in C. What you couldn't do in C is implement std::vector<T> correctly and efficiently for any type T. That's where much of the complexity comes from.
The hard part is giving the compiler enough information so that it can automate a lot of what would have to be manually written in a language like C, and to produce a result that is both correct and efficient.
And here we see this principle rear its ugly head yet again. In this case, its the combination of exceptions, manual memory allocation and the desire to make things work efficiently - of which the move constructor was developed as a "solution"