The criticisms related to UB are not about understanding the target platform and the target compiler's behavior. Undefined Behavior is not the same thing as Implementation-defined Behavior, and lots of folks (including me) would be satisfied with reclassifying chunks of UB as the latter.
The behavior of the target platform isn't really the issue. C23 mandates two's complement for signed integers. Most hardware wraps on overflow, but that literally doesn't matter. The standard says a program exhibiting signed overflow is undefined, period.
In practice, UB rules mean the compiler is free to remove checks for signed overflow/underflow, checks for null pointers, etc. This can and does happen. Man, just a few weeks ago, I just had to deal with a crash in a C program that turned out to be due to the compiler removing a null check. That was a painful one.
The what now? Though not lately, I did program in C for 15 years and never seen something like this. I did see some compiler bugs on obscure platforms (SINIX, IRIX, HPUX on Itanium64, etc.) with proprietary compilers, this kind of thing would make really get me shouting.
Were you able to determine why the compiler did this? Is it a bug in the compiler?
Compilers keep taking more and more advantage of inferring that a values in variables cannot be `x`, because if it were than some previous usage would have been UB. When people file bugs to complain, the compiler authors point at the spec which allows them to assume that UB behavior never happens, so the compiler behavior is legal. The only counterargument is if the compiler has chosen to document some specific behavior for this UB (possibly only with specific flags enabled) in which case the compiler testing that scenario as proof of impossibility is indeed a bug (when the required flags are set).
Like… edge cases? It's parsing a number! We're not talking about I/O on hard vs soft intr NFS mounts, here. There's a right answer.
strlen(), on valid null terminated strings, doesn't come with caveats like "oh we can't measure strings of length 99".
But sure, C is turing complete. It is possible to solve any problem a turing machine can solve.
> understand the target platform and the target compiler’s behavior.
This is neither. This is purely the language.
So, when you say, "it's purely the language", I have to disagree. The language means different things on different platforms but it's still defined exactly on the target platform. And it's efficient on that platform.
Nowadays, we prefer correct vs. efficient, which I do agree with, of course. But, I also understand why C is like it is. It is possible to claim it's a problem of the language but I would argue that it is not. C gives us barebones and working with it we have to know this. If that's not needed then sure, other languages will be easier to work with.
The C standard defines only its abstract machine, not actual hardware.
> The language means different things on different platforms but it's still defined exactly on the target platform
It's implemented to support a target platform, so that programs behave as if they ran on the abstract machine.
It'd be nice if we could move more stuff from UB to implementation defined.
Do keep in mind that target platform can change, in this regard. E.g. IIRC OpenBSD doesn't guarantee the ABI backward compatibility that Linux does, and can change things like size of int if they want, between versions.
> I also understand why C is like it is
Yup. It can be true that I understand why, and still understand that it's 2026.
The same code can be compiled for different platforms, yes, but the assembly and machine code will vary significantly, so it could behave differently. Porting to a new platform was usually a very complex process, but the code produced was efficient. Nobody seems to care about this nowadays, though, it seems.
People expect numbers to support specific ranges and it is fine to define the data types numerically rather than as a concrete bit pattern, but C just takes the cake.
Char is at least 8 bits, short is at least 16 bits, int is at least as big as short (genius idea), long is at least 32 bits, long long is at least 64 bits.
The point of "int" is to be the integer equivalent of size_t and therefore be of word size.
But nobody uses int like that. Everyone assumes it's a 32 bit datatype when it isn't.
The use case where you port existing C code to a microcontroller is extremely unappealing, because the number range gets changed under your feet. When I've had to work on embedded software everyone just used int8_t, int16_t, int32_t, int64_t for portability instead.