C constructs that still don't work in C++
36 points
3 days ago
| 10 comments
| lospino.so
| HN
mjs01
2 hours ago
[-]
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

reply
jalospinoso
3 days ago
[-]
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.

reply
uecker
5 minutes ago
[-]
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.
reply
pjmlp
2 hours ago
[-]
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.
reply
hgs3
2 hours ago
[-]
Not sure if you're aware, but defer is proposed for C2Y [1]. It's already available in Clang behind a compiler flag. It is interesting how the languages continue to diverge.

[1] https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3734.pdf

reply
pjmlp
2 hours ago
[-]
Because the communities aren't the same.

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.

reply
gritzko
1 hour ago
[-]
That is the entire point, yes. Reasoning about layers of completely imaginary entities is what demotivates me about C++ and Rust. Meanwhile, hardware bits are very real (and getting more expensive recently). Having implemented slices and generics in C, now C++ feels like Vietnam flashbacks.

https://replicated.wiki/blog/abc

reply
pjmlp
58 minutes ago
[-]
Yet C23 isn't K&R C any longer, nor is the hardware a PDP 11.

Also when we eventually start talking to agents that perform the whole execution steps by themselves, that is kind of irrelevant.

Except for the lucky ones that still code to keep the infrastructure going, which is mostly C++.

reply
uecker
11 minutes ago
[-]
The "nor is the hardware a PDP 11". Byte access was the main new feature of the PDP 11 that C adopted. Are you saying being able to access individual bytes is not relevant on modern hardware?
reply
jstimpfle
1 hour ago
[-]
Caring for the actual assembler output in selected critical pieces of code is not the same as ignoring the abstract machine model. What you claim is simply not the case if you check actual proficient systems programmers. Of which there are an astonishingly high share C and C++-but-mostly-C programmers.
reply
pjmlp
1 hour ago
[-]
Any user of compiled languages cares about Assembly, which is why regardless of the compiled language, an Assembler was always shipped alongside.

Also it isn't a C invention to have the compiler dump the Assembly output instead of object code.

Now the culture that C language constructs in 2026 are still 1:1 to Assembly instructions, that pretty much prevails, despite easy proof that isn't the case at various compiler optimization levels.

Proficient devs, well many still don't know to distinguish what is their compiler, and what ISO says.

reply
jstimpfle
57 minutes ago
[-]
It is the case that you can more easily know what happens when you don't use the wrong abstractions but stay in control. Highly-abstracted C++ code basically makes allocations and syscalls in the whitespace between the source code tokens. You can't do systems software like that, you have to roll back the abstractions and roll back the use of pre-canned containers and libraries that you don't understand.

So it's all about understanding and control, not about some idea that C was defined in terms of assembly instructions, which it obviously is not. That's a total strawman.

reply
pjmlp
25 minutes ago
[-]
Except modern C also has plenty of abstractions, devs wrongly assume it doesn't.

Then get surprised when it doesn't map to the SIMD/SIMT NUMA machine their code actually executes on.

reply
uecker
9 minutes ago
[-]
There is not much real evidence for "devs wrongly assume" and as someone writing numerical code (clusters, NUMA, SIMD, etc.) I think C is still the ideal tool for this.
reply
Retr0id
1 hour ago
[-]
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.

reply
mike_hock
1 hour ago
[-]
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."

reply
Panzerschrek
1 hour ago
[-]
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.
reply
arcadialeak
27 minutes ago
[-]
> std::string_view or std::span should be used instead

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.

reply
p0w3n3d
1 hour ago
[-]

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

reply
AdieuToLogic
2 hours ago
[-]
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.
reply
electroly
1 hour ago
[-]
30 years ago, in C89 and pre-standard C++, it was the case that `int foo()` in C is a function that accepts any parameters, and in C++ it is a function with no parameters. In C89 you have to write `int foo(void)` if you want no parameters. This counterexample to C++ being a superset of C was well-known even back then.

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.

reply
pjmlp
2 hours ago
[-]
Already in C++98 there were differences.

?: has another execution priority.

Implicit cast scenarios are reduced in C++.

reply
Pay08
33 minutes ago
[-]
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++.
reply
uecker
7 minutes ago
[-]
You can just run it through a compiler in c++ language mode.
reply
dhruv3006
2 hours ago
[-]
> restrict: a C promise, not a C++ contract

This takes the cake.

reply
grougnax
1 hour ago
[-]
At this point, just give up c++ and use Rust
reply
grougnax
1 hour ago
[-]
Give up C++ and use Rust
reply