Who Says C is Simple?
people.eecs.berkeley.edu
people.eecs.berkeley.edu
But that's the problem with C. It looks like a simple language with a small number of straightforward constructs, when in fact it has very poor predictability.
Real-world results are implementation and platform-dependent, which is a polite way of saying they're unhelpfully unpredictable.
Coding standards will help, but they only get you so far because some ambiguities will always be there.
Basically C is a half-assed wrapper around assembler - useful in its day, but machine-dependent and outdated now.
you drug in "offshored code delivered by sweep shop consultancies", got a response, and then move the goalposts.
go away.
You were the one asserting that it only happens in sweetshops, hence my remark wasn't valid, thus I decided to broaden the scope of people failing to write C.
Those mythical C developers that are yet to be found after 50 years searching for them.
But, it is, in fact, possible for good developers to write incorrect C code. Noone's claiming that's not the case. It's just a lot less likely for someone who actually knows the language and the codebase.
You can keep the syntax and all the useful parts of the standard library in your head.
A lot of modern languages sacrificed simplicity for features no one (with enough skill) asked. Even with Python, I can only keep some part of built-in functions and methods together with syntax in my head.
For a large part of standard library, I absolutely need IDE features. Especially since Python is an object oriented language where every data instance has a bunch of methods attached.
I'm in the process of moving an old codebase from a compiler with a very loose adherence to an old specification to an ARM chip that is using GCC for the compiler. Enabling -Wall to catch all issues that the compiler highlighted, 16-bit ints to 32-bit ints, and quite a few ternary operator issues.
None of these were issues on a system that's been running in production for many years but porting to a new architecture will mean there’s a lot of extra testing to be done just to make sure we catch all the edge cases in the code.
I like the C language and have been using it for close to 30 years now. The main part of the language can be kept in your head but there is so much more to working with it than just the base language.
Not to mention the chances of so many things to go wrong, in writing C code. Its like climbing a mountain without a safety gear.
If you're implementing custom data structures, you'll likely be able to make them optimal for your use-case, both from ease-of-use and performance. (except hashmaps. those do suck to write)
Membership testing? Use sets. Data storage? Use classes with slots or tuples/namedtuples. Custom switch statement? Use the new match/case keywords or just fall back to plain if/elif towers.
It's so much simpler.
We aren't computers, and that's not what we mean by having things in our head.
"The patient needs more dozing/dosing."
Like in JavaScript, you can understand and remember how double equals work, if you try hard enough... Or you can just decide to always use triple equals and voila, your problem is solved once and forever.
But I don't write C, so take this with a grain of salt.
How can you identify legal C without understanding undefined behavior? Or rather, what is the list of UB that nay professional C developer should be expected to know?
> If someone has some examples that a reasonable person would write, specifically examples that look like the simplest way to do something, then I’d love to hear them.
Basically all OOP-style C. For instance, the Windows macro
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(ULONG_PTR)(&((type *)0)->field)))
The Linux kernel is also an excellent source of these.Also, serialization/deserialization libraries are an excellent source for UB. And huge amounts of code depends on UB by using unsigned char arrays for storage of other types.
Furthermore, in practice, C code assumes most of the following:
- null pointers are represented as 0, e.g. if(ptr).
- pointer provenance does not exist
- char and unsigned char are 8-bit bytes
- two's-complement arithmetic
- page-based memory protection
- no types have trap representations
- the values of base types, besides floating point, do not have multiple representations.
- etc
Off the top of my head, as a non professional C programmer:
- can’t read from uninitialized memory
- one malloc = one free
- signed overflow is undefined
- pointers to stack variables can’t outlive the stack variable itself
- can’t dereference NULL pointers
(Obviously take this with a massive grain of salt, there are almost certainly important ones that I’m forgetting.)
> null pointers are represented as 0, e.g. if(ptr).
If(ptr) is guaranteed by the standard to work. Memset(ptr, 0, len) is not (and is very broken in other ways as well).
> char and unsigned char are 8-bit bytes
They’re guaranteed to be one byte, where a byte is at least 8 bits. So unless you’re trying to simulate %256 with an implicit conversion, or relying on unsigned overflow to turn 0xFF into 0x00, then you’ll be fine.
int this_is_a_fairly_long_variable_name_1;
int this_is_a_fairly_long_variable_name_2;
Why? Because section 5.2.4.1 requires implementations to support at least 32 significant characters in external identifiers and section 6.4.2.1 says "If two identifiers differ only in nonsignificant characters, the behavior is undefined."
These ones may be more "obvious", but:
Given e.g. int a[4][5] then accessing a[1][7] is undefined even though one might assume it's like accessing a[2][2]
x << -3 is undefined behaviour though logically one might assume it should be equivalent to x >> 3
And the classic, very common newbie use of "fflush(stdin);" is also undefined.
Not saying anything about the other things, but this (in my opinion) is something that should raise eyebrows. At least to me personally that looks rather weird.
I think the second example may contain UB on a <=32-bit architecture (right shift by a value greater or equal to the number of bits), or at least this is UB in C++. On a 64-bit architecture it would be fine (but the result would not be 0).
(It's also not unique to C)
It's not obvious which actions are atomic.
And I would never excrete such a line of code without automatically thinking "hmmm.. does this really do what I think it does?"
This is an undefined operation in almost all programming languages.
There is no intrinsic reason why a[i] = i++ has to result in undefined behavior.
if i is signed, this is undefined. You'd think it's just a loop though.
while (1) for(;;)There’s no need to have any subset of the language in memory, this is the whole standard.
No, you're wrong. "Undefined Behavior" (UB) in the context of C has a very different meaning from the colloquial meaning "not specified". The meaning of UB is explicitly defined in the C standard, and the C standard explicitly lists certain program execution patterns as UB.
> If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words ‘‘undefined behavior’’ or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe ‘‘behavior that is undefined’’
Much of the stuff programmers are just used to being "part of C" is actually from a library. For example NULL is just a macro that some library provided, C's "null pointers" are just the literal zero for a pointer, even if your hardware does not represent "null pointers" by an all-zeroes value.
Unless we are speaking about the very niche use case of allocating a big blob of static memory on the global segment.
Most of the stuff done with ISO C + language extensions/external Assembly, can be as easily done in safer languages.
Language extensions and external Assembly aren't a C priviledge.
But C doesn't come with functions like memcmp() and memcpy() or memmove() either and those are pretty fundamental even if you're on a tiny constrained system that can't aspire to stuff like "strings" and "allocation" you've probably heard of data structures and copying.
Or do you mean you can't write your own malloc that complies with the OS's memory management in C?
Because I'm pretty sure you can write a malloc in ISO C when you aren't dealing with an OS (which is usually when you need to write a malloc).
Same deal as it being impossible to implement BSD sockets in ISO C because of the sockaddr funny business.
Why does the following code return 0 for most values of x? (This should be easy.)
int x;
return x == (1 && x);
The answer is that the code can return what it wants or make demons fly out of your nose, because using the automatic variable x without initializing it is essentially UB (UB in simple words in C89, unarguably UB in C11 because the address of x is not taken, and debatable in C99 but only because of poor choices of words). But I don't think this is the answer that the authors are thinking of.“It is UB” also applies to “(1 - sizeof(int)) >> 32”, the next question, on ILP32 architectures that were still prevalent when this page was written (shifting an integer type of width 32 by 32), regardless of the discussion the authors want to have about the type of sizeof(t).
“Functions and function pointers are implicitly converted to each other” is one way to describe what the C standard actually says, but that makes it look more complicated than it is. In reality, functions decay to pointers to function in the same way that arrays decay to pointers-to-first-element, and if you are familiar with the latter, it's a good way to understand the former. Only function pointers can be applied. When you write “f(x)”, f first decays to pointer to function, and then is applied. The reason you don't need to dereference a pointer-to-function p when you apply it as “p(x)” is NOT that p will be converted implicitly to a function, but that function application expects a pointer to function.
The first example in 16.3 is also Undefined Behavior, regardless of the target architecture, because the type of “3” is always “int”, so it's a poor illustration of the VC compiler bug they are referring to.
The very 1st one-- I havent done embedded C for a long time, but the 1st thing I was taught was *not* to assume uninitialised variables would be set to 0. The author probably tested on a known safe (read lab like) system.
>> can return what it wants or make demons fly out of your nose,
Indeed, this is the correct answer.
Most the questions on the page seem to be "Let's do weird crap highly dependent on the architecture/compiler, using undefined behaviour, and LOL, we can then blame C"
And not defending C here at all, moved away from it years ago. But there are better criticisms than this
Btw I agree that UB can be dangerous and hard to debug. And that "nothing hidden" doesn't really hold because it can be really tricky sometimes to figure out what the compiler did. When faced with these problems I don't say "you are coding C wrong" as some here claim we do. I would say that you need to be very careful when coding C and use all tools you can to make your life easier and the probability higher for creating better software. Use sanetizers, follow coding standards, use static analysis tools.
In general, I believe conceptual simplicity is beneficial for its wide adoption. Nobody wants to learn a language that makes them feel stupid.
I don't think that's true; the UB that is relevant for day-to-day programming is easily learned.
It's just that a sucker is born every minute and there's always someone who's been taught that C is a high level assembler. Now and then someone slips a piece of code that assumes as much in a project and disproportionately big noise is made about it.
Most UB-related bugs in C code are not there because the author didn't know it's UB, but because they simply coded it wrong. People fall into simple buffer overflows, double frees, etcetra all the time and no-one thought it's ok to overflow a buffer.
On the other hand, I'm quite confident that I don't have much UB in my code (as running sanitizers from time to time confirms). And miraculously, I've never ever been surprised by "nasal daemons".
No, the problem of undefined behaviour isn't overstated. It's the reason C and C++ have such a poor security track-record.
> I'm quite confident that I don't have much UB in my code
Plenty of C and C++ programmers are confident they don't have UB in their code, and yet we see a constant stream of security issues arising from UB in codebases developed and maintained by the smartest and most motivated C/C++ programmers, such as kernel code and the Chromium codebase.
From the Chromium project: [0]
> Around 70% of our high severity security bugs are memory unsafety problems (that is, mistakes with C/C++ pointers). Half of those are use-after-free bugs.
These bugs are, of course, undefined behaviour. These are serious bugs that presumably made it through their code-review and code-analysis processes.
(Chromium is written in C++ rather than C, but the point stands.)
> running sanitizers from time to time confirms
It doesn't. Modern static analysis tools are pretty smart, but they're a long way from being able to detect every instance of possible UB, with no false positives. If they were, we'd be in a very different place.
With the current state-of-the-art, the only way we have of developing C/C++ codebases that are free of UB, is to use formal methods (e.g. SeL4). This approach is very rarely taken, on account of the impact on development speed, and the skill needed.
[0] https://www.chromium.org/Home/chromium-security/memory-safet...
Difficult-to-detect undefined behaviour is a significant problem with C and C++, otherwise it wouldn't be a major cause of serious security vulnerabilities in well-resourced, high-profile, security-sensitive C/C++ codebases.
There may also be many instances of easily-detected undefined behaviour that exist only because of sloppy software development. There may also be many instances of undefined behaviour that are relatively benign.
> and the consequences are pretty much as expected - e.g. memory gets corrupted as a consequence of an out-of-bounds write, and then almost anything could happen.
I don't think I'm seeing your point. We agree that undefined behaviour can have serious consequences.
> while this is "undefined behaviour", it is not what people mean when they complain about UB
I don't follow. Undefined behaviour is an unambiguous term of art in C and C++ programming. There are plenty of common misconceptions about UB, sure enough, but the term itself is precise.
Do you mean difficult-to-detect but "obviously" bugs that can lead to UB, like buffer overflows that only happen in rare circumstances (like with unsanitized input)? Or do you mean difficult to detect unexpected "miscompilations" by the compiler based on some UB that is non-obvious to most programmers and/or not well-known?
Because I was referring to the latter, and in my perception most talk about UB is. I haven't seen the latter happen myself, and I haven't read that many horror stories where this actually happened.
For better or worse, I'm not angry with the compiler if I code a logic bug and corruption happens. That's just what I expect.
Maybe if you're indeed saying the latter is a "major cause of security vulnerabilities", could you provide a few examples where it's the language's or compiler's "fault"? I can see that the line is not well defined here of course, because technically it's all just UB - but the distinction really was my point, which I made from a practical perspective.
> Do you mean difficult-to-detect but "obviously" bugs that can lead to UB [...]? Or do you mean difficult to detect [...] UB that is non-obvious to most programmers and/or not well-known?
I meant both, as I wasn't drawing this distinction.
> I haven't read that many horror stories where this actually happened
You may be right that most of the problematic instances of UB are doing something that's clearly not right even to inexperienced programmers, but again I don't see much value in making the distinction. I mentioned the Chromium stats earlier. Those security issues are real. Many of those issues presumably wouldn't happen in a language that prevented UB.
(It's not necessarily the case that all the issues would have been avoided, as it's possible a bug might still have manifested, but in a well-defined way. A UB-invoking memory-management bug might correspond to a failure to reset a data-structure, in a safe language. Only the former invokes UB, but both could have security consequences.)
> For better or worse, I'm not angry with the compiler if I code a logic bug and corruption happens. That's just what I expect.
You expect it because you know the C and C++ languages don't offer robust means to avoid undefined behaviour. This doesn't generalise to other languages.
In C and C++, silent data-corruption can occur if you try to write into an array but go out-of-bounds. That doesn't happen in many other modern languages. Unlike Ada, C doesn't even let you enable robust runtime bounds-checks for your debug builds. (To my knowledge no C compiler is able to offer this feature.)
> Maybe if you're indeed saying the latter is a "major cause of security vulnerabilities", could you provide a few examples where it's the language's or compiler's "fault"?
I wasn't drawing the distinction at all. I'll use an example from the plainly erroneous category.
Use-after-free and double-free errors are both undefined behaviour, in C and in C++. In Safe Rust, they cannot ever arise because of the way the language is defined, so the door is closed on those forms of runtime error. We don't need to talk about blame.
Memory-management issues of this sort are precisely the kinds of issues that affect Chromium. Not even Google can keep undefined behaviour out of their most high-profile C++ codebase. (In fairness though, the codebase would likely be quite different if written from scratch today in modern C++.)
On the not plainly erroneous side: a subtlety that can result in security consequences is the elision of a memset call by an optimising compiler which can result in sensitive data not getting zeroed out before deallocation. This isn't actually UB, but it's a good example of 'language lawyer' trouble with C/C++. [0]
As an aside: I once spotted a competent C++ programmer (vastly more experienced than myself) writing code which applied memcpy to an array of non-POD objects. If you'll forgive a painful mix of metaphors: C++ is large enough that even experienced programmers can have blind spots to its dark corners.
Valgrind offers bounds checks with malloc() allocations and probably other allocations, and while I don't have extensive experience with valgrind it works surprisingly well. I imagine runtime checks could be made possible for any allocator by offering a slice(ptr, type, count) builtin call. I don't really see the language causing any problems, it's more what compiler output and optimizations we've come to expect.
> Memory-management issues of this sort are precisely the kinds of issues that affect Chromium.
They're just what a C programmer expects. I'm not saying you have to like the outcome but this is not "UB" for me - in the sense that it's been utterly obvious from day 1 that this type of issue can cause corruption (because how would it not?), long before there was any talk about UB.
> As an aside: I once spotted a competent C++ programmer (vastly more experienced than myself) writing code which applied memcpy to an array of non-POD objects. If you'll forgive a painful mix of metaphors: C++ is large enough that even experienced programmers can have blind spots to its dark corners.
I'm not defending C++ (in fact I actively avoid having to deal with it) but I think it's obvious that this sort of code is extremely likely to break.
Right, and Valgrind is very impressive, but it's a very intrusive and heavyweight tool, rather than a compiler flag. (I often mention Valgrind by name in these sorts of discussions, [0] we're thinking along the same lines here.)
> I imagine runtime checks could be made possible for any allocator by offering a slice(ptr, type, count) builtin call
It would presumably need 'fat pointers', which have ABI issues.
It would need to cope with statically allocated arrays, heap-allocated arrays, and arrays with automatic lifetime, including VLAs and alloca. It would also need to cope with passing a pointer mid-way into an array (something forbidden in, say, Java).
There would be considerable payoff to solving this problem, but it isn't an easy problem to solve, which is why no compiler does so.
> I don't really see the language causing any problems, it's more what compiler output and optimizations we've come to expect.
No, it's the language. The behaviour of modern optimising compilers is consistent with the way the languages is defined. The way the language is defined, and the categories of undefined behaviour that it permits, are very consequential.
I already gave the example of how arrays work differently in Ada, such that Ada compilers can easily add bounds checks for dev builds, whereas C compilers cannot so you need Valgrind, which few people use. (Well, more properly, arrays work differently in C than in just about every other language.) I could give a dozen other examples of ways C permits you to introduce serious bugs into your codebase which other languages robustly defend against.
Again, many categories of bug simply cannot occur in other safer languages, because of the way safe languages are defined. Use-after-free, double-free, signed integer overflow, read-before-write, divide-by-zero, out-of-bound access, mis-aligned access, data-races. Using a language like Safe Rust closes the door on every one of those kinds of undefined behaviour.
Recall that these are precisely the kinds of errors that result in major security vulnerabilities. This isn't purely academic. Safe languages like Safe Rust (or even plain old Java, although Java has some unsafe corners) stops those issues arising, and that means better security.
Also, as we've discussed, the way C is defined makes it difficult for C compilers to use robust compile-time or even run-time checks to detect undefined behaviour. The result is that major C codebases get delivered with undefined behaviour bugs, which are a common source of major security vulnerabilities.
> I'm not saying you have to like the outcome but this is not "UB" for me - in the sense that it's been utterly obvious from day 1 that this type of issue can cause corruption (because how would it not?), long before there was any talk about UB.
Respectfully there is no such thing as 'UB for me'. It's an accepted technical term-of-art with a clear definition. People do PhDs on this topic. [1]
You're not engaging with the points I've made about how other languages are defined in such a way as to prevent these issues arising. You seem to be focussing on how it's the programmer's fault, which isn't the point at all.
Also, operations which cause data-corruption in C aren't always this way in other languages. In Java, an out-of-bounds write (into an array) results in an exception being thrown, for instance. In verified SPARK Ada, out-of-bounds writes cannot arise in the first place.
> long before there was any talk about UB
Prior to the standardisation of the C language that may have been true in the sense that perhaps the term undefined behaviour had not yet been coined, but that doesn't have any bearing on our discussion. C was always an unsafe language.
> I think it's obvious that this sort of code is extremely likely to break
And yet it didn't occur to the experienced C++ programmer. The whole idea of non-POD types isn't obvious to a C++ programmer who started out with C.
To understand C or C++ well you need to essentially know the language spec. You can't simply learn by doing, as you may easily be tricked into thinking that bad code is correct and robust. It isn't obvious that unsigned integer addition overflows by wrapping whereas signed integer addition overflow causes undefined behaviour.
Google's Chrome team are unable to keep undefined behaviour out of their (necessarily large and complex) C++ codebase. It's unlikely that you're smarter than them. Even if you are, undefined behaviour continues to be a problem for real C/C++ codebases, resulting in a steady stream of security issues.
Arguing like that and then saying I'm not "engaging with the points you've made" after you've been talking completely aside the points of my OP, well... you're being a bit of a pain in the butt. I totally get your points, so let's agree that we're just looking for different things.
(Btw, a couple of days ago I did try Rust once again for a few hours, intending to convert a simple toy project to it. After a few hours of fighting the compiler, editing boilerplate files, looking for the right crates for basic Win32 interop, waiting for downloads, etc... I quit without making it work. Nothing changed in my feeling that without an extreme (or potentially infinite) investment of energy, C will continue to be more productive for me personally, for what I do - despite all its flaws).
Agreed. Trying to know every single dark corner/UB of the language is not needed.
The most bugs in C don't come from undefined behavior, but from plain coding mistakes. That are 98% related to memory (mis)menagement.
From that point of view, C is simple.
These examples are fun, but having written C for some time, I would not accept these into a codebase. I don't need to know how the compiler behaves with those to know that it's going to be something weird.
I don't think it is meaningfully harder to understand translation into assembly between C and C++. Sure, you'll have to learn what a vtable is, and possibly think about exceptions, but if you have debugged some C++ assembly a few times, there shouldn't be anything surprising in there.
Yes, destructors and overloaded operators make the "what is the code doing" part harder to figure out from plain text, but that makes no difference for the emitted instructions (compared to an explicit function call in C). The degree of "magic" in terms of assembly depends more on how good your compiler is at optimizing than whether you use C or C++.
A big problem for the analysis this old project attempts is that C makes no effort to clearly define any circumstances it doesn't care about and yet many such circumstances are likely to arise in real world programs. I would consider this a defect in a modern language, but of course its proponents disagree.
+1. I think the simplicity of C (and also Unix) was a great idea when it first started, it allowed so much productive explorations and works to be done in a short amount of time. Unfortunately, after decades of development, programs and systems were getting increasingly sophisticated to the point that the original design was not prepared for. Then one starts to suffer from a myriad of design limitations, and people start adding complicated extensions and workarounds to the existing platform, and eventually you get something that is not simple at all.
At this point, the simplicity became a shadow of its former self.
For example, in C, nobody could tell exactly how many undefined or unspecified behaviors exist in a program. Historically, every major optimization technique in a new compiler releases would uncover some unintentional undefined behaviors and would break existing programs. Recently, to solve this problem, UndefinedBehaviorSanitizer has been developed, which is technically a super complex tool. In the same way, the memory safety problem led to the creation of stack canary, NX, ASLR, PIE, CFI, AddressSanitizer, large-scale automated fuzz testing, none of them is simple. Arguably, all of these complexities can be removed if the programming environment itself has a better memory model.
For another example, C stdlib's minimalism caused many attempts to create a powerful standard library. While C++'s STL and Boost are notorious for their all-encompassing features, but arguably the recreation of a homegrown library in all C projects have made them more complex. Not to mention the C stdlib's minimalism is also a shadow of its former self - people claim one could essentially internalize all the functions, well this could be true for ANSI C, but how many people know that C99 has native support for complex numbers? Or how C11 added multithreading?
The Unix situation is similar. When it got popular enough, people started to add more and more features and extensions to satisfy their needs. Nowadays everyone is saying that X is bloat, but if you're a retrocomputing fan and look into the history, you'll see that this has already been the trend ever since the popularization of Unix. It has already began during the early BSD development at Berkeley - students added tons of command line utilities and optional arguments which was heavily criticized by Rob Pike. During the Unix Wars, various vendors also packed lots of features often in a hurry and in a manner that lacked consistency, which was subsequently adopted by everyone because POSIX was the peace agreement. When 4.3BSD started supporting POSIX, ironically, this time it came under attack by some as moving away from the BSD philosophy of simplicity! Later, GNU also added tons of features to the system utilities, sometimes explicitly went against Unix's design to address its perceived limitations (GNU's Not Unix, after all). Eventually there are Linux kernel and systemd, which clearly do more than what a kernel or a service manager is supposed to do. Linux kernel even has Transport Layer Security now!
Most of the feature expansions in C or Unix really have useful purposes and solves real problems in spite of the claims of "bloat". The problem is you have an aging foundation with a false pretense of simplicity, and when extensions are added to meet the demands, they create a lot more complexities in real applications, and the "simple" system with extensions is actually more complex than a "complex" system with a more elaborate design.
Don't get me wrong. I still find the C Programming Language is an excellent work of technical writing, its concise and minimalist style has its own elegance. Also, I still like The Unix Programming Environment as described by Rob Pike, it's revealing how much useful work can be done flexibly in pipes and shell scripts. However, I have to say that the computing needs have progressed far beyond those days - I know how Worse is Better works, I also genuinely believe that the adaption of Unix as a base platform and the "exploitation" of its viral nature by the free software community is one of the most important successes in history - But all I want to say is that, Worse is Better does have its costs.
Probably the best non-fiction book I own, Ambrose Bierce and Randall Munroe's books are funnier, my X references are more useful as monitor stands, and my sister's book about cloth (she's an embroiderer) is much prettier, but K&R is the best book.
Perhaps it's notable that my "Student's Introduction to English Grammar" by Huddleston and Pullum is of similar size to K&R. Of course, Huddleston and Pullum merely happened upon English's Grammar already fully formed, which they can only try to explain in retrospect, whereas Ritchie wrote the first C compiler.
1. This is straight up unacceptable in any reasonable shop. You must initialise variables before use. This will be flagged at the static analysis step inside the CI/CD pipeline, and your code won't make it to production.
2. sizeof returns a size_t, not an int. This is basic knowledge. Just make it a habit never to perform arithmetic on size_t without casting. This will also probably fail in static analysis.
3. Don't use the same names for globals and locals. Make some kind of rule where globals always have to start with g_. Solved.
4. Just don't do this. I guarantee that you will never need to do strange things like this. As I said, C's problems appear when people are showing off.
5. Please just make two structs, and put one inside the other. Don't do this. It is never required.
I am not going to go on now. It just keeps getting more unnecessary and more ridiculous. There is absolutely no situation that requires you to write C code like this. IMO if you want to show off how smart you are, write code that is obvious at first glance to a kid just out of college. Writing useful code that is easily understandable is a real art, and demonstrates true expertise with the language.
> The following examples were actually encountered either in real programs or are taken from the ISO C99 standard or from the GCC’s testcases.
I have definitely seen (and probably even written) show-off code in C++ and other languages, but when it comes to C, I feel like the tricky stuff tends to be borne of necessity. For example, I test a code base that requires some type punning. The last time I investigated this, there didn't seem to be consensus on how to do this without UB.
Signed overflow is another bugbear in C. Code that worked perfectly fine for over twenty years was suddenly at the center of mysterious bugs when a new version of GCC decided to exploit some UB.
Lua has nearly the same length, small reference too.
Just for fun, I tried the last example (VC ugliness, 16.3, example 1.) and got this
warning C4293: '>>': shift count negative or too big, undefined behavior
and a program output of -1 using MSVC 2022 (Microsoft C/C++ Optimizing Compiler Version 19.31.31104 for x64).This made me ponder whether the bugs and wrong implementations mentioned have been fixed for quite some time at this point.
C still makes it trivial to write memory safety bugs. No one who likes C, especially not the committee, has any appetite whatsoever for improving the situation. I don't see that changing any time soon.
C isn't a language to force you to write guaranteed 100% memory-safe UB-free code. But it's still quite a bit easier to do so in C than assembly for each platform you want to target, and you can get pretty damn close to that assembly you would've otherwise written.
(I in particular do not want any forced memory safety - I've written a custom memory manager with its own semantics of ownership; the language doing anything about safety would just prohibit me doing what I want)
Then again, we guys in security need job safety anyway.
It is like selling food on a shitty street joint, people might keep on doing that, until the day sanitary health check comes around and closes it down.
The first few examples show the author is not well-versed in C.
It's true that C's semantics are not that simple, but one should know a language better before deciding to write an interpreter loop for it.
> GCC ugliness
Does the author not know about -std=c99 or -std=c11, which turns GNU extensions off?
C, is somewhat practical though.
That already breaks as soon as someone tries to compare function pointers, no?
Why did C++ make it so big then? There's a good talk about this from Herb Sutter: https://www.youtube.com/watch?v=wIHfaH9Kffs
(Also check out minute ~20 for discussion of your exact question!)
Is it easy to write programs in C? No. But C makes damn sure that what you wrote is what will get executed. Unless you like playing smart, like the guy in the opening post.