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.
[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.
Edit: I should've had more conviction in my instincts, this is slop.
- 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
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.
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."
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++
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.
It doesn't matter unless you are using constructors or modifying some variables in the initialization expression anyway.
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.
?: has another execution priority.
Implicit cast scenarios are reduced in C++.
This takes the cake.