Many of these differences are intentional and defensible from the C++ side. But some are still surprising because they invalidate patterns that were historically common, performant, or idiomatic in C.
The interesting part to me isn’t "C vs C++," but where the languages diverged philosophically: object lifetime vs raw storage, stronger type systems, implicit conversions, ABI and optimization assumptions, and the boundary between "portable" and "works on my compiler."
I’d also be curious which C constructs people still genuinely miss in modern C++. For me, restrict is still near the top of the list.
Also, let's not forget that implicit casts between unrelated pointer types is only a warning in C. Fortunately, modern C compilers started treating it as an error by default because it caused so much harm: https://gcc.gnu.org/gcc-14/porting_to.html. In C++ this was always a compiler error.
I’m not arguing that that’s better, or worse, but it’s definitely true and by no means a myth.
...so much this! A void pointer is an "any-pointer" by design. It shouldn't require casting from and to specific pointer types, that defeats the whole point of having void pointers in the first place.
Another annoying detail is that C++ doesn't seem to like forward references of `enum`s. That is, while
struct A* a_ptr;
is fine in both C and C++ even before `struct A` has been defined, apparently enum A* a_ptr;
is not cool in C++ until after `enum A` has been defined.One arguable benefit of keeping your C code compatible with (or at least convertible to) C++, is that you can theoretically use scpptool's auto-translation feature as build step to produce memory-safe executables from C code via transpilation to a memory-safe subset of C++.
[1] https://github.com/duneroadrunner/SaferCPlusPlus-AutoTransla...
I should add here that there's also (3): Switch to Fortran, which made fundamentally different choices and is IMO the only fully supported higher-than-C level language that can produce HPC applications without fighting a compiler left and right.
[1] https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3734.pdf
C++ is 1990's Typescript for C++, while C folks still think is a portable Assembly instead of designed to an abstract machine model.
As such C++ community embraces high level abstractions and type systems improvements, whereas C wants to still code as targeting classical hardware.
My pet peeve with C++ is that the sequence point operator can be overloaded at which point it stops being a sequence point.
...I've seen this more often in the opposite direction. Since C++ is stuck with a ca 1995 non-standard subset of C, C++ coders usually have a very outdated view of C.
> I’d also be curious which C constructs people still genuinely miss in modern C++.
Not implementing the full C99 designated init feature set was a huge missed opportunity in C++20. Every single feature of C99 designated init is important and clicks with the other features and the rest of the language, take one or two away and it becomes mostly useless (e.g. the order requirement in C++20 means that designated init is only useful for trvial structs).
It's especially tragic because Clang already had the full C99 designated init feature set in C++ mode implemented long before C++20 and it worked just fine.
> The interesting part to me isn’t "C vs C++," but where the languages diverged philosophically
IMHO this "schism" was completely unnecessary and only happened because of ignorance and hubris by the C++ designers. Objective-C shows that C can be extended with radical new features but without messing up the "C side" (e.g. ObjC features don't overlap with C features, which means that ObjC is automatically compatible with the latest C standards).
In the end it's not a big deal of course, C and C++ are now entirely different languages and longterm that's for the better. Even the C++ peeps seem to have come to that realization and no longer recommend to "compile C in C++ mode" (like Herb Sutter in 2012 when trying to justify why MSVC had no C99 support: https://herbsutter.com/2012/05/03/reader-qa-what-about-vc-an...):
"We recommend that C developers use the C++ compiler to compile C code (using /TP if the file is named something.c). This is the best choice for using Visual C++ to compile C code."
This was bad advice back then and is even worse advice today. At least MSVC got "good enough" C99 support a couple of years later (in VS2015), but after a few hopeful years after 2019 it looks like MSVC development has completely stalled again.Having attempted to implement it correctly in slimcc, there are indeed some edge cases[1][2] that justify not adapting it fully.
[1] Unordered side effects; evaluation of expressions with overlapped destination is implementation defined but not listed as such (the wording in standard is "can potentially not be evaluated").
[2] Both GCC and Clang still get this wrong in 2026: https://github.com/llvm/llvm-project/issues/190858
Following on the Secure Future Initiative activities.
The C updates have been what is required to compile critical FOSS projects, or support big name customers on Windows.
Apple and Google are also not racing to adopt new clang versions on their platforms.
The languages have diverged a lot, it's true. Still, it is worth noting that all the code in TCPL 2nd Ed was compiled with Stroustrups C++ compiler, as there wasn't a C compiler available. Source: Preface/Acknowledgments.
How did Clang handle differences between member declaration order and the order in which initializers appeared?
Edit: I should've had more conviction in my instincts, this is slop.
Perhaps be more careful in trying to make LLM output look like you wrote it yourself. The incongruent punctuation mark types, with curly apostrophes and straight double quotes mixed together in the same text, are a dead giveaway.
Instead of letting compiler implementers decide which features to add and how to implement them, C++ employs a standards-first, top-down approach. Features are often defined by committee members who may not use modern C++ in their daily workflows, leaving it entirely up to the individual implementations to catch up.
Some features were standardized back in 2023, yet not a single implementation supports them in 2026.
Loosely coupling a language to its compiler is 20th century thinking for when programming languages were simple. It works for C because C is simple enough to be implemented over and over again. But for today's hyper complicated languages, multiple implementations is a pain for everyone.
Are you sure about this one? I don’t know exactly who’s in the committee these days but last I checked they were all hardcore C++ programmers with decades of experience from the trenches.
https://www.open-std.org/Jtc1/sc22/wg21/docs/papers/2003/n14...
Now, I rather have them than not, but it is still clunky.
Rust is a language which isn't backwards-compatible, and certainly not compatible with source code in other languages.
Now, sure, Rust has its advantages, but - how can you fault C++ in the context of compatibility?
for example you want to add nice feature to c++ with nice syntax, but there is a similar syntax somewhere in C that nobody uses, but you have to support it. you end up with nice feature with horrible syntax.
Eh, all of the committee members I've known are obsessed with modern C++, and "can this feature be implemented?" is definitely a blocker; numerous features got kicked down the road from C++0x to later versions because compilers weren't ready for them.
For example in Rust there is one big entity that currently pours a lot of energy into improving C++ interop. Now, this is not exactly a niche topic, but especially in a world where AI makes many rewrites possible that we wouldn't have daunted to think about a couple of years before, we shouldn't waste too much effort to save legacy companies enormous codebases at the detriment of our preferred language.
Compilers can often do some static bounds-checking of such arrays. But because those features had been introduced together with variable-length array stack variables , people have lumped them all together and thus shunned these features that could otherwise have added safety.
BTW, one thing you should never do is use variable-length array declarations and alloca() in the same function. As VLA variables have scope life-time and allocas have function life-time they are not compatible — and allocations could overlap. Yet, not all compilers (/versions) that support both warn when they are used together.
In 2019 I wrote a short survey of C constructs that do not
work in C++. The point was not that C is sloppy or that C++
is superior. The point was that C++ is not a superset of C,
and that C programmers crossing the border should know
where the checkpoints are.
C++ was a superset of C 30-ish years ago. Now, as the author correctly identifies, it is not as both have taken different evolutionary paths.Another well-known counterexample is implicit conversion from void*. In C89 you can do `int* foo = malloc(100);` but in C++ it requires an explicit cast from void* to int*.
I don't believe there was ever a time, even pre-standardization, when C++ was a strict superset of C; it always had little incompatibilities here and there.
It looks like you're right and the answer to when was C++ a superset of C may well be "never".
From the description, Cfront had always been a full-fledged parser that only happened to output C since the very beginning.
?: has another execution priority.
Implicit cast scenarios are reduced in C++.
Address white_house{
.street = "1600 Pennsylvania Avenue NW",
.city = "Washington",
.state = "District of Columbia",
.zip = 20500,
};
For me this is the most important initialization in C that helps with clarity so much, I used mostly structs to have function parameters intialized like thisHowever C++ had at time no default initialization for unmentioned fields, so in 2017 I had to remove it when converting the code to C++
https://godbolt.org/z/3aKaa7dnM
only if you specifically ask to get an error, would you actually get it.
That is for when the owner is a std::string or an owning range respectively. But a raw pointer does still make sense as a non-owning view over a single element, doesn't it? I'm new to C++ so I might be wrong.
If you're allocating something on the heap anyway, you shouldn't be forced to pay for an indirection in order to have some variable-sized data in the object, you should just be able to put it all in the one allocation. (Sure, you can achieve that with placement new hackery but that certainly isn't "idiomatic" C++.)
Of course that's completely incompatible with the way allocation and construction work (storage has to be allocated before the constructor runs). Hence "design flaw" rather than "missing feature."
I use this approach as part my zero-copy serialization library for what I call "out-of-line" sequences.
It does require smart usage of std::launder to be standards-compliant though.
- C `_Atomic(T)` and C++ `std::atomic<T>`. C++23 has C compatible header `stdatomic.h` that defines `_Atomic(T)`, but it's still problematic
- C `_Noreturn/noreturn` and C++ `[[noreturn]]`. C23 `[[noreturn]]` makes them compatible
- C inline and C++ inline are different. Good news is their `static inline` are the same
- C has anonymous struct. C++ doesn't. Both have anonymous union though
It doesn't matter unless you are using constructors or modifying some variables in the initialization expression anyway.
This takes the cake.