362 comments

[ 2.7 ms ] story [ 330 ms ] thread
I can't tell if this is serious.

Surely if you're still using C now it's for legacy reasons. And if using it for legacy reasons you are likely stuck using an old C standard.

So who will use C23?

There are many many people out there using C for embedded software.
The problem is that embedded compilers are not known for keeping up with the standards. The only hope is that the moves to GCC and Clang continue.
No one cares about the standards. We only care about getting things done by writing regular C code that works.
Huh? What do you think defines “regular C code that works”? Standards.

Every compiler you have used operates at a bare minimum on the C standard. That’s why they are often advertised (if not GCC/Clang which are assumed to be up to date) as “C(89|99|11|17) compliant.” “Code that works” is code which is operates under constraints and guarantees specified by a standard, anything else is undefined or unportable.

Embedded environments are specific and esoteric enough that compiler portability is the least of your concern when migrating code from one platform to the other.
Only the standards people care about standards. We (programmers) want to just get things done. We don't care about the standards. No one reads the standards. Also no one cares about portability. All computers have been the same for the last 20 years.
We, developers, care about standards, because we want to get things done once.
(comment deleted)
(comment deleted)
Right, then comes the day another OS or CPU needs to be supported and some clever developers learn their lesson about language standards during a couple of late nights tracking down production issues.
Seems way more efficient to not care about standards and invest a couple late nights once every five years.
Exactly. You are the real MVP in this thread. We should team up.
I'll rather have code that runs on an actual computer than code that should hypothetically run according to a long document that costs 60 dollars to read.
Exactly. You get it 100%. We should work together.
gcc (thankfully) is the default (you download gcc compiled for ARM directly from ARM itself) for ARM embedded so when gcc gets it, ARM gets it.

So, presumably, RISC-V will also get it at the same time.

There are tons of CPython users AKA Python's default implementation... the software world is full of high level abstractions built on top of C.
We all use C because it works and we love to get things done.
Not all of us use C, because it's very easy to create security holes in C. For example, web browsers are using slow JavaScript just because it memory safe.
(comment deleted)
C is a portable alternative to assembly language.
When the world is a PDP-11.
And x86, Arm, PIC, Xtensa,... and everything else.
There is where you went off rails, with greetings from ACM.

https://queue.acm.org/detail.cfm?id=3212479

I'm so glad that you have linked this article from Chisnall. I can now ask you in what ways is the abstract model of a machine on which assembly language operates on, different from the one on which the C language operates on?

This is the claim which started this thread:

>>C is a portable alternative to assembly language.

C's abstract machine is leaky and full of surprises when UB comes into play for one.

Second when people talk about C being portable Assembly, they mistakenly assume to know what comes out of the compiler's backend.

Third, unless we are speaking about PDP-11 like CPUs, modern CPUs have tons of capabilities not exposed to ISO C.

Finally, since 1958 there are portable alternatives to Assembly in systems programming with JOVIAL being one of the first remarkable ones, yet another thing that C did not invent.

You haven't answered the question?

Maybe because is there is no concernable difference between abstract machine models of both languages? You can't do anything more about instruction reordering or cache invalidation at the CPU level with assembly than you can with C.

You surely can, better read those CPU manuals about line bus controllers.
Have you? I used to write device drivers mostly on ARM architectures and I have newer seen an instruction which would let me precisely control CPU instruction ordering or what it does with cache that I couldn't do in C.
That is why stuff like V-Tune exist with micro-ops profiler and pipeline debugging, maybe ARM should provide better tooling.
This tool helps as much with C (or even higher level languages) as it does with Assembly. It doesn't change the fact both languages work on the same abstract machine model.
What is C's abstract model for NEON?
A simple c library:

https://github.com/projectNe10/Ne10

You have not yet shown an argument for why c is not a portable assembly.

Access to CPU-specific flags like the carry flag. When doing a multiple-precision addition, if the CPU carry bit is inaccessible, then it must instead be compared to the result to see if it overflowed, which takes 3-5 more instructions per limb or worse on modern superscalar processors that are serial in terms of data accesses.
Portable by definition means it cannot capture every specific detail of every architecture out there.
By definition there isn't such thing as portable Assembly.
(comment deleted)
Not by definition, but by practice as generating assembly is usually done to optimize performance. Languages restricted to generate a common feature set between isomorphic ISAs and bijectively associated assembly languages are at a performance disadvantage compared to the full ISA specific assembly.
The carry flag is not a specific architectural detail; it is part of the output from the prime example of a basic electronic circuit that is the digital adder.
It is an architectural detail. MIPS for example doesn't have a carry flag. You have to do the exact same procedure in assembly on MIPS you have described in your previous comment when doing multiple word addition.
MIPS along with DEC alpha do not have adder circuits as a design goal was not to have flags. This along with the scenario of multi-precision arithmetic resulted in the implementation of multi-precision arithmetic on MIPS and DEC alpha using a form of branch like instruction with an ALU stage instead of the unavailable instructions dependent on the alternative adder circuits. The choice of having or not having a carry flag alone is not an architectural detail as the choices of [micro]architecture involve how operations are implemented in circuits/hardware. The carry flag specifically has to be considered in the context of its [the adder] circuit when making architecture decisions. The lack of control over flags leading to the inability to compile the same C program to the same assembly operations with same behavior on different hardware demonstrates that a strictly C program is unable to express a consistent portable assembly program across all hardware.
You are basically arguing CF == !CF. On MIPS you literally have to compare your result with an operand in order to establish if there was an overflow. On Arm you just look at the status register.

When I was doing embedded development, I used C as portable assembly in practice and I know I wasn't the only one. Can you do everything an architecture enables you in a portable way? No, of course not.

I don’t see my SIMD instructions anywhere.
The word "portable" means a subset of everything possible. Do you think every CPU architecture supports SIMD?
> The word "portable" means a subset of everything possible.

Exactly; for example, C has single precision and double double precision floating point, 64 bit types, and complex arithmetic because every CPU architecture has those built-in.

No, but those things can be easily emulated on every hardware or simply not used. One more reason to use C instead of plain assembly.
Where's my overflow flag?
Not all CPU's have overflow flags. On those that have them, you can also usually access them in C in a non portable way.
C23 will get support for checked integer operations that report overflow.
At a previous employer of mine we would have happily switched to c23. Postgres is written in c and if you want to write an extension for Postgres you're going to have the easiest time if your extension is also written in c. However, there's no reason not to use the newest c version.

So, c is not just for legacy code.

I will.

C is everywhere and is not going away (it's #2 on TIOBE and on the rise again). Now that fewer people are learning it, I get more juicy contracts.

I feel like there’s actually a growing subculture of C programmers of late, seemingly spawned by Casey Muratori’s Handmade Hero series, Zed Shaw’s “Learn C The Hard” way and other more recent popularizations of C and low level programming ideas. There’s a whole “Handmade” software scene growing up, seemingly as a reaction against the bloat and over engineering of more recent software trends like Electron and web apps. Just the other day I stumbled across a vibrant Discord group focused on a fairly new but surprisingly feature complete game engine called Gunslinger, written entirely in C99. I’ve been working on my own C-based game engine for awhile too after having been exposed to the ideas of Handmade Hero, as well as from the influence of a good friend who has been writing games in C for many years. He’s now working on an application called Pixel CNC for generating 3D tool paths from 2D images.
> seemingly spawned by Casey Muratori’s Handmade Hero series, Zed Shaw’s “Learn C The Hard” way

It's kind of unfortunately, really, because both authors have very skewed viewpoints of C that are arguably quite incorrect.

Out of curiosity: why do you consider Casey's viewpoint incorrect?
Casey doesn't really understand how optimizing compilers work, so he likes to talk about how they keep breaking his code and that they should just run the optimizations that won't do that.
What viewpoints are those, for those of us unfamiliar?
Zed Shaw doesn't really like C because it lets you shoot yourself in the foot. This is an OK opinion to have, but then he goes and writes books about C that basically talk about how the language is bad (ok, I guess?) but the then claims he has solutions to these problems, without understanding that the problems he has with C are not solvable within the language (for example, when handed an arbitrary pointer, there is no way to tell whether it is valid).

Casey Muratori is fundamentally at odds with how undefined behavior works in C and has opinions™ on how to fix the language to make things work the way he wants them to, when in reality he just probably wants another language.

C++ requires to devote your life to it but you can learn C and spend a year using it and suddenly you have a dangerous but powerful tool in your toolbox forever.
It's not like C++ is always the only alternative, see e.g. Rust which is way simpler
I agree that Rust is simpler than C++ (in some ways), but I’d say it is far more complex than C.
That depends on how you define complexity. It is far easier to write a working program without memory leaks, various types of vulnerabilities, and undefined behavior in Rust than it is in C.
It depends on what level of security you need. If you're writing a 3D game engine, a minor memory leak probably isn't worth the extra time required to write it in Rust. Even if you're writing a kernel, fuzzing and formal verification can get you 99.999 of the way to the guarantees of Rust.
I feel like writing Rust is a whole lot faster and less error-prone than writing something in C. But even assuming that it's slower, it's definitely much faster than writing C and doing formal verification. Naturally formal verification can help with other properties besides memory safety, and as far as I am aware Rust is still lacking a formal semantics so when you go all-in on formal methods C can reach a provable level of quality that Rust currently cannot. But very few people develop software that way.
C is probably still popular, but TIOBE is less accurate than a horoscope.

TIOBE index shows C has lost 62% of its users in 2016-2018, then almost tripled in popularity next year, and then again lost almost half of its users.

Do you think such swing of popularity is possible for such an established slow-moving language? I don't think so — it just shows how far TIOBE can diverge from reality, and that the error bars on the index should be +/- 50%. A few % blip means nothing. TIOBE is measuring rollouts of new Google search algorithms, not language popularity.

Huge amounts of software are “legacy” but built with perfectly modern tool chains and will support c23 just fine. Even for large bits of software which have been mostly rewritten you’ll often find core libraries that are still actively developed but have never been ported away from C.
Sounds like you're not really in touch with the real word. C has many benefits and it is super practical. Yes, you can write unsafe code, but safety isn't always your #1 priority. There are also tools you can use to identify issues, which will mitigate risks.
I'm really starting to believe that there's a cohort of modern programmers that only see programming languages in terms of the high level syntactical features they provide.

The logic of it is something like: I write the code, I run a command written using similar code, scenes deleted, my program runs and my IDE tells me if there are problems in my code.

This is sometimes defined as the difference between developers and engineers, but I don't really buy the analogy because they're mostly highly competent and able to build solid and principled architecture into their software.

This is the only reason I can summise that educated programmers would say something like C is irrelevant.

I guess not everyone needs to have a depth of understanding of the platforms that underly their languages of choice, but - as demonstrated here - it certainly doesn't inform the future of languages when they are inherently tied to their ability to integrate with different hardware and OSs.

It was already irrelevant when using MS-DOS with Turbo Pascal/Modula-2/Basic compilers, or Mac OS (originally written in Object Pascal/Assembly).

It was UNIX industry adoption, followed by GNU Manifesto that made to more relevant than it should ever have been.

So, it's not relevant, but then it's more relevant?

Well, sure, merit and popularity rarely align, and one with enough technical culture can always imagine a better world.

The truth is, C is merely a historical artifact, one step above the PDP-11 assembly, created in order to make the UNIX source code portable. It has transcended its original purpose, and with it being elegant and simple as it is, I would still never trust it to run a pacemaker.
Many things on a Linux system is written in C, and despite the noise on HN, developers who gets stuff done are constantly writing new code in C all the time.
Including plenty of new CVEs, we have to keep the security industry job market safe.
Sure, I'm certainly not arguing that C is a good language. In RHEL we turn all warnings on, compile everything with hardening flags, use valgrind during development, and feed everything through Coverity.
The exception that confirms the rule.
The exception? It's standard practice for distros to compile with hardening flags, and if RH submits the bugs they find with Coverity upstream, that protects the 1000s of C projects they package.
Mozilla is not a Linux distro.

You have made dozens of negative (and sometimes rude) comments in this thread. I'm curious why you're spending so much time and energy being negative. If C isn't something you're interested in, why not just ignore it and move on?

> Mozilla is not a Linux distro.

That's true. But should I interpret this to mean, "All that stuff about how careful we are doesn't actually apply to any third party software, we only do that for code we wrote" ? Because the thing about a Linux distro is that it's overwhelmingly third party software.

If that stuff does apply to NSS, then all that hard work apparently missed this pretty serious bug that could not have happened in a safe language.

C powers lots of junk where I as a user don't have a choice other than put up with security mitigations.

Being rude, yeah that is how people defend themselves when reality doesn't check.

> In RHEL we turn all warnings on, compile everything with hardening flags, use valgrind during development, and feed everything through Coverity.

Surely the compile time is not 100ms anymore then?

> developers who gets stuff done are constantly writing new code in C all the time.

The amount of new C projects being created is falling at a fast rate. C isn't going anywhere, but it's certainly losing rapidly its prominence in the systems programming area.

[citation needed]
You can start by C compilers nowadays written in C++, Arduino, AUTOSAR security standard, CUDA, DirectX, Metal, GoDot, Vulkan helper libs, SYSCL, ...
Those are just headlines, is there any evidence that those are actually overtaking C, and that the C++ used isn't just "C style C++"?
Doesn't matter, if the code doesn't compile on a C compiler, it is C++.
> So who will use C23

I can't say if I'd immediately jump to C23 but I'll be thrilled to give it a try.

Currently I'm rewriting an entire automotive OCPP charging point and it's associated CSMS in C because the original dev (who has no experience with Edge/IoT and just Cloud/SaaS) thought it was a great idea to implement this in golang. The binary size alone is already a problem where all I have is 30MB (needs to be shared with logs, sqlite, and 2 another "user-space" application).

I could have chosen Rust/Zig/Nim (I really wanted to) but the client rejected it for very good reasons that have nothing to do with type-safety/performance/security but are all about good-business practice and risk management:

- when I leave in a few weeks, there are plenty engineers inside the company who can maintain the system.

- the domain is quite niche so to understand their solutions and requirements you can't do away without an occasional trip to their office. the client is not in a capital city with a vibrant tech-scene and weekly meet-ups and lacks the talent-pool.

The constraints by the market (scarcity) makes Rust/Nim/Zig (my personal front-runners) very risky tools.

C++ could have been an option but it's total overkill because most of the components you need to build this (websocket, mqtt, http, sqlite, zlib, openssl, ...) are anyway implemented in C and I actually don't want things like exceptions or templates in this _at all_ for performance reasons.

If you split your OS into layers from HW (ring-0) all the way to layer-5 (process management e.g. systemd etc) then 99% is still written in C. So your argument throws every layer under the bus except user-land (L-6).

C isn't going away only because of embedded/IoT. It's going to stay because choosing a language is like the "3 most important rules in real-estate: location. location. location".

The language is your location and your primary lock-in factor for your solution/business. Once you've decided on language the only way to change this is an entire re-write. Even if you rewrite you still have to consider culture and available resources that must be in place long before you chose the programming language. And keep in mind the people who all took stakes in the original implementation and must all be sold on the idea that whatever new replaces the old "is sooo much better". That's not an engineering problem but a power/people problem.

Edit: So just because "the engineer inside of me" wants to use Rust/Nim/Zig or whatever new to satisfy my intellectual curiosity is not enough reason. In fact my potential inability to see beyond that is a good reason for them to be very very careful about what advise I give.

Is it a good summary that the only reason for deciding for C instead of Rust/Nim/Zig is that not enough programmers know Rust/Nim/Zig?
imo not the only reason but it's the most cited one by those who give me a budget and limit my freedom to use the tool I personally think is right for the job. just because I can sell an engineering audience on my (IMHO correct but artisanal) views doesn't mean it's what is best for the business.

Because they know the moment they come back "crying" saying "we can't find anyone with these skills" I'll consider it as their problem, because I for sure am not going to help sifting through LinkedIn profiles or get involved with their "talent acquisition strategy" (at least not in the capacity of an architect/engineer)

I'm not saying always use C but if you have a very narrow skill-set in your team then switching languages is a long-term strategic decision that goes well beyond the opinion of the SW-teams. It's about strategy, budget, risk. That's probably true for most new languages / tools that replace the status-quo and not just about C vs Rust&Co. If I bring in a new technology then I'd first use them in parts of the system that aren't core-business (e.g. my infra automation etc) and then gain experience slowly with them before considering allowing them into parts that make up my core-business. It's a long term project to adapt a new technology not something a few devs decide. (start-ups switching from MVP to real products might be an exception, as are bigger companies switching from PoC to a real implementation, etc.)

Well then "legacy reasons" cited by GGGP was actually right. That's what I was wondering as well.
From my personal experience, at work we use C and not Rust because for really low-level code (e.g., kernel bypass, niche datastructures, real-time, etc.) I believe Rust would have to use "unsafe" due to work with raw pointers and other code that can't be easily, or at all, expressed otherwise. In such a scenario a lot of the advantages Rust supposedly brings are diminished or gone.

When that happens, considering that our company's knowledge/experience with C (or C++, although not my case) is vastly deeper than with Rust it makes no sense to choose it for any of that type of code we write, old or new.

Zig looks promising but it's still too soon to take any risk in adopting it; in 5 years time it may be worth revisiting it and see how it has matured...

Finally, software we may write but that gives us no competitive advantage over other businesses, sure that can be in Rust, Go, Java, Python, whatever.

C is the best choice if you want all of: small (both language and binaries), fast (both compiler and binaries), obvious (no/minimal complex magic), close to the metal, with excellent debugging support, portability and integrations.

No other language has been battle tested for longer and more extensively than C.

Your kernels, OSes, drivers, databases, web servers and compilers are written in C.

If some of these features are not important to you, there are hundreds of slower, more complex and less portable languages to choose from, that provide other benefits instead, such as more convenience, more correctness and higher level abstractions.

Languages exist that compile to C which gives them many of the above advantages and more, so why choose C over them?
Having never used such a language, I'm curious, what's the debugging experience like? Can I source level step the application in the original language or do I have to debug the generated C?
You can put in line pragmas for e.g. gcc that will point you from "foo.gen.c" to "foo.fancylang". However it is not very useful unless the fancy language matches C's structs and native types and also does not mangle names.
It is always a pain to debug transpiled C IME.
One of the criteria is "excellent debugging support" which you don't get with transpiled languages, since you'll be buried in generated code when you open the debugger.
Languages like Eiffel prove this isn't the case, it is a matter of proper debug tooling.
Of course, Eiffel is such a popular and widely used language, with a great toolset. /s
The amount of users doesn't change the fact of the tooling quality, but no wonder, UNIX folks tend to only take free beer, so what do they know about fine wine.
I've read the Eiffel books, and tried to use the Eifell tools, on Windows - a horrid experience. I am not a "UNIX folk" and I can't imagine what comparing UNIX and Eifell has to do with anything. It is basically an unuseable programming language and toolset, promoted by an egomaniac with a grudge against C++.

If you disagree, please provide a link to something medium sized and useful written in Eifel.

Useful to whom?

To you or the companies that keep Eiffel Software in business for 30 years?

> the companies that keep Eiffel Software in business for 30 years?

Such as? If you can't provide links to people using Eiffel succefuly, then maybe there are none.

Control over the code that actually runs.

We already lose quite a bit of control with C (e.g. it reorders your code as it pleases) to gain portability across CPUs, otherwise we'd have to rewrite the code in several assembly languages.

If you consciously choose to give up even more control over the code that runs (because a compiler that targets C necessarily adds another layer of autogenerated code that you don't control) then it better be a wise tradeoff.

The downside of reordering being performance? Or something else?
Performance is usually the upside. The downside is subtle bugs that can happen due to side effects of the statements being reordered between sequence points.
AFAIK compilers don't reorder if it would change the result.
Compilers not allowed to violate the language spec when optimizing. But the spec may be fairly generous in its allowed interpretations, which may not match what a programmer may naively expect. C, with its many undefined behaviors and implementation defined behaviors, is especially dangerous.
Depending on transpiler (compilers that will compile to C), you can get near representation in C (rarely) or something completely different (frequently). In Scheme/LISP world (where transpilers were popular for a while), even a simple expression like "(+ 1 1)" will rarely give you something like: "int a = 1 + 1;". I've seen these things would produce dozen of lines of various boxing/unboxing calls, type checks and GC protection marks. If the compiler is really aggressive, it will just put "2" as a constant.

I think that the only benefit of "compiling to C" was reusing existing compilers, not tooling around it. Also, debugging that code is a nightmare unless you are intimate with transpiler internals.

A compiler that translates to another language used to be called a translator.
All compilers covert programs in one language (say C) into another, (say assembler or machine code), and they are called compilers. The whole transpiler thing is a bit bogus, but a C compiler has never IME been called a translator.
C isn’t that small, compare it to a Zig hello world. Fast is relative — due to C not having good expressive/abstracting powers, it will leave you to inferior solutions, eg. counting string length multiple times at call sites, vs C++’s small string optimization, which is simply not possible in a user-ergonomic way in C. Regarding obviousness, I would add UB here, so Zig for example would beat it.

C is not any closer to the metal than other system level programming languages, this myth should just die. Your code at -O3 gets mangled to oblivion, unless you are a compiler writer of the respective compiler, you will have no idea on the generated code. This is the exact same as with C++, Rust, etc, hell, C doesn’t have proper simd support, so in a way the former two is closer to the metal.

But I do agree on portability and integrations - so C will not die, but I really have a hard time seeing why should I choose C over any of the listed languages, unless I target some obscure CPU architecture.

Are you going to mix all those languages into one project and somehow use their advantages but steer away from their disadvantages?

Sure, other languages have caught up or have improved on some of the features where C shines.

Let's remove portability and integration from the feature list, because that's strongly related to C's tenure.

Which one of the languages you listed matches the rest of the feature set I brought up?

  * Small language
  * Fast compile times
  * Fast binaries
  * Small binaries
  * Great debugging experience
  * Close to the metal
In my opinion, they all fail in at least one category, and that's expected - additional functionality can't come for free. You can only accept its cost.
And they fail in other categories too, e.g. C can be very terse whereas most modern so-called replacements tend towards verbose. For me that's a development exactly in the wrong direction; I'd rather have a more terse C.
Zig? I personally haven't used it, but based on what I've heard about it, it fulfills these criteria.
That’s what I am betting on - but it isn’t 1.0 yet, and that matters too.
In C it's possible (and, to some level, very much encouraged) to write your own things to replace whatever built in thing you don't like for whatever reason, be it speed, size, portability.

C not having fancy built-in structures means programmers are more careful about choosing simple ones, which dramatically cuts down the amount of completely pointless generated code. -O3 can optimize registers and maybe memory reads/writes, but it can't remove reallocation from each call to a vector push, remove a reference count from a whole object type, or do pretty much anything with any moderately complex heap data structure. Sure, that comes at the cost of more advanced things being horrible to write, but it's a trade-off.

And I'll just disagree that UB must be unobvious. To me, it's one of the most useful optimization tools. You can check for it with sanitizers, and explicitly invoke it to convey information about hidden behavior to a compiler. It's not magic.

> C not having fancy built-in structures means programmers are more careful about choosing simple ones

Resulting in linked lists everywhere which have pretty terrible performance characteristics. You should be worried about those way before the occasional vector push reallocation cause you any problem. (And you can specify initial capacity so there is that)

linked lists - true, but at least when you write them you'll be certain of what they do, and, coupled with knowledge of cache performance, precisely how bad they are. About reallocation I don't even mean the copying, but just the code needing to check for the chance overflow on every push. Besides the obvious waste of instructions to check for that, it also clobbers registers even if the branch isn't taken, and thus will easily result in a lot more unnecessary spilling to the stack. This kind of pointless thing happens in a ton of data structures.
Branch prediction exists so the common case can be made very fast (much faster than jump to this remote pointer), and also, rust uses an optimizing compiler for a reason. Eg., you use a vector inside a for, the vector’s code can be inlined and the checking part be done only at the boundary. Also, one can also manually perform an unchecked push/get/whatever as well.
The common case may be fast, but it's still slower than not having a rare case in the first place. The untaken branch will still affect register allocation (out of the ~15 x86-64 general-purpose registers, only 5 (6 if you count rbp) preserve data after a non-inlined function call in the System-V ABI (of which there will be at least one, ending in malloc somewhere), and the compiler has to accommodate both taken and untaken at the branch end) and will eliminate any hope of SIMD vectorization, among other wrecked optimizations.

Unchecked operations are acceptable if your code has a single hot loop, but if you have a hundred small functions, each taking 1% of the time, you probably won't carefully examine every stdlib function each of them uses and write code to "work around" every unnecessary thing the stdlib does.

Yes, that's a ton of micro-optimization, but micro-optimization can bring a ton of speedup, so I'll take whatever makes it simpler (or not needed in the case where you already know what will happen due to having written it)

Feel free to wrap it into a repr(transparent) type which exposes the unsafe operations by default.

But I seriously doubt that programs would benefit much from these micro-optimizations - there are rare and unfortunate cases where indeed there is no one single bottleneck for a program and thus there is no simple way to improve performance, but the vast majority of programs spend all their life in a tight hot loop, and anything else doesn’t matter in the slightest - hell, as mentioned C gets away with as many linked data structure as they want, but writing those in goddamn bash would suffice as well.

That depends on the project. There definitely are projects in which micro-optimizations such as this wouldn't be beneficial for one reason or another, but there definitely also are ones where they're make-or-break. For those, C is pretty good, and you're not fighting against the language (or the core intent of the language at least).

(I'd guesstimate that micro-optimizations would go a very long way in improving performance of nearly every program anywhere, but I don't have much data on that besides the couple projects where I've tried to care about performance, and having achieved pretty decent results)

I don't think I've ever used a linked list in 12 years of embedded C programming.
>due to C not having good expressive/abstracting powers

I disagree, if anything, C's scantness forces you to abstract things much properly, unless you plan to write pages of boilerplate code here and there.

With all due respect - how do you write a string library in C then? Char* is not one.
An appropriate data type (you could use structs maybe) and methods for the common operations, concat, length, etc...

It's not as trivial "string s" like in other languages, but it's also not that big of a deal. Also, you make sure it works properly and you just use it anywhere you want.

C certainly has been battle tested but I wouldn't say that it passed with flying colours. Every one of the examples that you cite has had bugs/crashes/vulnerabilities that would have been avoided with newer languages.
How many of your preferred programming languages are written in C?

Yes, many self-host, but a considerable number of them don't want the complications that can bring.

Modern C compilers are written in C++.

So much for C's simplicity in writing compilers.

Yes, C can be simple for writing a compiler. [0][1]

No, optimisation, on the other hand, is not a simple problem.

[0] https://github.com/rui314/8cc

[1] https://github.com/rui314/chibicc

Naturally it can be simple, it was already a basic language when compared with what 1960's have produced.

The evolution of Bootstrapping CPL compiler + a set of basic types.

There are plenty of C compilers written in C. Large compiler suites for C/C++ are usually written in C++. But as a contributor to GCC, I can say that large parts of the code still look very much like C.
Plenty of compilers... right.

If you cannot compile GCC with a C compiler, it isn't C.

There a couple of open source C compilers TCC, SDCC, tendra, chibicc, ACK, LCC, PCC, etc. and also many commercial ones.

It is true that GCC is not written in C anymore, but it is easy to see that the code is still relatively close. I am also myself writing a C compiler in C at the moment and there is no problem at all.

How many of those are actually shipping software into production?

Quite curious about which commercial C compilers, written in C, are being sold, keeping those companies in business.

Every year thousands of CS students write toy C compilers.

It will be hard to agree with you what "production" even means. I have written and shipped at least one compiler/interpreter (written in plain C). The language wasn't an issue at all. (This compiler doesn't have any meaningful optimizations, but I don't see why that should change the game).

GCC and Clang aren't "sold" either, nor is MSVC really.

I have never contributed to GCC, but as a fact I have visited plenty of files from its source code, and so far haven't encountered any meaningful use of C++ that couldn't easily be rewritten in plain C. The overwhelming majority of the code would probably compile as C without changes.

I'm sure that there are far more meaningful "problems" with the architecture of a compiler written in C that are not the implementation language. For example, GCC is a pain to build (IIRC when I tried many years ago, I gave up).

Trying to win a pointless argument based on pedantic logic that misses the point.
Usually the typical answer when facts aren't appealing, the other way around apparently being pedantic matters.
Obviously one should always argue to the point instead of picking an interpretation that is easy to refute using "logic".
I will continue to use C because I haven't seen an appealing alternative.
I'll keep writing things in C, because it maximizes my productivity and gives me extra time to make it secure (less time than it would require to write it in Rust in the first place).
“`typeof` is standard”

Hallelujah, after only five decades generic, type-safe macros which evaluate their arguments exactly once will finally be possible with standards-compliant C.

How so? You'd also need statement expressions for most of them.
No, you can use do{}while(0) usually. You only need statement expressions if you want it to evaluate to a value as well.
Whenever I use C I use it for the express purpose of supporting ancient platforms. These platforms often only support C89, so that’s the dialect of C to which I restrict myself. If I am able to ignore those old platforms, I use C++. RAII is just so convenient.
Have you evaluated Rust instead of C++?
Indeed, RAII and function overloading are the two things I sorely miss when I think of coding in C.
So true and false keywords are the only thing that has been added, or am I missing a part of this thread?

Edit: I think one of these could be a better link: https://en.cppreference.com/w/c/23 http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2864.pdf

The linked tweet is the 4th in a sequence. If you scroll up you should see 3 more tweets with more info.
(comment deleted)
Thanks - Twitter UI is terrible.

I'm glad the terrible "defer" suggestion didn't make it, but unfortunate that they didn't standardise the existing and widely used attribute cleanup.

This is off topic, but I don't think the problem is just that the Twitter is terrible.

If you want to link to a thread, you should click on the first tweet of that thread and copy its url. It's easy to see the rest of the thread: just scroll down.

If you want to link to one tweet in a thread you should click on that tweet and copy its url.

The person who posted the link to HN did the latter, while they should have done the former.

Twitter could make it easier to see that a tweet is part of a thread though.

Twitter is bad if you don't have a twitter account. It'd be great if HN could change twitter links to go through nitter.net
While we're off topic here's my little rant about Twitter "threads":

It's easy to see all of the tweets made by the author, at least those made as replies to themselves, but reading the actual threads is hard or impossible. At least I haven't figured out how to do it despite considerable effort.

For example, say you want to view the replies to the tweet above the linked tweet, the one about "assert". There are supposedly 4 replies. If you tap that tweet you remain in the same list of self-replied tweets. There are replies at the end, but are they replies to the tweet you want to focus on? It doesn't look like it, but who knows, maybe? It's even worse on linger Twitter self-reply "threads".

I don't know how Twitter's engineers got the well-established threading pattern so wrong. This behavior doesn't even increase the number of ads you see.

The cppreference.com link says it removes "Representations for signed integers other than two's complement" - this could be pretty significant in making many previously implementation-defined programming patterns strictly conforming, but is it true? Doesn't seem mentioned in the Twitter thread.
This is the accepted twos complement proposal: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2412.pdf

It seems like a sensible simplification to me. Is there any architecture that doesn't use twos complement (eg. uses separate sign bit + magnitude) and could run C code?

Some microcontrollers used to have saturating arithmetic, which is basically the only other behavior that makes any sense.
Perhaps I'm missing something obvious, but isn't that orthogonal to representation? You could implement saturating arithmetic for any representation scheme, no?
You can only observe whether the representation is 2s-complement or not if you can see what wrapping does.
That doesn't sound right. You can use the union trick in C (but not C++, strictly speaking) to read the bit-pattern of a signed integer as an unsigned integer, for example. [0]

Overflow of signed integer types remains undefined behaviour in both C and C++. A compiler is free to make a promise like signed integer overflow is always handled with wrapping or signed integer overflow is always handled with saturation. (GCC has a flag, -fwrapv, to enable guaranteed wrapping. [1])

[0] https://stackoverflow.com/a/25664954/

[1] https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

> Is there any architecture that doesn't use twos complement (eg. uses separate sign bit + magnitude) and could run C code?

I think such machines are pretty much a thing of the past. This topic cropped up in a 2018 thread when C++ made the same change. [0]

[0] https://news.ycombinator.com/item?id=17190864

The devil is not in the number representation but in the details. The proposal says:

> Overflowing operations and out-of-range conversion are generally mapped to modulo operations and cannot trap or raise signals.

This would be a problem on x86, where INT_MIN / -1 causes a #DE (divide error) exception. In C17 that is fine because signed integer overflow is undefined behaviour. But if the behaviour is defined, that trap would have to be caught or avoided.

The best solution for performance would be to have a new trap handler. That would have to parse the instruction to find its length, and the divisor (in memory or register) to differentiate it from division by zero, write the constant result and remainder into the destination registers (always rax and rdx thankfully) and then continue with the next instruction.

But I think this could be difficult to do in a fool-proof way, with how CPU exceptions are handled in different operating systems. Some operating systems might not even allow it at all.

The sentence you quote is near to the words:

“[The C++ standardization committee] WG21 has recently adapted the changes promoted in their document p12363. Generally, C++ goes much beyond what is presented here:”

I would be extremely surprised if the proposal to make signed arithmetic overflow defined behavior in C made it into C23. The window is narrowing and this would be a very big change to the language. Making it official that 2's complement is the only representation for signed integers is already a large change.

Later in the decade, maybe.

The AGC (Apollo Guidance Computer). This means we won't go to the moon anytime soon ;-)
Based on another thread - I think the following topic should be an interesting one for HN to chew on: If someone wanted to write systems type of programming (whether it be embedded or Unix tools or even writing a library), should they go for C (C23) or Rust today?
I'm going for C, because I am experienced with C and despite it being more tedious, with having to do everything yourself, I stick with it because the compile times, and in fact the compile process, are so much faster and simpler. I really enjoy the direct feedback I can get. I considered C++ but that too had terrible compile times. Ideally a debug build should simply not take longer than 100ms for a codebase that isn't even 5KLoc, and only C seems to be able to offer that experience.
I get that the compile process is faster, but simpler?

In my experience, the compile process usually consists of "cargo build"

A advantage of Rust is that usually you spend way less time debugging, because the compiler prevents many mistakes from compiling in the first place.

I have an IDE with clang-lint and I find that it can spot pretty much every mistake. It can even detect some lifetime issues. Other than that my programming style is very much pass-by-value, trust the compiler to inline it, so the typical memory safety issues are simply not relevant to me.
Pass-by-value and let LLVM clean it up is also the idiomatic Rust way. rust-analyzer is pretty good for IDE integration, and it will catch way more than clang-lint (global lifetime analysis with guaranteed correctness, not just best-affort local, and thread safety/data races).

Regarding compile times, Rust has a different approach. Instead of a fast cycle of run and see if it broke, it's more of following compiler nags until the program is correct and runs on the first try. Incremental (cached) compile times aren't too bad these days.

Before I tried Rust (I got sick of it and the slow progress of Herb Sutter's research), I have used CLion with clang-tidy and had plenty of memory issues going undetected.

Needless to say I didn't restrict myself to pass-by-value (compatible -> structs with ptrs are problematic as well) style, which seems quite restrictive

With current ISO C++ velocity and the stance to keep doing language evolution in person meetings, I don't think Herb's proposals will ever come to be, and even if they do, it won't matter when they most likely can only make it post-ISO C++26 with best efforts.
Why does it matter if compile time is 100ms or 300ms? At even a second, that’s going to be so negligible in the context of code-compile-debug work. Especially if something like a flash download is involved.
0. it is a noticeable delay and impinges my momentum

1. A flash download is not involved

2. I hardly debug. I seem to be in a minority here, but my linter really does catch pretty much everything. I don't blindly write code, I always ponder my approach first.

So basically, the compile times pull me out of the flow.

Very much with you on this one. Even with 50k+ LoC a 'make clean;make' takes less than a second (with help from ccache and a RAM disk). It effectively gets the compiler out of the way and keeps the thought processes continuous.

One trick I like is to have a 2nd terminal watching the file(s) I'm editing, triggering a make/run on each save; building and running is then a single keypress away:-

  while [ 1 ]; do
  inotifywait -e move_self foo.c
  cc foo.c -o foo && ./foo # or 'make && ./foo'
  done
Are your rust builds really that fast? Wth am I doing wrong! I'd love to measure it in seconds.
It's slow for me, but I'm not working on a very powerful machine.

That said, cargo check is fast, and my general experience with Rust is that it generally works once it compiles. People getting into more advanced features might have different experiences.

The compilation time difference between C and Rust for nontrivial projects is much bigger than 3x.
Are there features you can ban in your style guide to prevent serious compile time problems?
You could get productive in Rust in 3 months max, and then the productivity gains would quickly outweigh the initial investment
Rust code takes longer to write than C code
My experience is mainly with C++, but from my own evaluation I'm x2 to x3 more productive overall in Rust than in C++. The reasons are a simpler build system that makes using the ecosystem faster, more expressive type system (iterators with combinators, sum types), the safety part that allows to not accidentally introduce UB and to not waste time with static and runtime analysis, shortening debug and review time. This compounds over time to give a better maintenance time.

I don't have a similar evaluation for C <-> Rust, but I suspect the result would be even more skewed in Rust's favor: sure, you might be writing individual lines faster in C, but the lack of iterators, generics, traits and RAII, and the relative difficulty to use dependencies means that you will waste a lot of time reinventing the wheel, often suboptimally.

Total time of «coding + debugging» (development time) is shorter for safe Rust, because Rust eliminates few classes of errors. Unsafe Rust and C are same, but Rust have better abstractions (match operator, Rust enum's, typed macros, etc.)
It does for a novice. But once you learn Rust, it's actually quite productive. Rust has proper collections, strings, iterators, automatic (but not GC) memory management, error propagation, etc. This eliminates a lot of busywork, and makes Rust "denser", so you write less code, but get more done.
No, the greenfield coding phase of Rust will always take longer than the same code in C: Rust will require you to think things through, while C by default accepts any morass of half-defined code.

The eventual rust code+debug+maintain speed will be higher than the equivalent C speed, for the same reason.

This means that rust is slower for vanity projects that won't be used in practice, or for low quality products with an attitude a la hack it- ship it - hide for customers - start next mess. The scary part: this is actually an existing corner of our market.

I know where you're coming from. I've felt the same when I was novice in Rust, and could not imagine how people get anything done when 10 lines of code have 10 borrow checker issues to deal with. But this obstacle completely goes away once you understand ownership and Rust idioms, instead of trying to make rustc accept C-style code.

I have many years of experience in both, and I can confidently say that for experienced programmers Rust is both quicker than C to get something useful working, and to get projects polished and production-ready.

Rust has a useful standard library (compared to C). You have hashmaps, btrees, or vectors with lots of helper methods all available in less than it takes to hand-roll a linked list.

Rust has lots of high-level crates that you can leverage. You need to fetch some JSON? It's a few lines of code to get it into a native struct, type-checked for you. You can have it running in less than it takes to craft a Makefile.

Rust is also productive for robust code. You don't fiddle with malloc, so the whole `goto cleanup` code is gone, and you don't spend any time chasing memory corruption, leaks, or unexpected NULLs. And these are often static guarantees, not something to fuzz out of a stanitizer.

If you need code to be multi-threaded, it can be as easy and high-level as OpenMP, but you get a compile-time guarantee of no data races, even in 3rd party code you use. Some errors pointed out by the Rust compiler have saved me what could have been weeks of heisenbug debugging.

It's normal that you move slower when writing an unfamiliar language, and Rust certainly has a steep learning curve. But once you learn it, Rust feels closer to high-level scripting languages than C.

I’m going for C because programming language choice is a matter of personal preference in essence and Rust annoys me.
How does it annoy you? Development velocity, creativity hinderance?
Visually it’s annoying. It’s like the comic sans of programming languages.
I used to code Perl in the late 90s. To me it doesn't look /that/ bad.
If rust is comic sans then Perl is wingdings, so yes, it could be worse.
Hilarious
I don't think that can be answered in general. It depends on what exactly you want to do and the maturity of the libraries in that area, and how security critical your application is.
> the maturity of the libraries in that area,

While true for C++ which is not very interoperable, C being very interoperable means that there is no advantage into using a C API from C rather than from Rust.

> how security critical your application is.

Rust also has productivity and general "correctness" advantages that go beyond security (although yes that alone is a factor)

In general, I think they should go for Rust. See [1].

Now, embedded is a special case. You need platform support, and possibly certification, none of which are a given in Rust today. Arguably, not in C23 either at the moment, though.

[1]: http://cliffle.com/blog/prefer-rust/

Neither, unless we are speaking of kernel and drivers kind of systems programming, in which case I would pick Rust.

For anything else, a language with automatic memory management.

I wish D was more widely used, as it seems to be good as a systems language, too.
Me too, unfortunately the lack of focus has allowed the competition to step in, so at this point it is hard to tell what would be the killer application that can make it take off.
D is a better modern C than Rust.

Rust is very nice, but if you just want a safer C that will actually make you more productive versus less productive until you understand how rust wants you to program then D is a better fit.

Agree. "A better C" than C++ claims to be.
C, because I don't know how to do intrusive data structures in Rust.
There's also Zig to consider here, and it arguably competes more directly and explicitly with C than Rust does.
Nim with ARC works really well for embedded or writing system tools. Compiling to C or C++ is still handy sometimes.
(comment deleted)
Having trouble figuring out if this is real. It sounds way too good to be true.
Removing K&R style function prototypes though is kind of big. I remember when one of the Objective-C upgrades started enforcing K&R. I don't think ObjC will be upgraded to C23 (ever, as it's a language being deprecated now I presume) so it will be stuck with K&R... while the rest will move on?

Could someone also explain what this means for pure C in terms of compatibility? If there's

  void foo();
would the meaning of it depend on which C standard is activated?
What's being removed in C23 is the ancient K&R syntax for function definitions:

    int max(a, b)
    int a, b;
    {
        return a>b?a:b;
    }
My understanding is that C23 does not change the meaning of function declarations. So "void foo();" remains a declaration of a function accepting an unknown number of parameters.
According to the OP tweet though, `foo()` is now a function that takes no arguments.
Wouldn't int foo(void) be a function that takes no arguments and int foo() a function that takes an unknown number of arguments?
That's a different thing. That's implied `foo(void)`.
It WAS a different thing. That's changing in C23.
Agree the tweet makes it seem the behavior is changing.

Edit: parent is correct it IS changing

> We could make this change because foo() decls and definitions had been deprecated since C89/99, longer than I've been alive.

> Implementations will likely warn on foo() decls for a while, and error if someone calls it with foo(too, many, args);.

https://mobile.twitter.com/__phantomderp/status/149481259506...

Here's the document for that change: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2841.htm

> In this proposal, a function declarator without a parameter list declares a prototype for a function that takes no parameters (like it does in C++).

And it seems like gcc implements this under -std=c2x now:

    zx2c4@thinkpad /tmp $ cat a.c
    int blah()
    {
            return 7;
    }
    
    int main(int argc, char *argv[])
    {
            return blah(argc);
    }
    zx2c4@thinkpad /tmp $ gcc -std=c17 a.c
    zx2c4@thinkpad /tmp $ gcc -std=c2x a.c
    a.c: In function ‘main’:
    a.c:8:16: error: too many arguments to function ‘blah’
        8 |         return blah(argc);
          |                ^~~~
    a.c:1:5: note: declared here
        1 | int blah()
          |     ^~~~
Which is much more consistent, IMO.

int foo(int, int) is a function taking two arguments. int foo(int) is a function taking one argument. int foo() ought to mean a function taking no arguments, not a function with an unspecified number of arguments!

Wow, that's annoying. There's a bunch of old code we have that'll need updating (written that way to keep the line lengths down).
...can't you just configure your build system to compile those modules with pre-c23 standard?
Of course. But one of the great things about C is that as a rule nothing breaks with compiler changes (as long as appropriate care has been taken) which this appears to violate.
Maybe the compiler will add a extension flag? I mean the K&R declarations still need to be supported in the compiler for earlier standards.
All the compilers I use have a default C version, and an optional flag to control which version to use. It's a non-issue.
IME ObjC (unlike C++) automatically supports the latest C standards, because ObjC just adds some syntax sugar on top of C, but otherwise doesn't 'mangle' its C subset into its own dialect like C++ does.
>IME ObjC (unlike C++) automatically supports the latest C standards,

This is only true as long as the changes in C aren't conflicting with ObjC. In the future if ObjC is deemed dead by the council it won't have to worry about such things. So C58 may have tons of things that don't work in ObjC98 (or whatever the ObjC version is).

Obj-C can be compiled using any version of C (and Obj-C++ can be compiled using any version of C++). You already can use C2x with obj-c if you desire. I don't know what "enforcing K&R" means in this context, but if you think that Obj-C requires using K&R-style function definitions you're very confused about something.
> [dropped] Representations for signed integers other than two's complement

May I assume that there won't be signed int undefined behavior after C23? INT_MAX + 1 == INT_MIN?

I doubt it, and wouldn’t want it. It would break all kinds of optimizations. For example, with wrapping

  for(int i = 0; i <= m; i++) { foo(); }
Wouldn’t terminate if m = INT_MAX. That means compilers either have to special-case that value, or can’t use the loop instructions of some CPUs (M68000 has “decrement and branch if larger than zero”, for example)

Also, with wrapping, a[i] and a[i+1] aren’t guaranteed to be adjacent. That can make auto-vectorisation difficult.

Those things are very subtle and complex, so I'm not 100% sure, but I think your first example is not great:

    For example, with wrapping
    
        for(int i = 0; i <= m; i++) { foo(); }
    
    Wouldn’t terminate if m = INT_MAX.
This is undefined behavior in the current standard when m = INT_MAX. The condition i <= m is always true and i will overflow. So it could result in an infinite loop (or worse). AFAICT, with wrapping signed ints, it would infinite loop but at least not yield UB?
Yes, for m = INT_MAX and wrapping signed int, it is undefined behavior.

Because of that, the compiler is allowed to assume m ≠ INT_MAX, and that can mean it can generate more efficient and/or smaller code.

Assuming m ≠ INT_MAX isn’t (considered to be [1]) a big loss because chances are the programmer who wrote that didn’t intend to write an infinite loop.

[1] of course, is an opinion, but it’s the opinion the standards writers have had for decades.

C is the only sane language left. It never changes and it lets us write code that just works. We can focus on getting things done and shipping rather than learning new features and Googling compiler errors. Learn once, ship forever.
Majority of CVE's are because of C memory model, which is fixed in modern languages. C memory model is the constant source of vulnerabilities, thus it better to write new code in a memory safe language, like Rust.
Rust is a syntax error, not a programming language.
what does this mean? It sounds like you tried to make a funny programming insult, but it fell flat if that's what you were going for
>Majority of CVE's are because of C memory model

I haven't checked that but I believe you. My only nitpick is that I wouldn't put the whole blame for that on C, as there are proper ways to produce "safer"[1] code. What other languages did was lower the entry barrier to write better code, but that doesn't mean one language is inherently better that another.

1: No code is ever safe, not even formally verified code.

> Majority of CVE's are because of C memory model

“Memory safety” (or lack thereof) is probably less ambiguous: usually “memory model” is the interaction between program and memory, specifically as it pertains to concurrency. And in the latter understanding I believe C has about the same model as C++, which is also the one adopted by Rust. And while it’s fiddly, it’s not a complete disaster.

C and Rust have the same memory model though, unless you're using a definition of "memory model" that I'm unaware of.
Ye the churn is crazy in many languages. I almost had a heart attack when the twitter post stated that "true" and "false" are keywords in C23. However that seems incorrect.

On my last workplace we argued if we dared to use stdint.h, since it was so new and fancy. I was a proponent, but there is something calm over discussing whether to use 23 year old additions or not.

Sounds like a dream workplace similar to mine.
...except it just changed.

And you will be forever left writing insecure memory unsafe code.

One change per decade is not a change.
More than once per decade: C11, C17, C23
I guess you can say C17 is a change if you want to be pedantic except it didn't actually introduce any new features; it just corrected some defects found in C11.

So we have C89, C99, C11, and C2X (C23?) as the only revisions introducing new stuff...

It’s also upwardly compatible so no messing with code from a deprecated version. Python 2 to python three has this issue. If I need to change my code to make it run on the latest language standard then that is a big red flag for me.
Well said, we should team up!
C99 is the last sane language left. The additions since then are just minor syntactic sugar.
The effort required to do anything besides low level utils in C is immense. And once you have it working, how do you know it's correct? You have to carefully review it.

You could write and rewrite everything multiple times in a high level language with that much work.

C libraries break all the time. They change them whenever they want. The standard lib and language might be stable but the large frameworks aren't.

It doesn't seem at all suitable for large application programs.

I do sometimes worry that one day my Python code won't run and 5 year projects will become useless. But so far, popular language aren't showing any sign of going away.

> Support for calling realloc() with zero size (the behavior becomes undefined)

OMG! Please do not add more undefined behaviour!

We really need less UB, not more! What was wrong with 'implementation defined' as in C17? UB means that code that exists will now break if a new compiler decides that it can be smart. And we know how inventive this can get, right? Like removal of 'if(new_size==0) {...}' if the compiler can prove that 'new_size' went into 'realloc' before.

They'd better not convert that to undefined behavior!

First, the specification for malloc() says you can call it with size 0 and it will still return a pointer to zero bytes; this means an array of bytes having size zero is legal.

Second, having the compiler detect when realloc() is called with size zero changes what should be a library call into an intrinsic, whose very presence in the code is determined by the value of its arguments[0]. If you're going to do that you might as well just put exceptions in the language!

[0] Most compilers assume UB "can't happen" and just remove the offending code altogether.

> Most compilers assume UB "can't happen" and just remove the offending code altogether.

To be clearer: UBs (specifically the assumption that they don’t happen) get translated into constraints (since C’s type system is extremely weak UBs are a very valuable source of such constraints to optimising compilers), those constraints are then propagated (forwards, and backwards, and upwards due to inlining) and any path for which the constraint does not hold can be considered dead.

Manual says

> If size is 0, then malloc() returns either NULL, or a unique pointer value that can later be successfully passed to free().

So it's not really an array of bytes of size 0.

What qualifies something as a "real" array of bytes?

All RAM is an array of bytes, so any pointer to real memory(vs. virtual memory) is a pointer to an array of bytes.

C mandates that arrays have positive size. It's not really a discussion about what the target platform does, rather it's what the abstract C machine mandates.
Oh sure - malloc() returns a pointer to sequentially-addressable bytes, not a C array. So even if the size were greater than 0, it would prove nothing about C arrays.
> ... malloc() returns a pointer to sequentially-addressable bytes ...

malloc () doesn't return even that. It returns 'void *' which isn't of much use without a further upcast. The required upcast, and the rest that follows, is the coder's own idea of what happens.

The second allowed result of malloc(0), a unique pointer passable to free(), is not a pointer to an array of bytes. It can’t be used to access bytes - only compared to other pointers since it’s unique, and passed to free(). It doesn’t point to anything.
Where did you see this? I can't find it in the Twitter thread or any of the linked sources. I agree that "adding" support for something by making it UB makes no sense... as that generally would imply breaking forward-compatibility...
Not only that, but the feature is genuinely useful.
Does anybody bother about the stuff after C99?
MSVC just got C11 support a little over a year ago, so you won't see many big cross-platform projects using it yet, unfortunately. Some projects can optionally use C11 stdatomic.h and stdalign.h to replace old OS and compiler-specific extensions, but most of them still keep fallback code in case C11 isn't available.
So does that mean we should expect C23 support in ~2033?
You could also use GCC's mingw platform to target windows, which supports all the same modern C features GCC does for other targets.
For all of us that follow an "I do not want to log in to twitter" philosophy, can someone here please post an alternative link?

Incidentally, I'm glad Twitter now blocks you from reading if not signed in. I'm hoping this will encourage the use of alternatives.

There is indeed nitter.I also recommend using an extension for general redirecting.A popular one I use with presets for big sites is Privacy Redirect (ff) but there are also ones for that allow customization (Redirector for example, also ff).
I'm not signed in and read it fine. My uBlock filters are

  twitter.com##[id^="layers"] > [class^="css-"]:has([dir^="auto"]):not(:has([aria-expanded])):has(a[href^="/"]):has-text(/Log in|Sign Up/)
  twitter.com##html[dir]:style(overflow: auto !important;)
(comment deleted)
(comment deleted)
You can paste this on console to remove the popup

(function () { document.querySelector('html').style = ''; document.querySelector('.css-1dbjc4n.r-aqfbo4.r-1d2f490.r-12vffkv.r-1xcajam.r-zchlnj.r-ipm5af').remove(); })()

It used to be that every new language starts off with a translator to C. Not sure if that’s the case nowadays.
With the advent of LLVM that's not common anymore.

Bootstrapping can be done in pretty much any language nowadays.

Neither it was common before, Objective-C, C++, Eiffel are probably the only known ones with mainstream market share that actually went via the compile to C as initial implementation.
Not really, some did follow that path.
> It used to be that every new language starts off with a translator to C.

No, it really never was that way, though there were some languages (especially ones that started as C plus some additional features, like C++ and Obj-C) that did that.

> typeof(...) is standard

I thought C++11 went with decltype because typeof was implemented by compilers as an extension with various subtle semantic differences than what they wanted.

I wonder why the C committee went with typeof instead of decltype?

IIRC typeof is implemented in the same way by all compilers that have it, but it had some subtle differences with what the committee wanted. The differences presumably do not matter for C, in fact I think it was something related to references.
(comment deleted)
> In its London 2019 meeting, WG14 has found consensus to elevate false and true to proper keywords.

The committee settled this over a beer, or some other substance? Because the elevation to a proper type is opportunistic at best, and against the spirit of C at worst. Next people celebrate this with the use of capslock.

The futility of it all blows my mind :C

> the elevation to a proper type is opportunistic at best, and against the spirit of C at worst

Can you explain what you mean by this?

So C is all about the machine, in this case about storage, memory. Other languages emphasize types, whereas C instead emphasizes values and their representation.

So a condition or comparison does not result in a boolean, it results in a value that can be stored in 1 bit. Its values therefore are 0 and 1. There is nothing to understand, except that the values are dictated by the width of the storage, as usual. The stdbool annotation is cultural, optional, but not C. C does not have boolean values. True and false are mental crutches, names that are somewhat helpful in the absence of an explicit bit type, but still very much a lie.

I think people coming from other languages are driving the future of C now. They are used to booleans as a distinct type, exclusive from other types. Yet in C any int or pointer value has a boolean interface defined on it, and the ambiguous nature of, for for example a timeout, being both a value (time remaining) and a boolean (timed out), highlights the why of C and why C is the king of embedded programming.

So at minimum there are multiple aspects of "booleans" in C that other languages specifically de-emphasize, like storage, representation and the ambiguity of values with other types. The synthesis of the newly introduced C booleans seemingly doesn't take this impedance mismatch into account. From reading it seems there is no synthesis.

Bits have been a traditionally dark corner of C, as I can't declare or index an array of bits; and bit-fields are not my cup of tea (don't use them). I think that's where improvements can be made. Specifically the introduction of a bit type would be in the spirit of C. No need to rename the values, 0 and 1 are more truthful than true and false. If true and false are needed, then a distinct boolean type (as in other languages) might make sense, as an addition, not a replacement. Just to get true/false values and conceptual isolation.

stdbool could not do that and made compromises. Cxx enshrines that compromise, instead of genuinely improving on it. So maybe a bit type for the C programmer and a bool type for everybody else.. :/

Yet C is an impossible ship to turn around, in terms of culture and sunken investments. Changes are virtually impossible and, even more so, highly undesirable. Instead it's time to move on and sunset C. The best C at this point is a sealed C, with a tombstone on it. Then at least it's a known entity, and then C code shall exist side by side with say Rust or Zig code, to be compiled and linked by the same compiler, without language churn. Such systems will proudly carry C into the future. Any C language changes, if needed, should focus exclusively on making that coexistence work.

So, the sky is falling for unspecified reasons because booleans in C are types instead of macros.

And your proposed solution is to sunset C and move on to Rust or Zig, both of which have primitive boolean types.

> This is only one meeting. We have like 5 more before C23, and only ~2 more before it's ISO Draft Ballot time
> Oh, and a big deal: true/false are KEYWORDS NOW, and all of the bool/thread_local/etc. are all KEYWORDS NOW!!!!

Why does it even matter? I could take true and false to be an improvement (having them as macros expanding to int literals had its problems, but even for that one I would not mind _True and _False at all), but the rest? Who cares? It’s literally the least significant change.

Finally C catches up with Algol 60 boolean types, it only took 60 years.

Maybe in another 60 it will get proper strings and arrays.

>Maybe in another 60 it will get proper strings and arrays.

If you think this is missing, there's a big chance you don't "get" C.

Also, if you don't know how to properly emulate strings in a safe way in C nowadays (for which plenty of code sample and librariesexist), your skills with the language are quite poor.

I surely get C, having to deal with its casualties in DevSecOps.

I have been "getting" C since 1992, across Xenix, DG/UX, HP-UX, Solaris, AIX, FreeBSD, Linux, Amiga, Windows 3.x,.....

You once claimed C requires UNIX like OS to run.
And he was right. He didn't mean it as a statement of fact (which would be wrong, and he already knew it).
I claim it yet again on this very comment.

Without UNIX spreading with its free beer into universities, C would have been a footnote on the history of system programming languages.

Nope in the above comment you claim something else.

Here is where you claim C requires UNIX like OS:

https://news.ycombinator.com/item?id=20828974

Let me tell you, in embedded development we run C on bare metal or under RTOS. There is no UNIX in sight. And the code is portable to many CPU architectures.

A snowflake RTOS without POSIX, call me amazed.
I'm happy you learned something.
Not really, still waiting for the name of such marvelous OS.
Keil RTOS. You can also use C on bare metal without an OS. I hope you won't claim next that CPU's are UNIX like OSs.
Much more importantly, without UNIX and free beer TCP/IP there's a fair chance the Internet isn't the Network and none of this (gestures at HN and the Web) happens. That's a much bigger impact than C which is just a programming language, even if you count the influence of NUL-terminated strings, and all the semi-colon languages (including even Javascript and Rust, which are very different from C but preserve the enthusiasm for semi-colons) the Internet as Network makes more difference.

In particular it's possible X.25 could have won. JANET (the tertiary education network in the United Kingdom) was exclusively doing X.25 until 1991, when it tries an experiment offering IP. Unsurprisingly free beer TCP/IP offers a cornucopia of interesting new toys and sites are very enthusiastic about JIPS (the experimental IP service) so that by 1992 the IP service is no longer an experiment but the primary service, with X.25 maintained only until 1997.

You do get the Network anyway of course, that's what everything is leading up to after Shannon, but you probably get an X.25 flavoured Network instead of the Internet, or some other system coming from another direction, and broad deployment might have been delayed into the early 21st century rather than the 1990s depending on exactly how things work out. Stallman's GNU project doesn't happen, Tim's toy hypermedia system either doesn't exist or is a small inconsequential experiment largely unknown outside hypermedia fanatics, and Linus Torvalds probably doesn't make an operating system kernel for his 386 microcomputer. It's a very different environment, C is small potatoes by comparison.

So? I could deal with such alternative universe, after all, between 1986 and 1994, I managed to work with computers and networking just fine.
Adding so-called fat pointers to C that could be bounds checked would probably instantly eliminate a large fraction of all security issues.

Of course, you can emulate these yourself, it's just a struct but not everyone does.

Adding opaque builtin struct types to C would be kind of a big deal though and would probably creep into other parts of the language and interact with them in unexpected ways. Features like fat pointers, slices, builtin dynamic arrays/strings or tagged unions would all require builtin opaque structs. This would be a whole different language (closer to Zig).
It really wouldn't be that hard to do. The syntax would be trivial, and the whole point is that you want it to creep so people actually use them.

There's no overhead to doing it, but you save a bunch of bugs earlier, all for stopping conflating arrays with pointers, which is something not much code does these days anyway

C99 introduced variable-length array parameters and variable-length array pointers together with variable-length arrays (VLA). These take another parameter or variable as a length-parameter. These became optional in C11 together with VLAs (for some reason) but are supposed to again become mandatory in C23. However (yet, for some reason) the pointers can (in the latest published draft, still) only be inside procedures: not in structs or in global variables.

I think it would not be impossible for a C compiler to detect and produce a warning if an index used with such an array-pointer is not checked against the array-pointer's length variable.

This would not make bounds-checks automatic, but the use of these types could make compilers point out when bounds are not checked or checked incorrectly.

People don't really get the distinction you make here, which is a shame. Often any VLA's are outlawed which is tossing the baby out with the bathwater.
Compilers can already do this:

https://godbolt.org/z/jMMa8n48r

Yes, putting them into structs is a missing feature. It is not in the draft because no compiler can do it and their is no implementation experience. I am working on it.

C must be really amazing compared to Algol than. It powers today's computing infrastructure without having proper boolean type. Are there any widely used applications written in Algol?
UNIX free beer, hard to beat.
You do realize there is a lot of C code out there which has nothing to do with UNIX. Although I do remember when you once claimed C requires UNIX like OS to run. :)
Well, UNIX is surely responsible for C becoming popular (if not its being a thing in the first place).
I'm not arguing that it is not? It's funny though how many people saw value in C but not in Algol. Maybe C has something going for it?
Free beer alongside a free beer OS, instead of paying thousands of dollars for timesharing OSes like VMS.
I know you keep repeating it. You are conveniently ignoring plenty of other commercial OSs and languages which were successful.
Who, except for people too young to know better, saw value in C but not in Algol? The Bell Labs folks obviously thought enough of Algol such that C is a descendant in the family of Algol-like languages. Algol was widely used in Europe whereas in the U.S. we made do with Fortran, Cobol, and, I think to a lesser extent, PL/I, although the latter had many Algol-like features at the time. (And Fortran evolved to have many of those features; I suppose Cobol also, but I'm not familiar with its post-1981 history.)

Taking a contrarian view to everyone else in this thread, I don't think Unix rocketed C to stardom - Turbo C did. Turbo Pascal was very very popular for PC programming and was eventually supplanted by Turbo C and Turbo C++. Without Turbo C, I think C would have probably become just another niche language used on a niche OS, Unix.

And in line with what you say in another comment, commercial OSes -- including VMS -- were very powerful and much more popular than Unix. In 1982, a single VAX 11/780 running VMS with 4 and 8 MB of RAM could support 10s of programmers editing, compiling, debugging, and testing code. VMS was also a real-time operating system that we used in production to control/monitor/perform-I/O hardware devices handling LANDSAT image data. And Unix is still trying to figure out async I/O ... (Most of my career has been spent on Unix, but I appreciate other operating systems I've worked on as well.)

I have nothing against Algol. Turbo Pascal was my first language (i agree with you about Turbo C). I loved it at the time. But C is different. The same way Lisp is different. It is just one of those languages which have a very simple core idea which is also very powerful at the same time.

Lisp has the code-data duality (everything is a list) going for it and C has everything is pointer to a block of memory and that's it. That block can be a device register, data structure, array of sth. or just about anything you can imagine. This idea enables you some kind of proto-polymorphism without the verbosity and chains of higher level languages (at the cost of safety of course). That is the beauty of C and unlike Lisp it is also lot closer to the metal, which helped a lot with performance.

I think it's normal to be jaded when something objectively superior is being replaced with something simpler. But that doesn't mean the simpler thing is without merit. The world has more dimensions, the technical aspect is just one of it.

No, everything is not a pointer in C; C is based on value types that are passed around by copy (with the exception of arrays and functions). An variable of a given type designates a piece of storage which contains that value, rather than a pointer to it.

C is not significantly different from Algol or Pascal. The difference between C and Algol is a lot smaller than between either of those and Lisp or ML.

What's missing in some Algol-like languages compared to C is the loose pointer arithmetic, and possibly type punning via pointer conversions. Those features allow C programmers to do things like write their own memory allocator, which is incredibly useful in embedded systems. A C project can produce a self-contained image that boots on bare metal, with minimum assembly language. Or on almost bare metal, where there is a boot loader program that provides no services to the C program other than jumping to its entry point and maybe some console printing routines or something.

You have completely misunderstood my comment about why C stands out. I though it was obvious from the example I gave, what I meant, when I said everything is a pointer to something?

As an embedded developer in previous life, I can assure you C is not special because it can run on bare metal. Many other languages can too.

UNIX is to C like the Web is to JavaScript.

JavaScript also runs outside of the browser nowadays, so what.

I don't know what are you trying to say? How does this follow from the parent post?
It follows that the Web made JavaScript unavoidable, then nodejs made it available everywhere else.

Likewise UNIX made C unavoidable in systems programming for timesharing servers, eventually like JavaScript outgrew the browser, certain people wanted to run C in their microcomputers.

I don't disagree with you on C being popular because of UNIX. I do disagree if you believe this is the only reason why it still enjoys popularity. And you are selling the UNIX authors short if you think they chose C just for the fun of it.
The UNIX authors didn't choose C; they made it, specifically to make UNIX. And, much like the original UNIX itself, it was a hack - because they were trying to make do within the limits of their hardware. So, some of the syntax is the way it is because it was simpler to parse, for example. And some of the semantics was originally "because PDP does that".

As for why C has staying power, that's solely a function of its early success. There's nothing about the language itself that isn't better provided for by some of its historic competitors (e.g. Modula-2). But there's a self-reinforcing loop here, where all new hardware platforms start with C as a basis because it lets you write all the existing code; and conversely, using C is the way to ensure that your code will work even on future platforms.

You're hung up on words. I know they made it. It was based of Ken Thomson's B. They chose it, when they could have taken some other existing language. I see great beauty and simplicity in C the same way I see it e.g. in Lisp or Javascript for that matter. I dabbled in Modula-2 and it does not offer the same feeling in my opinion. The core features of C are very powerful and simple at the same time, which is the reason it stayed popular. People writing low level code understand that.
The core features of Modula-2 are largely the same as C + modules, but with more consistent syntax and fewer footguns (like no unsafe implicit pointer conversions), so I don't see how C can objectively possess "great beauty and simplicity" in comparison. Unless you're referring specifically to the syntax?

B was also written as a hack - it was basically a minimal subset of BCPL small enough to fit in the memory of the machines they were working with. B is elegant in a sense that it's a very simple language for a word-oriented architecture - but that's exactly the part that they had to get rid of for C to target byte-oriented ones. Coincidentally, it's also one of the sources of weirdness in C syntax (e.g. implicit int and K&R prototypes are all legacy of B).

Claiming something is not beautiful because it is "hacky" on HN is a bit unexpected.

For me this is beautiful:

https://briancallahan.net/blog/20220220.html

I'm sure it's just a hack to you, but you can't do that in Pascal.

I mostly like C because of what I wrote here: https://news.ycombinator.com/item?id=30404529

"Hacked together" can mean different things. In this case, it means something slapped together on short notice with limited resources. Such things rarely tend to be beautiful. Beautiful hacks absolutely exist, but they are never hacked together in this sense.

The only thing your blog post demonstrates is that C has a built-in textual macro facility. But if you wanted the same in Pascal, you could have it just the same, by running it through the C preprocessor; or better yet, something more powerful like M4. Of course, you'd have to deal with various impedance mismatches, such as the fact that the definition of "token" is different between the preprocessor and the compiler - but that's also true of C! In fact, this discrepancy alone is quite sufficient to deem C inelegant, in my opinion.

The Burroughs MCP operating system and some applications written on top of that. In fact, I believe that was the very first OS to be written entirely in a high-level language.

MCP is not today widely used in the same way, say, React or Linux is, but businesses used and depend on it such that it's still sold and supported, as Unisys ClearPath MCP.

I’ve heard that Burroughs had an absolutely world class engineering team in their heyday.
Cool, I will definitely look it up.
Then it would become Algol, wouldn't it?
So many features still missing.
It's no fun writing code without switch variables, goto-labels as arguments to procedures, and call-by-name.
Algol 60 pales and laughs nervously in the shadow of the Algol 68 giant and its type system towering over it.
I wonder what other string could have C used 50 years ago that would have stayed usable for that long.

I love that, thanks to C, plenty of old programs can still be used today. In these days of accelerated bit-rot and language "communities", and perpetual rewrite of everything over and over again, C stability and ubiquity is a safe harbor, and has been at least until now a good investment.

I have no idea what you mean by "proper arrays". I guess bound checks?

Just like JOVIAL or FORTRAN from 1950's, you get to know their size at any time, and yes bounds checking.
Can you still write a function that accept arrays of any size, then?
I mean, C++ doesn't have "proper" strings, so I wouldn't wait up for C getting them.
I see them on the ISO C++ standard document, maybe your copy is missing pages.
C has had a true Boolean type for over 20 years now, since C99. This time around they're merely dumping the backwards-compatible _Bool/_True/_False syntax + keyword-like #defines in <stdbool.h>, and replacing it all with proper keywords.

That said, it would be nice if C caught up with Algol 68, seeing how that's the one many of the concepts (and the corresponding keywords) are taken from. Let's start with first-class functions...

That was a kludge.
It was a syntactic kludge, perhaps, but it had good reasons - and in terms of expressive power, it's exactly the same as any other Boolean type.
Another nice convenience: a new #elifdef preprocessor directive to replace longhand #elif defined().

https://en.cppreference.com/w/c/23

Oomph. There seem to be more meaningful changes in C23. I sure hope they wouldn't consider a new standard for such trivialities alone. There's cost to those changes. Compilers will naturally need to be updated, but other tools, e.g. static code checkers and documentation, e.g. MISRA-C, too.
Can't believe they still denied us anonymous functions.