Most active commenters
  • johnisgood(5)
  • (5)
  • flohofwoe(3)

←back to thread

The C23 edition of Modern C

(gustedt.wordpress.com)
515 points bwidlar | 28 comments | | HN request time: 2.34s | source | bottom
1. johnisgood ◴[] No.41854897[source]
Personally this[1] just makes C much more complicated for me, and I choose C when I want simplicity. If I want complicated, I would just pick C++ which I typically would never want. I would just pick Go (or Elixir if I want a server).

"_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).

replies(10): >>41854987 #>>41855182 #>>41855214 #>>41855526 #>>41855553 #>>41856362 #>>41857239 #>>41859032 #>>41860073 #>>41935596 #
2. cornstalks ◴[] No.41854987[source]
> what is wrong with NULL?

For starters, you have to #include a header to use it.

replies(2): >>41855143 #>>41855812 #
3. zik ◴[] No.41855143[source]
And it avoids the NULL == 0 ambiguity, allowing for better type checking.
4. ◴[] No.41855182[source]
5. mmphosis ◴[] No.41855214[source]
NULL is not wrong. The things that I will do with NULL are
replies(1): >>41855486 #
6. ◴[] No.41855486[source]
7. ◴[] No.41855526[source]
8. nickelpro ◴[] No.41855553[source]
> what is wrong with NULL?

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.

replies(1): >>41859624 #
9. johnisgood ◴[] No.41855812[source]
Well, I always include stdio.h which includes stddef.h that defines NULL as (void *)0.
replies(1): >>41855960 #
10. dhhfss ◴[] No.41855960{3}[source]
In my experience, hardly any source files require studio.h

stddef.h on the other hand is required by most to get size_t

replies(1): >>41859219 #
11. flohofwoe ◴[] No.41856362[source]
auto is mostly useful when tinkering with type-generic macros, but shouldn't be used in regular code (e.g. please no 'almost always auto' madness like it was popular in the C++ world for a little while). Unfortunately there are also slight differences between compilers (IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime).

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

replies(2): >>41857227 #>>41857765 #
12. humanrebar ◴[] No.41857227[source]
> IIRC Clang implements a C++ style auto, while GCC implements a C style auto, which has subtle differences for 'auto pointers' - not sure if those differences have been fixed in the meantime

Both have compatibly implemented the standard C++ auto. Since 2011 or so.

replies(1): >>41857438 #
13. ◴[] No.41857239[source]
14. flohofwoe ◴[] No.41857438{3}[source]
Well, not in C :)

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

replies(2): >>41859640 #>>41860573 #
15. eqvinox ◴[] No.41857765[source]
> AFAIK defer didn't actually make it into C23?

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.

16. josefx ◴[] No.41859032[source]
> (what is wrong with NULL?)

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.

replies(1): >>41859241 #
17. johnisgood ◴[] No.41859219{4}[source]
You are right. Hereby I correct my parent comment: I talked about my own personal experience[1], but yeah, as you said, stddef.h is often required (and yes, often I do not need stdio.h, stddef.h is what I need) which defines NULL, which was my point. If it is often required, then it does not matter whether you have to include a header file or not, IMO.

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!

18. johnisgood ◴[] No.41859241[source]
What platforms are those that are in use, and how widespread their use is?
19. ◴[] No.41859624[source]
20. johnisgood ◴[] No.41859640{4}[source]
This difference of implementation in two of the major C compilers leaves a bad taste in my mouth. :/
21. consteval ◴[] No.41860073[source]
> If I want complicated, I would just pick C++ which I typically would never want

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.

replies(1): >>41864908 #
22. cpeterso ◴[] No.41860573{4}[source]
> 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;
        |     ^~~~
replies(1): >>41860919 #
23. flohofwoe ◴[] No.41860919{5}[source]
No, GCC is right to error there, because the code uses a C++-ism (the '*' after 'auto' only makes sense in C++ but not in C).
24. swells34 ◴[] No.41864908[source]
In my experience, complex tools encourage fluffy programming. You mention a generic container; if I were using C, I just wouldn't use a generic container; instead, I'd specify a few container types that handle what needs handled. If there seem to be too many types, then I immediately start thinking that I'm going down a bad architecture path, using too many, or too mixed, abstraction layers, or that I haven't broken down the problem correctly or fully.

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.

replies(2): >>41870621 #>>41898956 #
25. consteval ◴[] No.41870621{3}[source]
Sometimes, but I would argue that C is too simplistic and is missing various common-sense tools. It's definitely improving, but with things like namespaces there's pretty much no risk of "too complex" stuff.

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.

26. avvvv ◴[] No.41898956{3}[source]
I think that is fair. A simple language with a simple memory model is nice to work with.

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.

replies(1): >>41899090 #
27. Gibbon1 ◴[] No.41899090{4}[source]
I feel that if C had tagged unions and a little sugar you could write non magical generic functions in C. Non magical meaning unlike C++ etc instead of the compiler selecting the correct function based on the arguments the function itself can tell and handle each case.

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));
28. jlokier ◴[] No.41935596[source]
> what is wrong with NULL?

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);