Great article. Modern C++ has come a really long way. I think lots of people have no idea about the newer features of the standard library and how much they minimize footguns.
I write C++ daily and I really can't take seriously arguments how C++ is safe if you know what you're doing like come on. Any sufficiently large and complex codebases tend to have bugs and footguns and using tools like memory safe languages limit blast radius considerably.
Smart pointers are neat but they are not a solution for memory safety. Just using standard containers and iterators can lead to lots of footguns, or utils like string_view.
This is a good article but it only scratches the surface, as is always the case when it comes to C++.
When I made a meme about C++ [1] I was purposeful in choosing the iceberg format. To me it's not quite satisfying to say that C++ is merely complex or vast. A more fitting word would be "arcane", "monumental" or "titanic" (get it?). There's a specific feeling you get when you're trying to understand what the hell is an xvalue, why std::move doesn't move or why std::remove doesn't remove.
The Forest Gump C++ is another meme that captures this feeling very well (not by me) [2].
What it comes down to is developer experience (DX), and C++ has a terrible one. Down to syntax and all the way up to package management a C++ developper feels stuck to a time before they were born. At least we have a lot of time to think about all that while our code compiles. But that might just be the price for all the power it gives you.
Wow, I don't understand what anything means in those memes. And I'm so glad I don't!
It seems to me that the people/committees who built C++ just spent decades inventing new and creative ways for developers to shoot themselves in the foot. Like, why does the language need to offer a hundred different ways to accomplish each trivial task (and 98 of them are bad)?
I don't plan on ever using C++ again, but FWIW in Rust there are lots of cases where you specify `move` and stuff doesn't get moved, or don't specify it and it does, and it's also a specific feeling.
but it's true that when a user first sees `std::move(x)` with no argument saying _where_ to move it to, they either get frustrated or understand they have to get philosophical :-)
Python’s “there should be one obvious way to do it” slogan often collides with reality these days too, since the language sprawled into multiple idioms just like C++: for printing you can use print("hi"), f-strings like f"hi {x}", .format(), % formatting, or concatenation with +; for loops you can iterate with for i in range(n), list comprehensions [f(i) for i in seq], generator expressions (f(i) for i in seq), or map/filter/lambda; unpacking can be done with a,b=pair, tuple() casting, slicing, *args capture, or dictionary unpacking with *; conditionals can be written with if/else blocks, one-line ternary x if cond else y, and/or short-circuit hacks, or pattern matching match/case; default values can come from dict.get(k,default), x or default, try/except, or setdefault; swapping variables can be done with a,b=b,a, with a temp var, with tuple packing/unpacking, or with simultaneous assignment; joining strings can be done with "".join(list), concatenation in a loop, reduce(operator.add, seq), or f-strings; reading files can be open().read(), iterating line by line with for line in f, using pathlib.Path.read_text(), or with open(...) as f; building lists can be done with append in a loop, comprehensions, list(map(...)), or unpacking with [*a,*b]; dictionaries can be merged with {*a,*b}, a|b (Python 3.9+), dict(a,*b), update(), or comprehensions; equality and membership checks can be ==, is, in, any(...), all(...), or chained comparisons; function arguments can be passed positionally, by name, unpacked with * and \*, or using functools.partial; iteration with indexes can be for i in range(len(seq)), for i,x in enumerate(seq), zip(range(n),seq), or itertools; multiple return values can be tuples, lists, dicts, namedtuples, dataclasses, or objects; even truthiness tests can be if x:, if bool(x):, if len(x):, or if x != []:. Whew!
> Just using Rust will not magically make your application safe; it will just make it a lot harder to have memory leaks or safety issues.
You know, not sure I even agree with the memory leaks part. If you define a memory leak very narrowly as forgetting to free a pointer, this is correct. But in my experience working with many languages including C/C++, forgotten pointers are almost never the problem. You're gonna be dealing with issues involving "peaky" memory usage e.g. erroneously persistent references to objects or bursty memory allocation patterns. And these occur in all languages.
"you can write perfectly fine code without ever needing to worry about the more complex features of the language. You can write simple, readable, and maintainable code in C++ without ever needing to use templates, operator overloading, or any of the other more advanced features of the language."
You could also inherit a massive codebase old enough to need a prostate exam that was written by many people who wanted to prove just how much of the language spec they could use.
If selecting a job mostly under the Veil of Ignorance, I'll take a large legacy C project over C++ any day.
I'm not sure what I feel about the article's point on boost. It does contribute a lot to the standard library and does provide some excellent libraries, like boost.Unordered
> you can write perfectly fine code without ever needing to worry about the more complex features of the language
Not really because of undefined behaviour. You must be aware of and vigilant about the complexities of C++ because the compiler will not tell you when you get it wrong.
I would argue that Rust is at least in the same complexity league as C++. But it doesn't matter because you don't need to remember that complexity to write code that works properly (almost all of the time anyway, there are some footguns in async Rust but it's nothing on C++).
> Now is [improved safety in Rust rewrites] because of Rust? I’d argue in some small part, yes. However, I think the biggest factor is that any rewrite of an existing codebase is going to yield better results than the original codebase.
A factor, sure. The biggest? Doubtful. It isn't only Rust's safety that helps here, it's its excellent type system.
> But here’s the thing: all programming languages are unsafe if you don’t know what you’re doing.
Somehow managed to fit two fallacies in one sentence!
1. The fallacy of the grey - no language is perfect therefore they are all the same.
2. "I don't make mistakes."
> Just using Rust will not magically make your application safe; it will just make it a lot harder to have memory leaks or safety issues.
Not true. As I said already Rust's very strong type system helps to make applications less buggy even ignoring memory safety bugs.
> Yes, C++ can be made safer; in fact, it can even be made memory safe. There are a number of libraries and tools available that can help make C++ code safer, such as smart pointers, static analysis tools, and memory sanitizers
lol
> Avoid boost like the plague.
Cool, so the ecosystem isn't confusing but you have to avoid one of the most popular libraries. And Boost is fine anyway. It has lots of quite high quality libraries, even if they do love templates too much.
> Unless you are writing a large and complex application that requires the specific features provided by Boost, you are better off using other libraries that are more modern and easier to use.
Uhuh what would you recommend instead of Boost ICL?
I guess it's a valiant attempt but this is basically "in defense of penny farthings" when the safety bicycle was invented.
> Just using Rust will not magically make your application safe; it will just make it a lot harder to have memory leaks or safety issues.
Even if we take this claim at face value, isn’t that great?
Memory safety is a HUGE source of bugs and security issues. So the author is hand-waving away a really really good reason to use Rust (or other memory safe by default language).
Overall I agree this seems a lot like “I like C++and I’m good at it so it’s fine” with justifications created from there.
When it comes to programming, I generally decide my thoughts based on pain-in-my-ass levels. If I constantly have to fiddle with something to get it working, if it's fragile, if it frequently becomes a pain point - then it's not great.
And out of all the tools and architecture I work with, C++ has been some of the least problematic. The STL is well-formed and easy to work with, creating user-defined types is easy, it's fast, and generally it has few issues when deploying. If there's something I need, there's a very high chance a C or C++ library exists to do what I need. Even crossing multiple major compiler versions doesn't seem to break anything, with rare exceptions.
The biggest problem I have with C++ is how easy it is to get very long compile times, and how hard it feels like it is to analyze and fix that on a 'macro' (whole project) level. I waste ungodly amounts of time compiling. I swear I'm going to be on deaths door and see GCC running as my life flashes by.
Some others that have been not-so-nice:
* Python - Slow enough to be a bottleneck semi-frequently, hard to debug especially in a cross-language environment, frequently has library/deployment/initialization problems, and I find it generally hard to read because of the lack of types, significant whitespace, and that I can't easily jump with an IDE to see who owns what data. Also pip is demon spawn. I never want to see another Wheel error until the day I die.
* VSC's IntelliSense - My god IntelliSense is picky. Having to manually specify every goddamn macro, one at a time in two different locations just to get it to stop breaking down is a nightmare. I wish it were more tolerant of having incomplete information, instead of just shutting down completely.
* Fortran - It could just be me, but IDEs struggle with it. If you have any global data it may as well not exist as far as the IDE is concerned, which makes dealing with such projects very hard.
* CMake - I'm amazed it works at all. It looks great for simple toy projects and has the power to handle larger projects, but it seems to quickly become an ungodly mess of strange comments and rules that aren't spelled out - and you have no way of stepping into it and seeing what it's doing. I try to touch it as infrequently as possible. It feels like C macros, in a bad way.
If you are willing to give up incremental compilation, concatenating all C++ files into a single file and compiling that on a single core will often outperform a multi-core compilation. The reason is that the compiler spends most of its time parsing headers and when you concentrate everything into a single file (use the C preprocessor for this), it only needs to parse headers once.
Merely parsing C++ code requires a higher time complexity than parsing C code (linear time parsers cannot be used for C++), which is likely where part of the long compile times originate. I believe the parsing complexity is related to templates (and the headers are full of them), but there might be other parts that also contribute to it. Having to deal with far more abstractions is likely another part.
That said, I have been incrementally rewriting a C++ code base at a health care startup into a subset of C with the goal of replacing the C++ compiler with a C compiler. The closer the codebase comes to being C, the faster it builds.
> Fortran - It could just be me, but IDEs struggle with it. If you have any global data it may as well not exist as far as the IDE is concerned, which makes dealing with such projects very hard.
You really should not have global data. Modules are the way to go and have been since Fortran90.
> CMake - I'm amazed it works at all. It looks great for simple toy projects and has the power to handle larger projects, but it seems to quickly become an ungodly mess of strange comments and rules that aren't spelled out - and you have no way of stepping into it and seeing what it's doing. I try to touch it as infrequently as possible. It feels like C macros, in a bad way.
C++ will always stay relevant. Software has eaten the world. That transition is almost complete now. The languages that were around when it happened will stay deeply embedded in our fundamental tech stacks for another couple decades at least, if not centuries. And C and C++ are the lion's share of that.
COBOL sticks around 66 years after its first release. Fortran is 68 years old and is still enormously relevant. Much, much more software was written in newer languages and has become so complex that replacements have become practically impossible (Fuchsia hasn't replaces Linux in Google products, wayland isn't ready to replace X11 etc)
Especially the 'backend' languages that do all the heavy lifting for domain-specific software. Just in my vertical of choice, financial software, there are literally billions of lines of Java and .NET code powering critical systems. The code is the documentation, and there's little appetite to rewrite all that at enormous cost and risk.
Perhaps AI will get reliable enough to pour through these double-digit million LOC codebases and convert them flawlessly, but that looks like it's decades off at this point.
It seems likely that C++ will end up in a similar place as COBOL or Fortran, but I don't see that as a good future for a language.
These languages are not among the top contenders for new projects. They're a legacy problem, and are kept alive only by a slowly shrinking number of projects. It may take a while to literally drop to zero, but it's a path of exponential decay towards extinction.
C++ has strong arguments for sticking around as a legacy language for several too-big-to-rewrite C++ projects, but it's becoming less and less attractive for starting new projects.
C++ needs a better selling point than being a language that some old projects are stuck with. Without growth from new projects, it's only a matter of time until it's going to be eclipsed by other languages and relegated to shrinking niches.
I don't think there could be any purer of an expression of the Blub Paradox.
> Just use whatever parts of the language you like without worrying about what's most performant!
It's not about performant. It's about understanding someone else's code six months after they've been fired, and thus restricting what they can possibly have done. And about not being pervasively unsafe.
> "I don’t think C++ is outdated by any stretch of the imagination", "matter of personal taste".
Except of course for header files, forward declarations, Make, the true hell of C++ dependency management (there's an explicit exhortation not to use libraries near the bottom), a thousand little things like string literals actually being byte pointers no matter how thoroughly they're almost compatible with std::string, etc. And of course the pervasive unsafety. Yes, it sure was last updated in 2023, the number of ways of doing the same thing has been expanded from four to five but the module system still doesn't work.
> You can write unsafe code in Python! Rewriting always makes the code more safe whether it's in Rust or not!
No. Nobody who has actually used Rust can reasonably arrive at this opinion. You can write C++ code that is sound; Rust-fluent people often do. The design does not come naturally just because of the process of rewriting, this is an entirely ridiculous thing to claim. You will make the same sorts of mistakes you made writing it fresh, because you are doing the same thing as you were when writing it fresh. The Rust compiler tells you things you were not thinking of, and Rust-fluent people write sound C++ code because they have long since internalized these rules.
And the crack about Python is just stupid. When people say 'unsafe' and Rust in the same sentence, they are obviously talking about UB, which is a class of problem a cut above other kinds of bugs in its pervasiveness, exploitability, and ability to remain hidden from code review. It's 'just' memory safety that you're controlling, which according to Microsoft is 70% of all security related bugs. 70% is a lot! (plus thread safety, if this was not mentioned you know they have not bothered using Rust)
In fact the entire narrative of 'you'll get it better the second time' is nonsense, the software being rewritten was usually written for the first time by totally different people, and the rewriters weren't around for it or most of the bugfixes. They're all starting fresh, the development process is nearly the same as the original blank slate was - if they get it right with Rust, then Rust is an active ingredient in getting it right!
> Just use smart pointers!
Yes, let me spam angle brackets on every single last function. 'Write it the way you want to write it' is the first point in the article, and here is the exact 'write it this way' that was critiquing. And you realistically won't do it on every function so it is just a matter of time until one of the functions you use regular references with creates a problem.
I think, if one of the most prominent C++ experts in the world(herb sutter), who chaired the C++ standards committee for 20+ years, who has evangelized the language for even longer than that - decides that complexity in the language has gotten out of control and sits down to write a simpler and safer dialect, then that is indicative of a problem with the language.
My viewpoint on the language is that there are certain types of engineers who thrive in the complexity that is easy to arrive at in a C++ code base. These engineers are undoubtedly very smart, but, I think, lack a sense of aesthetics that I can never get past. Basically, the r/atbge of programming languages (Awful Taste But Great Execution).
When NIST released its summary judgement against C++ and other languages it deemed memory unsafe, the problem became less technical and more about politics and perception. If you're looking to work within two arms' length of the US Government, you have to consider the "written in C++" label seriously, regardless of how correct the code may be.
> in C++, you can write perfectly fine code without ever needing to worry about the more complex features of the language. You can write simple, readable, and maintainable code in C++ without ever needing to use templates, operator overloading, or any of the other more advanced features of the language.
This... doesn't really hold water. You have to learn about what the insane move semantics are (and the syntax for move ctors/operators) to do fairly basic things with the language. Overloaded operators like operator*() and operator<<() are widely used in the standard library so you're forced to understand what craziness they're doing under the hood. Basic standard library datatypes like std::vector use templates, so you're debugging template instantiation issues whether you write your own templated code or not.
Exactly, the "you dont have" part lasts until the first error message and then it's 10 feet of stl template instantiation error avalanche. And stl implementation is really advanced c++.
Also, a lot of "modern" code cannot be debugged at all (because putting in a print statement breaks constexprness) and your only recourse is reading code.
This is in big part also because of committee, that prefers hundred-line template monster "can_this_call_that_v" to a language feature, probably thinking that by not including something in language standard and offloading it to library they do good job.
> Overloaded operators like operator*() and operator<<() are widely used in the standard library so you're forced to understand what craziness they're doing under the hood
you don't need to understand what an overloaded operator is doing any more than you have to understand the implementation of every function you call, recursively
This is a failed attempt at muddying the waters. You don't know what move semantics is? You go learn what they are. Best/simplest way is to just disable your copy ctor.
You need to know the operator overload semantics for a particular use case? It is not exactly hidden lore, there are even man pages (libstdc++-doc, man 3 std::ostream) or just use std::println.
You are stuck instantiating std::vector? Then you will be stuck in any language anyway.
> You have to learn about what the insane move semantics are (and the syntax for move ctors/operators) to do fairly basic things with the language
That is simply not true. You can write a lot of C++ code without even touching move stuff. Hell, we've been fine without move semantics for the last 30 years :P
> Overloaded operators like operator*() and operator<<() are widely used in the standard library so you're forced to understand what craziness they're doing under the hood
Partially true. operator*() is used through the standard library a lot, because it nicely wraps pointer semantics. Still, you don't have to know about implementation details, as they depend on how the standard library implements the underlying containers.
AFAIK operator<<() is mainly (ab)used by streams. And you can freely skip that part; many C++ developers find them unnecessarily slow and complex.
> Basic standard library datatypes like std::vector use templates, so you're debugging template instantiation issues whether you write your own templated code or not.
As long as you keep things simple, errors are going to be simple. The problem with "modern C++" is that people overuse these new features without fully comprehending their pros and cons, simply because they look cool.
> operator<<() ... widely used in the standard library
Not that widely. You must be thinking of the IO streams part of the library. Yes, it's rather poor in many respects. But you don't have to use it! We have perfectly nice variadic printing functions these days!
auto number = 42;
std::println("Hello, {}! The answer is {}", "world", number);
std::println dates to C++23. This is very recent. You only have to be using Clang 18 (March 2024) or older to find the feature unsupported: https://godbolt.org/z/xPqYafhss (Clang 19 dates to September 2024, only a year ago.)
In contrast, the standard C++ stream types have used operator<< overloading for more than 25 years. glog/gtest assertions continue to use it.
Good article overall. There's one part I don't really agree with:
> Here’s a rule of thumb I like to follow for C++: make it look as much like C as you possibly can, and avoid using too many advanced features of the language unless you really need to.
This has me scratching my head a bit. In spite of C++ being nearly a superset of C, they are very different languages, and idiomatic C++ doesn't look very much like C. In fact, I'd argue that most of the stuff C++ adds to C allows you to write code that's much cleaner than the equivalent C code, if you use it the intended way. The one big exception I can think of is template metaprogramming, since the template code can be confusing, but if done well, the downstream code can be incredibly clean.
There's an even bigger problem with this recommendation, which is how it relates to something else talked about in the article, namely "safety." I agree with the author that modern C++ can be a safe language, with programmer discipline. C++ offers a very good discipline to avoid resource leaks of all kinds (not just memory leaks), called RAII [1]. The problem here is that C++ code that leverages RAII looks nothing like C.
Stepping back a bit, I feel there may be a more fundamental fallacy in this "C++ is Hard to Read" section in that the author seems to be saying that C++ can be hard to read for people who don't know the language well, and that this is a problem that should be addressed. This could be a little controversial, but in my opinion you shouldn't target your code to the level of programmers who don't know the language well. I think that's ultimately neither good for the code nor good for other programmers. I'm definitely not an expert on all the corners of C++, but I wouldn't avoid features I am familiar with just because other programmers might not be.
A pet peeve of mine is when people claim C++ is a superset of C. It really isn't. There's a lot of little nuanced differences that can bite you.
Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
C++20 does have C11's designated initialization now, which helps in some cases, but that was a pain for a long time.
enums and conversion between integers is very strict in C++.
`char * message = "Hello"` is valid C but not C++ (since you cannot mutate the pointed to string, it must be `const` in C++)
C99 introduced variadic macros that didn't become standard C++ until 2011.
C doesn't allow for empty structs. You can do it in C++, but sizeof(EmptyStruct) is 1. And if C lets you get away with it in some compilers, I'll bet it's 0.
Anyway, all of these things and likely more can ruin your party if you think you're going to compile C code with a C++ compiler.
Also don't forget if you want code to be C callable in C++ you have to use `extern "C"` wrappers.
> It really isn't. There's a lot of little nuanced differences that can bite you.
These are mostly inconsequential when using code other people write. It is trivial to mix C and C++ object files, and where the differences (in headers) do matter, they can be ifdefed away.
> void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
This makes sense because void* -> T* is a downcast. I find the C behavior worse.
> enums and conversion between integers is very strict in C++.
As it should, but unscoped enums are promoted to integers the same way they are in C
> `char * message = "Hello"` is valid C but not C++
Code smell anyway, you can and should use char[] in both languages
You didn't mention the difference in inline semantics which IMO has more impact than what you cited
The two will also continue to diverge over time, after all, C2y should have the defer feature, which C++ will likely never add. Even if we used polyfills to let C++ compilers support it, the performance characteristics could be quite different; if we compare a polyfill (as suggested in either N3488 or N3434) to a defer feature, C++ would be in for a nasty shock as the "zero cost abstractions" language, compared to how GCC does the trivial re-ordering and inlining even at -O1, as quickly tested here: https://godbolt.org/z/qoh861Gch
I used the [[gnu::cleanup]] attribute macro (as in N3434) since it was simple and worked with the current default GCC on CE, but based on TS 25755 the implementation of defer and its optimisation should be almost trivial, and some compilers have already added it. Oh, and the polyfills don't support the braceless `defer free(p);` syntax for simple defer statements, so there goes the full compatibility story...
While there are existing areas where C diverged, as other features such as case ranges (N3370, and maybe N3601) are added that C++ does not have parity with, C++ will continue to drift further away from the "superset of C" claim some of the 'adherents' have clung to for so long. Of course, C has adopted features and syntax from C++ (C2y finally getting if-declarations via N3356 comes to mind), and some features are still likely to get C++ versions (labelled breaks come to mind, via N3355, and maybe N3474 or N3377, with C++ following via P3568), so the (in)compatibility story is simply going to continue getting more nuanced and complicated over time, and we should probably get this illusion of compatibility out of our collective culture sooner rather than later.
> void * implicit casting in C just works, but in C++ it must be an explicit cast
In C, casting a `void *` is a code smell, I feel.
Most confusing one is how the meaning of `const` differs between C and C++; I'm pretty certain the C `const` keyword is broken compared to `const` in C++.
Yes, casting in general is a code smell. (Not as in never use it, but as in use it as less as possible of course.)
The thing is, void was introduced exactly to represent this use-case without casting. The type to represent random amount of bytes, check which type and then cast is called char *. void * is for when you know exactly which type it has you just need to pass it through a generic interface, which isn't supposed to know or touch the type. The other use case is for when the object hasn't a type yet, like from malloc.
Previously everything was typed char *, then void * was used to separate the cases: casting from char *, don't cast from void *. Now in C++ both are the same again.
> A pet peeve of mine is when people claim C++ is a superset of C. It really isn't. There's a lot of little nuanced differences that can bite you.
> Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
Your very first example reverses the definitions of superset and subset. "C++ is a superset of C" implies that C++ will have at least as many, if not more, keywords than C.
> Here’s a rule of thumb I like to follow for C++: make it look as much like C as you possibly can, and avoid using too many advanced features of the language unless you really need to.
Also, avoid using C++ classes while you're at it.
I recently had to go back to writing C++ professionally after a many-year hiatus. We code in C++23, and I got a book to refresh me on the basics as well as all the new features.
And man, doing OO in C++ just plain sucks. Needing to know things like copy and swap, and the Rule of Three/Five/Zero. Unless you're doing trivial things with classes, you'll need to know these things. If you don't need to know those things, you might as well stick to structs.
Now I'll grant C++23 is much nicer than C++03 (just import std!) I was so happy to hear about optional, only to find out how fairly useless it is compared to pretty much every language that has implemented a "Maybe" type. Why add the feature if the compiler is not going to protect you from dereferencing without checking?
std::optional does have dereference checking, but it's a run-time check: std::optional<T>::value(). Of course, you'll get an exception if the optional is empty, because there's nothing else for the callee to do.
102 comments
[ 2.9 ms ] story [ 97.2 ms ] threadI just had a PR on an old C++ project, and spending 8 years in the web ecosystem have raised the bar around tooling expectations.
Rust is particularly sweet to work with in that regard.
the truth
On legacy code bases, sure. C++ rules in legacy C++ codebases. That’s kind of a given isn’t it? So that’s not a benefit. Just a fact.
Why performance-critical domains? Does C++ have a performance edge over Rust?
Smart pointers are neat but they are not a solution for memory safety. Just using standard containers and iterators can lead to lots of footguns, or utils like string_view.
When I made a meme about C++ [1] I was purposeful in choosing the iceberg format. To me it's not quite satisfying to say that C++ is merely complex or vast. A more fitting word would be "arcane", "monumental" or "titanic" (get it?). There's a specific feeling you get when you're trying to understand what the hell is an xvalue, why std::move doesn't move or why std::remove doesn't remove.
The Forest Gump C++ is another meme that captures this feeling very well (not by me) [2].
What it comes down to is developer experience (DX), and C++ has a terrible one. Down to syntax and all the way up to package management a C++ developper feels stuck to a time before they were born. At least we have a lot of time to think about all that while our code compiles. But that might just be the price for all the power it gives you.
[1] https://victorpoughon.github.io/cppiceberg/
[2] https://mikelui.io/img/c++_init_forest.gif
It seems to me that the people/committees who built C++ just spent decades inventing new and creative ways for developers to shoot themselves in the foot. Like, why does the language need to offer a hundred different ways to accomplish each trivial task (and 98 of them are bad)?
Whenever someone asks you about std::move, or -values I have a ready-made answer for that: https://stackoverflow.com/a/27026280/1593077
but it's true that when a user first sees `std::move(x)` with no argument saying _where_ to move it to, they either get frustrated or understand they have to get philosophical :-)
You know, not sure I even agree with the memory leaks part. If you define a memory leak very narrowly as forgetting to free a pointer, this is correct. But in my experience working with many languages including C/C++, forgotten pointers are almost never the problem. You're gonna be dealing with issues involving "peaky" memory usage e.g. erroneously persistent references to objects or bursty memory allocation patterns. And these occur in all languages.
You could also inherit a massive codebase old enough to need a prostate exam that was written by many people who wanted to prove just how much of the language spec they could use.
If selecting a job mostly under the Veil of Ignorance, I'll take a large legacy C project over C++ any day.
Nitpick, I guess, but Windows 1.0 was released in November 1985:
https://en.m.wikipedia.org/wiki/Windows_1.0
> you can write perfectly fine code without ever needing to worry about the more complex features of the language
Not really because of undefined behaviour. You must be aware of and vigilant about the complexities of C++ because the compiler will not tell you when you get it wrong.
I would argue that Rust is at least in the same complexity league as C++. But it doesn't matter because you don't need to remember that complexity to write code that works properly (almost all of the time anyway, there are some footguns in async Rust but it's nothing on C++).
> Now is [improved safety in Rust rewrites] because of Rust? I’d argue in some small part, yes. However, I think the biggest factor is that any rewrite of an existing codebase is going to yield better results than the original codebase.
A factor, sure. The biggest? Doubtful. It isn't only Rust's safety that helps here, it's its excellent type system.
> But here’s the thing: all programming languages are unsafe if you don’t know what you’re doing.
Somehow managed to fit two fallacies in one sentence!
1. The fallacy of the grey - no language is perfect therefore they are all the same.
2. "I don't make mistakes."
> Just using Rust will not magically make your application safe; it will just make it a lot harder to have memory leaks or safety issues.
Not true. As I said already Rust's very strong type system helps to make applications less buggy even ignoring memory safety bugs.
> Yes, C++ can be made safer; in fact, it can even be made memory safe. There are a number of libraries and tools available that can help make C++ code safer, such as smart pointers, static analysis tools, and memory sanitizers
lol
> Avoid boost like the plague.
Cool, so the ecosystem isn't confusing but you have to avoid one of the most popular libraries. And Boost is fine anyway. It has lots of quite high quality libraries, even if they do love templates too much.
> Unless you are writing a large and complex application that requires the specific features provided by Boost, you are better off using other libraries that are more modern and easier to use.
Uhuh what would you recommend instead of Boost ICL?
I guess it's a valiant attempt but this is basically "in defense of penny farthings" when the safety bicycle was invented.
Even if we take this claim at face value, isn’t that great?
Memory safety is a HUGE source of bugs and security issues. So the author is hand-waving away a really really good reason to use Rust (or other memory safe by default language).
Overall I agree this seems a lot like “I like C++and I’m good at it so it’s fine” with justifications created from there.
Nobody claims that Rust is a perfect language, the argument is that Rust is a better language than C++.
And out of all the tools and architecture I work with, C++ has been some of the least problematic. The STL is well-formed and easy to work with, creating user-defined types is easy, it's fast, and generally it has few issues when deploying. If there's something I need, there's a very high chance a C or C++ library exists to do what I need. Even crossing multiple major compiler versions doesn't seem to break anything, with rare exceptions.
The biggest problem I have with C++ is how easy it is to get very long compile times, and how hard it feels like it is to analyze and fix that on a 'macro' (whole project) level. I waste ungodly amounts of time compiling. I swear I'm going to be on deaths door and see GCC running as my life flashes by.
Some others that have been not-so-nice:
* Python - Slow enough to be a bottleneck semi-frequently, hard to debug especially in a cross-language environment, frequently has library/deployment/initialization problems, and I find it generally hard to read because of the lack of types, significant whitespace, and that I can't easily jump with an IDE to see who owns what data. Also pip is demon spawn. I never want to see another Wheel error until the day I die.
* VSC's IntelliSense - My god IntelliSense is picky. Having to manually specify every goddamn macro, one at a time in two different locations just to get it to stop breaking down is a nightmare. I wish it were more tolerant of having incomplete information, instead of just shutting down completely.
* Fortran - It could just be me, but IDEs struggle with it. If you have any global data it may as well not exist as far as the IDE is concerned, which makes dealing with such projects very hard.
* CMake - I'm amazed it works at all. It looks great for simple toy projects and has the power to handle larger projects, but it seems to quickly become an ungodly mess of strange comments and rules that aren't spelled out - and you have no way of stepping into it and seeing what it's doing. I try to touch it as infrequently as possible. It feels like C macros, in a bad way.
Merely parsing C++ code requires a higher time complexity than parsing C code (linear time parsers cannot be used for C++), which is likely where part of the long compile times originate. I believe the parsing complexity is related to templates (and the headers are full of them), but there might be other parts that also contribute to it. Having to deal with far more abstractions is likely another part.
That said, I have been incrementally rewriting a C++ code base at a health care startup into a subset of C with the goal of replacing the C++ compiler with a C compiler. The closer the codebase comes to being C, the faster it builds.
You really should not have global data. Modules are the way to go and have been since Fortran90.
> CMake - I'm amazed it works at all. It looks great for simple toy projects and has the power to handle larger projects, but it seems to quickly become an ungodly mess of strange comments and rules that aren't spelled out - and you have no way of stepping into it and seeing what it's doing. I try to touch it as infrequently as possible. It feels like C macros, in a bad way.
I like how you wrote my feelings so accurately :D
COBOL sticks around 66 years after its first release. Fortran is 68 years old and is still enormously relevant. Much, much more software was written in newer languages and has become so complex that replacements have become practically impossible (Fuchsia hasn't replaces Linux in Google products, wayland isn't ready to replace X11 etc)
Bit off more than it could chew, no we all have indigestion
Perhaps AI will get reliable enough to pour through these double-digit million LOC codebases and convert them flawlessly, but that looks like it's decades off at this point.
These languages are not among the top contenders for new projects. They're a legacy problem, and are kept alive only by a slowly shrinking number of projects. It may take a while to literally drop to zero, but it's a path of exponential decay towards extinction.
C++ has strong arguments for sticking around as a legacy language for several too-big-to-rewrite C++ projects, but it's becoming less and less attractive for starting new projects.
C++ needs a better selling point than being a language that some old projects are stuck with. Without growth from new projects, it's only a matter of time until it's going to be eclipsed by other languages and relegated to shrinking niches.
> Just use whatever parts of the language you like without worrying about what's most performant!
It's not about performant. It's about understanding someone else's code six months after they've been fired, and thus restricting what they can possibly have done. And about not being pervasively unsafe.
> "I don’t think C++ is outdated by any stretch of the imagination", "matter of personal taste".
Except of course for header files, forward declarations, Make, the true hell of C++ dependency management (there's an explicit exhortation not to use libraries near the bottom), a thousand little things like string literals actually being byte pointers no matter how thoroughly they're almost compatible with std::string, etc. And of course the pervasive unsafety. Yes, it sure was last updated in 2023, the number of ways of doing the same thing has been expanded from four to five but the module system still doesn't work.
> You can write unsafe code in Python! Rewriting always makes the code more safe whether it's in Rust or not!
No. Nobody who has actually used Rust can reasonably arrive at this opinion. You can write C++ code that is sound; Rust-fluent people often do. The design does not come naturally just because of the process of rewriting, this is an entirely ridiculous thing to claim. You will make the same sorts of mistakes you made writing it fresh, because you are doing the same thing as you were when writing it fresh. The Rust compiler tells you things you were not thinking of, and Rust-fluent people write sound C++ code because they have long since internalized these rules.
And the crack about Python is just stupid. When people say 'unsafe' and Rust in the same sentence, they are obviously talking about UB, which is a class of problem a cut above other kinds of bugs in its pervasiveness, exploitability, and ability to remain hidden from code review. It's 'just' memory safety that you're controlling, which according to Microsoft is 70% of all security related bugs. 70% is a lot! (plus thread safety, if this was not mentioned you know they have not bothered using Rust)
In fact the entire narrative of 'you'll get it better the second time' is nonsense, the software being rewritten was usually written for the first time by totally different people, and the rewriters weren't around for it or most of the bugfixes. They're all starting fresh, the development process is nearly the same as the original blank slate was - if they get it right with Rust, then Rust is an active ingredient in getting it right!
> Just use smart pointers!
Yes, let me spam angle brackets on every single last function. 'Write it the way you want to write it' is the first point in the article, and here is the exact 'write it this way' that was critiquing. And you realistically won't do it on every function so it is just a matter of time until one of the functions you use regular references with creates a problem.
My viewpoint on the language is that there are certain types of engineers who thrive in the complexity that is easy to arrive at in a C++ code base. These engineers are undoubtedly very smart, but, I think, lack a sense of aesthetics that I can never get past. Basically, the r/atbge of programming languages (Awful Taste But Great Execution).
Yet here we are.
This... doesn't really hold water. You have to learn about what the insane move semantics are (and the syntax for move ctors/operators) to do fairly basic things with the language. Overloaded operators like operator*() and operator<<() are widely used in the standard library so you're forced to understand what craziness they're doing under the hood. Basic standard library datatypes like std::vector use templates, so you're debugging template instantiation issues whether you write your own templated code or not.
This is in big part also because of committee, that prefers hundred-line template monster "can_this_call_that_v" to a language feature, probably thinking that by not including something in language standard and offloading it to library they do good job.
Oh the horror!
you don't need to understand what an overloaded operator is doing any more than you have to understand the implementation of every function you call, recursively
You need to know the operator overload semantics for a particular use case? It is not exactly hidden lore, there are even man pages (libstdc++-doc, man 3 std::ostream) or just use std::println.
You are stuck instantiating std::vector? Then you will be stuck in any language anyway.
That is simply not true. You can write a lot of C++ code without even touching move stuff. Hell, we've been fine without move semantics for the last 30 years :P
> Overloaded operators like operator*() and operator<<() are widely used in the standard library so you're forced to understand what craziness they're doing under the hood
Partially true. operator*() is used through the standard library a lot, because it nicely wraps pointer semantics. Still, you don't have to know about implementation details, as they depend on how the standard library implements the underlying containers.
AFAIK operator<<() is mainly (ab)used by streams. And you can freely skip that part; many C++ developers find them unnecessarily slow and complex.
> Basic standard library datatypes like std::vector use templates, so you're debugging template instantiation issues whether you write your own templated code or not.
As long as you keep things simple, errors are going to be simple. The problem with "modern C++" is that people overuse these new features without fully comprehending their pros and cons, simply because they look cool.
Not that widely. You must be thinking of the IO streams part of the library. Yes, it's rather poor in many respects. But you don't have to use it! We have perfectly nice variadic printing functions these days!
In contrast, the standard C++ stream types have used operator<< overloading for more than 25 years. glog/gtest assertions continue to use it.
> Here’s a rule of thumb I like to follow for C++: make it look as much like C as you possibly can, and avoid using too many advanced features of the language unless you really need to.
This has me scratching my head a bit. In spite of C++ being nearly a superset of C, they are very different languages, and idiomatic C++ doesn't look very much like C. In fact, I'd argue that most of the stuff C++ adds to C allows you to write code that's much cleaner than the equivalent C code, if you use it the intended way. The one big exception I can think of is template metaprogramming, since the template code can be confusing, but if done well, the downstream code can be incredibly clean.
There's an even bigger problem with this recommendation, which is how it relates to something else talked about in the article, namely "safety." I agree with the author that modern C++ can be a safe language, with programmer discipline. C++ offers a very good discipline to avoid resource leaks of all kinds (not just memory leaks), called RAII [1]. The problem here is that C++ code that leverages RAII looks nothing like C.
Stepping back a bit, I feel there may be a more fundamental fallacy in this "C++ is Hard to Read" section in that the author seems to be saying that C++ can be hard to read for people who don't know the language well, and that this is a problem that should be addressed. This could be a little controversial, but in my opinion you shouldn't target your code to the level of programmers who don't know the language well. I think that's ultimately neither good for the code nor good for other programmers. I'm definitely not an expert on all the corners of C++, but I wouldn't avoid features I am familiar with just because other programmers might not be.
[1] https://en.cppreference.com/w/cpp/language/raii.html
Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
C++20 does have C11's designated initialization now, which helps in some cases, but that was a pain for a long time.
enums and conversion between integers is very strict in C++.
`char * message = "Hello"` is valid C but not C++ (since you cannot mutate the pointed to string, it must be `const` in C++)
C99 introduced variadic macros that didn't become standard C++ until 2011.
C doesn't allow for empty structs. You can do it in C++, but sizeof(EmptyStruct) is 1. And if C lets you get away with it in some compilers, I'll bet it's 0.
Anyway, all of these things and likely more can ruin your party if you think you're going to compile C code with a C++ compiler.
Also don't forget if you want code to be C callable in C++ you have to use `extern "C"` wrappers.
C++ designated initializers are slightly different in that the initialization order must match the declared member order. That is not required in C.
These are mostly inconsequential when using code other people write. It is trivial to mix C and C++ object files, and where the differences (in headers) do matter, they can be ifdefed away.
> void * implicit casting in C just works, but in C++ it must be an explicit cast (which is kind of funny considering all the confusing implicit behavior in C++).
This makes sense because void* -> T* is a downcast. I find the C behavior worse.
> enums and conversion between integers is very strict in C++.
As it should, but unscoped enums are promoted to integers the same way they are in C
> `char * message = "Hello"` is valid C but not C++
Code smell anyway, you can and should use char[] in both languages
You didn't mention the difference in inline semantics which IMO has more impact than what you cited
I used the [[gnu::cleanup]] attribute macro (as in N3434) since it was simple and worked with the current default GCC on CE, but based on TS 25755 the implementation of defer and its optimisation should be almost trivial, and some compilers have already added it. Oh, and the polyfills don't support the braceless `defer free(p);` syntax for simple defer statements, so there goes the full compatibility story...
While there are existing areas where C diverged, as other features such as case ranges (N3370, and maybe N3601) are added that C++ does not have parity with, C++ will continue to drift further away from the "superset of C" claim some of the 'adherents' have clung to for so long. Of course, C has adopted features and syntax from C++ (C2y finally getting if-declarations via N3356 comes to mind), and some features are still likely to get C++ versions (labelled breaks come to mind, via N3355, and maybe N3474 or N3377, with C++ following via P3568), so the (in)compatibility story is simply going to continue getting more nuanced and complicated over time, and we should probably get this illusion of compatibility out of our collective culture sooner rather than later.
In C, casting a `void *` is a code smell, I feel.
Most confusing one is how the meaning of `const` differs between C and C++; I'm pretty certain the C `const` keyword is broken compared to `const` in C++.
The thing is, void was introduced exactly to represent this use-case without casting. The type to represent random amount of bytes, check which type and then cast is called char *. void * is for when you know exactly which type it has you just need to pass it through a generic interface, which isn't supposed to know or touch the type. The other use case is for when the object hasn't a type yet, like from malloc.
Previously everything was typed char *, then void * was used to separate the cases: casting from char *, don't cast from void *. Now in C++ both are the same again.
> Ignore the fact that having more keywords in C++ precludes the legality of some C code being C++. (`int class;`)
Your very first example reverses the definitions of superset and subset. "C++ is a superset of C" implies that C++ will have at least as many, if not more, keywords than C.
Other examples make the same mistake.
Also, avoid using C++ classes while you're at it.
I recently had to go back to writing C++ professionally after a many-year hiatus. We code in C++23, and I got a book to refresh me on the basics as well as all the new features.
And man, doing OO in C++ just plain sucks. Needing to know things like copy and swap, and the Rule of Three/Five/Zero. Unless you're doing trivial things with classes, you'll need to know these things. If you don't need to know those things, you might as well stick to structs.
Now I'll grant C++23 is much nicer than C++03 (just import std!) I was so happy to hear about optional, only to find out how fairly useless it is compared to pretty much every language that has implemented a "Maybe" type. Why add the feature if the compiler is not going to protect you from dereferencing without checking?