32 comments

[ 3.2 ms ] story [ 44.6 ms ] thread
I wrote this after repeatedly seeing experienced C programmers hit the same sharp edges while moving into modern C++ codebases.

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.

I appreciate that restrict isn't there, because it is yet another UB source, programmer knows not to do errors kind of attitude, and secondly no one seems to care enough to write a language proposal for it.
Did you use an LLM to write this comment? (I don't mean this as an accusation, I'm uncertain. I'm just trying to calibrate myself.)

Edit: I should've had more conviction in my instincts, this is slop.

The "stronger type system" is mostly a myth in my opinion. It was true in the past in pre-prototype C. The void pointer rules are better in C IMHO as they avoid unneeded casts (that then remove more type safety) and FAMs and variably-modified types can express things C++ simply can't do well.
> I wrote this after repeatedly seeing experienced C programmers hit the same sharp edges while moving into modern C++ codebases.

...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.
Because Microsoft has been focusing on improved C# for low level coding, see recent update on memory model changes for C# 16/.NET 12 roadmap, Rust adoption, and good enough C++ support.

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.

You might be interested in the scpptool feature to help convert C code to a subset of C that will also compile as C++ (under clang++ at least) [1]. While many of the necessary modifications are fairly trivial, some of them aren't completely so. For example, C++ does not allow `goto`s that would skip over the declaration/initialization of a variable that would be accessible after the jump. So getting the C code to work as C++ can involve some (automatic) code restructuring.

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...

About half of them read as "I tried to use C++ as a worse C" e.g. using struct initilisation instead of constructors, using malloc instead of new or new[].

My pet peeve with C++ is that the sequence point operator can be overloaded at which point it stops being a sequence point.

> isn’t "C vs C++,"

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.

From the article:

  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.
Some unmentioned incompatibilities I've encountered that makes a C header not directly usable in C++:

- 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

> restrict: a C promise, not a C++ contract

This takes the cake.

(comment deleted)
The thing with the flexible trailing array member is a C++ design flaw. Now the fix wouldn't be to allow those "flexible arrays" in C++, at least not the way C has them, but it should have a concept (not that kind of concept) of types that are indeterminately sized at compile time and whose size is determined at construction.

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 this

However C++ had at time no default initialization for unmentioned fields, so in 2017 I had to remove it when converting the code to C++

Yeah you can even create linked lists of static data that way, as you can very well just take the pointer of an object here.
restrict in C++ can't work well. One can mark pointer parameters with this attribute, but in C++ it's not recommended to pass raw pointers, std::string_view or std::span should be used instead, but there is no way to specify restrict for the internal pointer of these containers.
Is there some sort of tool that checks headers for this stuff? On the occasion that I write a C library, I prefer it to be directly usable in C++.
Designated initializers is one area where C feels much more expressive than C++. And that feature has been standard since C99.
I think the problem is that C++ is a poorly designed language with a fundamentally flawed development process.

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.

https://en.cppreference.com/cpp/compiler_support

https://cppstat.dev/

Man this table is disturbing. "Same", same as?
You can pass a flag to clang to allow reordering field initializations in designator initializer thing. It makes the syntax super annoying in case of large structs anyway.

It doesn't matter unless you are using constructors or modifying some variables in the initialization expression anyway.

I find variable-length arrays (i.e. arrays whose length is defined at run-time and typically live on the stack) to be kind of dangerous, and try to avoid them, even in C.
Why do you think they are dangerous?
(comment deleted)
Please stop using c++. It has a way too complex syntax creating a hard dependency on beyond reasonable "real-life" compilers and runtimes.
Is there some kind of ambiguity inherent in designated initializers such that the field ordering requirement is necessary for C++ or is it rather just a quirk of the compiler authors’ choices?
C++ the good parts is essentially C. If C had the concept of traits and generics or something like them like Rust there’d be no need for C++ and its ideas of mediocre inheritance and macros (templates).