Remix.run Logo
jalospinoso 4 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.

safercplusplus 4 hours ago | parent | next [-]

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

uecker 6 hours ago | parent | prev | next [-]

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.

spacechild1 an hour ago | parent | next [-]

I don't get your point at all. C++ has different casting operators (static_cast, const_cast, reinterpret_cast) that are strictly safer than C-style casts.

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.

skrebbel 4 hours ago | parent | prev | next [-]

I don’t understand your point at all, C++ objectively has a much stronger type system. It’s turing complete!

I’m not arguing that that’s better, or worse, but it’s definitely true and by no means a myth.

lelanthran 3 hours ago | parent [-]

I don't think GP meant "it's completely made up", I think he meant the distinction doesn't matter most of the time.

I.e. most of the time the typing in real C++ code isn't meaningfully stronger than that found in C code.

uecker 2 hours ago | parent [-]

More complex != stronger. A weak type system would imply that the type systems forgives type mismatches. C had this before it imported prototypes from C++ where you could call a function without declaring it first and if you got it wrong you got some error or crash. The only part where I think C++'s type system is meaningfully stronger than C are enumeration types.

flohofwoe 4 hours ago | parent | prev [-]

> The void pointer rules are better in C IMHO as they avoid unneeded casts

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

TuxSH 4 hours ago | parent [-]

> It shouldn't require casting from and to specific pointer types

You don't need to explicitly cast T* to void* (guaranteed to be safe), you only need to cast when converting out of void*.

The rules are basically the same as casting between pointer-to-derived-class and pointer-to-base-class and they make sense.

uecker 2 hours ago | parent [-]

They make sense but reduce type safety, because once you add the cast the case might hide some real typing issue. I sympathize with the idea that the down-cast should be explicit though.

spacechild1 an hour ago | parent [-]

> They make sense but reduce type safety

Yes, downcasting can be unsafe and should be used carefully, but what's the alternative? At least in C++ you can't cast between unrelated types without an explicit reinterpret_cast (or C-style cast).

chrchang523 35 minutes ago | parent [-]

and you CAN use static_cast to convert from void*; this silently keeps working if you refactor the void* into a matching-type pointer later, while raising a compilation error if you refactor to a different-type pointer.

uecker 11 minutes ago | parent [-]

Yes, a static_cast would be safe, but then most C++ seems to use a C-style cast because it is less clunky and then is less safe than the corresponding C code. The issue is not that the downcast is unsafe (it is, but once you have a void pointer you already accepted this), but that it becomes even less safe by adding a C-style cast.

It is also not clear what is gained by forcing programmers to add a cast. Void pointers should be used sparingly anyway.

acontopo an hour ago | parent | prev | next [-]

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

hgs3 9 hours ago | parent | prev | next [-]

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

pjmlp 9 hours ago | parent [-]

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.

gritzko 7 hours ago | parent | next [-]

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

pjmlp 7 hours ago | parent [-]

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

uecker 6 hours ago | parent | next [-]

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?

pjmlp 2 hours ago | parent | next [-]

It is, however hardly something unique to C, as the C crowd pretends it to be.

shakna 6 hours ago | parent | prev [-]

Might more mean that we've standardised on a few things like what a byte even is.

The PDP-11 had both 8 and 9-bit bytes. Thats a complexity that few programmers have to touch on, today.

elch 6 hours ago | parent [-]

IIRC PDP-11 was a 16 bit word machine with an 8-bit byte. Maybe you remember PDP-10 with 4x9=36 bit words?

zabzonk 18 minutes ago | parent | next [-]

Actually, if you were mad enough to use the feature, the Dec10 had 6-bit "bytes" - 6 to a word.

uecker 5 hours ago | parent | prev | next [-]

Anyway, I do not see how this affects the design of C in a way that makes no sense anymore today (except that one could require CHAR_BIT to be eight, but there are still DSPs where this is not the case). I think people repeat the "the C design reflects the out-dated PDP-11 hardware" meme because it sounds smart while in reality it is just nonsense.

pjmlp an hour ago | parent [-]

So when is WG14 standardising modern hardware into the C standard?

Basic stuff like SIMD, SIMT, without requiring users to go beyond language extensions, something that any programming language can offer in similar capacity?

uecker 10 minutes ago | parent [-]

Ah, the "what is not standardized does not exist" argument again.

shakna 3 hours ago | parent | prev [-]

On the 11, the UNIBUS was 18 bit, the program space was 16 bit, and addressing was 22 bit. So it depended if you were using I-space or D-space.

flohofwoe 4 hours ago | parent | prev [-]

The PDP-11 myth is getting a bit tired by now ;)

If C would be so hardwired to the PDP-11 architecture it would have died with it. In reality C works just fine on all sorts of hardware (like GPUs) with only minor extensions.

pjmlp 2 hours ago | parent [-]

Just like plenty of other programming languages.

I am also tired that language extensions in C to work around ISO defencies is considered an advantage when argued by C folks, while at the same time it is considered a language design fault when the same crowd points to other programming languages.

jstimpfle 8 hours ago | parent | prev [-]

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.

pjmlp 7 hours ago | parent [-]

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.

jstimpfle 7 hours ago | parent [-]

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.

pjmlp 7 hours ago | parent [-]

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.

uecker 6 hours ago | parent | next [-]

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.

pjmlp 2 hours ago | parent [-]

Why should it be, the language doesn't expose any of it.

jstimpfle 6 hours ago | parent | prev [-]

such a strawman again... You don't want to be writing explicit platform specific SIMD most of the time. You just want to write a dumb function that doesn't do any non-obvious calls, doesn't cause thread contention, doesn't hide complexity, isn't going to be a nightmare to change later, no surprises.

I am talking about self-inflicted complexity that is entirely within the C(++) machine model. Avoid that complexity and you're pretty good already. Only drop down to concrete hardware arch level where it makes sense. But largely, the C machine model is still very much suited as a model for actual hardware. Writing straightforward obvious code allows you to stay in control of memory layout and the data transformation paths. It easily gets you within <<2x of what you could achieve with hand coded assembler for the >90% of the code that are pretty boring and straightforward. And obviously you couldn't get the work done in time when coding everything in assembler.

pjmlp 2 hours ago | parent [-]

So abstractions are desired then?

I have seen plenty of self inflicted complexity in C, starting in the golden age of Yourdon Structured Method, and all those libraries that replicate C++ basic features with preprocessor macros.

jstimpfle an hour ago | parent [-]

Most of these methods and ideas turned out to not work, neither in messy C libraries, nor in C++ or other languages.

pjmlp 9 hours ago | parent | prev | next [-]

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.

m_mueller 5 hours ago | parent [-]

I take it you probably never tried to use any of these languages for HPC. Without a language standard, you have two options there to compile decently performant executables: (1) compiler pragmas, (2) give up and drop to assembly code.

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.

pjmlp an hour ago | parent | next [-]

Long time ago at CERN.

There are some ATLAS TDAQ/HLT papers with my name on them.

Template metaprogramming, multi-threading, and custom IP protocols where much more relevant.

einpoklum 4 hours ago | parent | prev [-]

Can you link to some writeup regarding how Fortran is preferential to C++ (or rather C++ plus compiler `__restrict`) in this respect?

m_mueller 3 hours ago | parent [-]

I keep looking around and not finding any, so let me just try here before someone just takes it and slopifies it:

* built-in multidimensional arrays with efficient storage.

* related to this: built-in array intrinsics

```

real, dimension(100,100) :: A, B, C

C = A + B

```

this kind of code is already a close-to optimal "naive" implementation (not considering parallelization). so you start already at a solid place. then you can easily run it in parallel without too much specialized knowledge with OpenMP, OpenACC, MPI or even CUDA. the only thing you really need to be aware of when implementing your own loops/kernels: the intrinsic storage order, to optimize for cache hits.

* crucial: all the above amounts to a standard/best practice about how data is structured and formatted. everyone just uses the built-ins. Thus, interoperability between native Fortran numerical libraries is usually a complete non-issue. Meanwhile, Cpp has a fractured ecosystem with different array/vector types for its libraries. Converting between one and the other is usually a no-go.

* next, the intent plus pass-by-reference system. it combines IMO the best of both worlds of a functional vs. procedural approach:

  - I can predefine if an array / variable is intended as input, output or both, simply with `intent(in)`, `intent(out)` etc.

  - compiler can thus do checks for me if I'm breaking a contract.

  - yet, since I pass by ref, I don't have to worry about memory not being used efficiently. This really matters once you deal with GB, TB or even PB worth of data going into a simulation over time - there's just no way to deal with that in a purely functional way.

  - only where really necessary, you can still drop down to pointer semantics, e.g. for the outer glue code of a simulation that swaps outputs and inputs for the next time step.

  - having `restrict`-by-default semantics here helps immensely. Imagine you have many arrays with lots of data to deal with, and your kernels access always several at a time to do calculations. In Fortran I can write it intuitively, while in C/C++ I must remember to specify `restrict` or to preload all point-inputs first into separate variables to direct the compiler. Otherwise the memory pressure increases, and by far most such physics simulations are memory bandwidth bound - every access counts.
* finally, a clean symbol definition system that decouples types from byte lengths. a `float` in fortran is just `real(4)`, a double is `real(8)`, a long int is `integer(8)` and so on. now, it's trivial to do a bit of preprocessing to switch the precision.

However, the last part is where Cpp has a strong advantage: Well supported meta-programming (generics, templating or even just well supported pre-processors). Fortran's compilers come with a lot of built-ins, so the lack of these is less of an issue than you might think, but it's still a limiting factor. All that being said, a typical scientist doesn't tend to care and just wants to solve a particular problem rather than thinking in generalized frameworks - and that's why I find Fortran still serves them better for numerics than anything that came since.

crest 2 hours ago | parent | prev | next [-]

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.

spacechild1 2 hours ago | parent [-]

What's the "sequence point operator"?

flohofwoe 5 hours ago | parent | prev | next [-]

> 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.
aw1621107 9 minutes ago | parent | next [-]

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

How did Clang handle differences between member declaration order and the order in which initializers appeared?

fuhsnn 2 hours ago | parent | prev [-]

> IMHO this "schism" was completely unnecessary and only happened because of ignorance and hubris

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

Retr0id 7 hours ago | parent | prev [-]

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.