One of the few advantages of ISO standardization is you can just read the associated papers to answer questions like this: https://wg21.link/p2312
The quick bullet points:
* Surprises when invoking a type-generic macro with a NULL argument.
* Conditional expressions such as (1 ? 0 : NULL) and (1 ? 1 : NULL) have different status depending how NULL is defined
* A NULL argument that is passed to a va_arg function that expects a pointer can have severe consequences. On many architectures nowadays int and void* have different size, and so if NULL is just 0, a wrongly sized argument is passed to the function.
For starters, you have to #include a header to use it.
stddef.h on the other hand is required by most to get size_t
Just include the stddef.h header if you want to use NULL, similarly to how you include a header file if you want to use anything else, e.g. bool from stdbool.h.
[1] I am not entirely sure in retrospect, actually, as I might be misremembering, but my point stands with or without stdio.h!
_BitInt(N) isn't typically used directly but typedef'ed to the width you need, e.g.
typedef _BitInt(2) u2;
The 'ugly' _B syntax is needed because the combination of underscore followed by a capital letter is reserved in the C standard to avoid collisions with existing code for every little thing added to the language (same reason why it was called _Bool).AFAIK defer didn't actually make it into C23?
I'm also more on the conservative side when it comes to adding features to the C standard, but IMHO each of the C23 additions makes sense.
Both have compatibly implemented the standard C++ auto. Since 2011 or so.
Here's an example where Clang and GCC don't agree about the behaviour of auto in C23:
https://www.godbolt.org/z/WchMK18vx
IIRC Clang implements 'C++ semantics' for C23 auto, while GCC doesn't.
Last time I brought that up it turned out that both behaviours are 'standard compliant', because the C23 standard explicitly allows such differing behaviour (it basically standardized the status quo even if different compilers disagreed about auto semantics in C).
PS: at least Clang has a warning now in pedantic mode: https://www.godbolt.org/z/ovj5r4axn
Did you mean gcc? Your link shows a gcc error:
<source>:3:5: error: 'auto' requires a plain identifier, possibly with attributes, as declarator
3 | auto* p = &i;
| ^~~~Correct, defer didn't make it into C23.
It (in its __attribute__((cleanup())) form) is also one of the most useful extensions in GCC/clang — but, again, for use in macros.
The old definition did not even specify wether it was a pointer or an integer. So for platforms that did not follow the Posix ((void*)0) requirement it was a foot gun that had neither the type nor the size of a pointer.
> On top of that "constexpr" and "nullptr" just reeks of C++.
Probably because they where back ported from C++. You can still use NULL, since that was apparently redefined to be nullptr.
In my opinion, complexity doesn't scale linearly like this. Sometimes, in fact often times, having more complex tools means a simpler process and end result.
It's like building a house. A hammer and screwdriver are very simple. A crane is extremely complex. But which simplifies building a house? A crane. If I wanted to build a house with only a hammer and screwdriver, I would have to devise incredibly complex processes to get it done.
You see the same type of thing in programming languages. Making a generic container in C++ is trivial. It's very, very hard in C. You can make it kind of generic. You can use void * and do a bunch of manual casting. But it's cumbersome, error prone, and the code is more complex. It's counter-intuitive - how can C, a simpler language, produce code that is more complex than C++?
Or look at std::sort vs qsort. The power of templates and functors makes the implementation much simpler - and faster! We don't have to pass around void * and dereference them at runtime, instead we can build in comparison into the definition of the function itself. No redirection, no passing on the stack, and we can even go so far as to inline the comparison function.
There's really lots of examples of this kind of stuff. Point being, language complexity does not imply implementation complexity.
The constraints of the tool are inherited in the program; if the constraints encourage better design, then the program will have a better design. You benefit from the language providing a path of least resistance that forces intentionality. That intentionality makes the code easier to reason about, and less likely to contain bugs.
You do pay for this by writing more boilerplate, and by occasionally having to do some dirty things with void pointers; but these will be the exception to the rule, and you'll focus on them more since they are so odd.
Also, I wouldn't be saying this if people didn't constantly try to recreate C++-isms in C. Which sometimes you need to do. So, then you have this strange amalgamation that kind of works but is super error prone and manual.
I also don't necessarily agree that C's constraints encourage better design. The design pushes far too much to runtime, which is poor design from a reasoning point of view. It's very difficult to reason about code when even simple data models require too much indirection. Also, the severely gimped type system means that you can do things you shouldn't be able to do. You can't properly encode type constraints into your types, so you then have to do more validation at runtime. This is also slightly improving, starting with _Bool years ago.
C++ definitely is a very flawed language with so, so many holes in its design. But the systems it has in place allows the programmer to more focus on the logic and design of their programs, and less on just trying to represent what they want to represent. And templates, as annoying as the errors are, prevent A LOT of runtime errors. Remember, every time you see a template that translates into pointers and runtime checks in C.
I also think that it wouldn't be bad for code to be more generic. It is somewhat unnecessary for a procedure to allow an argument of type A but not of type B if the types A and B share all the commonalities necessitated by the procedure. Of course procedures with equivalent source code generate different machine code for different types A or B, but not in a way that matters much.
I believe it is beneficial for the language to see code as the description of a procedure, and to permit this description to be reused as much as possible, for the widest variety of types possible. The lack of this ability I think might be the biggest criticism I have for C from a modern standpoint.
Basically you can write a function that takes a tagged union and the compiler will passed the correct union based on named arguments.
int ret = foo(.slice = name);
int ret = foo(.src = str, .sz = strlen(str));This code has a bug, and may even crash on some architectures:
execlp("echo", "echo", "Hello, world!", NULL);
This code doesn't have the bug: execlp("echo", "echo", "Hello, world!", nullptr);
Neither does this: execlp("echo", "echo", "Hello, world!", (char *)NULL);
"_BitInt(N)" is also ugly, reminds me of "_Bool" which is thankfully "bool" now.
[1] guard, defer, auto, constexpr, nullptr (what is wrong with NULL?), etc. On top of that "constexpr" and "nullptr" just reeks of C++.
That said, Modern C is an incredible book, I have been using it for C99 (which I intend to continue sticking to).