C23: a slightly better C

One of the established and most popular programming languages is the C programming language. It is relatively easy to learn, and highly practical.

Maybe surprisingly, the C programming language keeps evolving, slowly and carefully. If you have GCC 13 or LLVM (Clang) 16, you already have a compiler with some of the features from the latest standard (C23).

// Only include stdio.h if it exists
#if __has_include (<stdio.h>)
  #include <stdio.h>
#endif

#include <stdlib.h>

[[deprecated]]
void f() {}

[[nodiscard]]
int g(int x) {
  return x + 1;
}

int main() {
  f(); // compile-time warning: 'f' is deprecated
  g(1); // compile-time warning
  auto x = 0b1111;
  typeof(x) y = 1'000'000; // type of y is the same as x
  printf("%d\n", x); // prints 15
  printf("%d\n", y); // prints 1000000
  constexpr int N = 10;
  // compile-time asserts using static_assert
  static_assert (N == 10, "N must be 10");
  bool a[N]; // array of N booleans
  for (int i = 0; i < N; ++i) {
    a[i] = true;
  }
  printf("%d\n", a[0]); // prints 1
}

  1. The first part of the code contains some preprocessor directives, which are instructions for the compiler to process the source code before compiling it. The #if directive checks a condition at compile time and includes the following code only if the condition is true. The __has_include macro is a feature of C++17 adopted by C23 that checks if a header file exists and can be included. In this instance, it is not useful because we know that stdio.h is present, but in other instances, this can prove useful to determine what headers are available.
  2. The next part of the code defines two functions with attributes, which are annotations that provide additional information to the compiler about the behavior or usage of a function, variable, type, etc.
    • The [[deprecated]] attribute is a feature of C++14 adopted by C23 that marks a function as obsolete and discourages its use. The compiler will issue a warning if the function is called or referenced.
    • The [[nodiscard]] attribute is a feature of C++17 adopted by C23 that indicates that the return value of a function should not be ignored or discarded. The compiler will issue a warning if the function is called from a discarded-value expression.

    In this case, the function f is deprecated and does nothing, while the function g returns the input value plus one and should not be ignored. The first two lines of the main function call the functions f and g and trigger the warnings.

  3. The third line of the main function declares a variable x with the auto keyword, which is a feature of C++11 that lets the compiler deduce the type of the variable from its initializer. In this case, the initializer is a binary literal, which is a feature of C++14 and adopted by C23 that allows writing integer constants in binary notation using the prefix 0b. The value of x is 0b1111, which is equivalent to 15 in decimal.
  4. The fourth line declares another variable y with the typeof operator that returns the type of an expression. In this case, the expression is x, so the type of y is the same as the type of x. The initializer of y is a digit separator, which is a feature of C++14 adopted by C23 that allows inserting single quotes between digits in numeric literals to improve readability. The value of y is 1’000’000, which is equivalent to 1000000 in decimal.
  5. The seventh line declares a constant variable N with the constexpr keyword, which is a feature of C++11 adopted by C23 that indicates that the value of the variable can be evaluated at compile time. The value of N is 10. Previously, one would often use a macro to define a compile-time constant (e.g., #define N 10).
  6. The eighth line uses the static_assert keyword, which is a syntax of C++11 adopted by C23 that performs a compile-time assertion check. The keyword takes a boolean expression and an optional string message as arguments. If the expression is false, the compiler will emit an error and stop the compilation, displaying the message. If the expression is true, the compiler will do nothing. In this case, the expression is N == 10, which is true, so the compilation continues.
  7. The ninth line declares an array of N booleans named a. An array is a collection of elements of the same type that are stored in contiguous memory locations. The size of the array must be a constant expression for standard C arrays (otherwise it becomes a variable-length array which may be less efficient), which is why N is declared with constexpr. We also use the keywords true and false which become standard keywords in C23.

There are many more features in C23, but it will take some time for compilers and system librairies to catch up.

My thoughts so far:

  • The introduction of constexpr in C will probably help reduce the dependency on macros, which is a good idea generally. Macros work well in C, but when a bug is introduced, it can be difficult get meaningful error messages. It does not happen too often, but in large code bases, it can be a problem.
  • I personally rarely use auto and typeof in other languages, so I don’t expect to use them very much in C. In some specific cases, it can greatly simply one’s code, however. It is likely going to help reduce the reliance on macros.
  • The idea behind static_assert is great. You run a check that has no impact on the performance of the software, and may even help it. It is cheap and it can catch nasty bugs. It is not new to C, but adopting the C++ syntax is a good idea.
  • The __has_include feature can simplify supporting diverse standard libraries and test for available libraries. For example, it becomes easy to check whether the standard library supports AVX-512. If a header is missing, you can fail the compilation with instructions (e.g., you need to install library X). It is generally a good idea for people who need to write portable code that others can rely upon.
  • I did not include the introduction of `char8_t` in the language. I worked extensively with Unicode in C++ and I have not found good use cases for the `char8_t` type so far: `char` is always sufficient in my experience.

Daniel Lemire, "C23: a slightly better C," in Daniel Lemire's blog, January 21, 2024.

Published by

Daniel Lemire

A computer science professor at the University of Quebec (TELUQ).

48 thoughts on “C23: a slightly better C”

  1. The introduction of consexpr in C will probably help reduce the
    dependency on macros, which is a good idea generally.

    Defining constants is probably the most harmless and easiest to debug usage of macros in C, so I wouldn’t be that optimistic.

    That said, pleasant surprises from C. It seems they finally picked up a bunch of easy quality-of-life improvements from C++.

  2. Thanks for your indepth yet simple introduction to the new features of C. I love C language (not C++) but in these last years I am writing more code in other languages than C.
    It is great to see that C continues to improve.

  3. Looks like good features, beside of auto. auto is the new goto. There is nearby no place where auto makes any sense and only obfuscates which datatype is really used.

    1. C libraries should prefix all their externally visible symbols with something like a “namespace” (e.g. xyz_func1, xyz_func2, etc.). Any C lib that doesn’t do so is poorly written and probably not the kind of thing you’d want to use anyway.

  4. Static assertions were added in C11 already. C23 just deprecates the old “_Static_assert” spelling and makes the message argument optional.

  5. I don’t like the double square bracket notation. This will make it annoying to embed bits of this C in markdown etc which already use that to mean hyperlinks.

    Why aren’t these pragmas? That notation already exists for C and could be used for other compile-time checking hints.

          1. There is no current semantics for overloading operators in C…

            My proposal is not to copy C++’s overloading feature, you should read the paper I linked.

  6. Seems like C23 is filling up with bloat.

    constexpr is great at making your code less reusable, a design philosophy REJECTED by The original C designers but the code Nazis wanting to force others to program “the right way” (not too different than the people wanting to force others to speak “the right way”) have finally worn down the standards committee into submission…

    I have never seen a usage of “auto” that i wouldn’t object to in a Google SWE C++ readability code review. So happy to see they are expanding the opportunity to make this mistake …

    And so on and so forth …

    1. Bloat? Not having decent compile-time language features is what adds actual bloat, i.e. the sort you can measure in machine code and execution time.

      I’m curious to hear your opinion about things like _Generic() and “stdatomic.h` . Or C11 in general.

      If anything, C23 is a very conservative step. There are still many more to take ot will genuinely improve C while staying true to purpose.

      All that aside, here’s a tongue-in-cheek suggestion for a native template syntax:

      #template<my_tmpl.h>

  7. Recently I was thinking (in the context of C++, but still) that the auto keyword is very useful if you use an IDE which supports replacing it with the explicit type. For long types, I sometimes now find myself writing auto only to swap it a few instants later. With shortcut its much faster that writing out or sometime verifying the correct type through context.

  8. I love the __has_include feature, but not as just a guard to include: it can make portability easier (no need for loads of compilee -D options). It could be useless however if Visual Studio takes 10 years to support it.

    I don’t see how the auto keyword can actually work in real code: see the subsequent printf in the example that assumes that this is an int (“%d”). So the auto type may not always work and cause hard-to-find bugs. It is bad when one cannot write a small usage example without hitting a basic consistency problem..

    The typeof keyword can be useful even if auto is not used: you make the type decision only once and it is easier to change it later. However it can make code convoluted if that dependency propagates through several modules. Using typedef is still the gold standard, IMHO.

    You can use true and false, but you cannot print them?? (It prints as 1 in the example.)

    Many of these changes look like half backed designs.

    1. Printf’s quirks aren’t even tangential to auto typing. If you want to spice up your format strings you can make use of _Generic() to some extent.

      Also, auto is basically just a shorter typeof. Many of the benefits you see in typeof apply to auto just as well.

      #define shitty_auto(x, ...) \
      typeof(__VA_ARGS__) x = __VA_ARGS__

  9. I do a lot of embedded development, but where available, I typically choose C++, primarily due to things like template and constexpr functions and such. I use metaprogramming to efficiently deal with things like graphics code, while maintaining flexibility – something you can’t really do in many cases in C without code generation during the build step – the preprocessor can only get you so far in that regard.

    I am thrilled to see constexpr gaining ground in C. If I could start achieving compile time expressiveness that I can in C++ I’d have little reason to use C++. As it is I already forgo the STL.

  10. “You can use true and false, but you cannot print them?? (It prints as 1 in the example.)”

    The handling of enum’s is still underdeveloped in the C-Space.
    The compiler knows the values and the keywords. Why is there no min- and max-value? Why no enum to const char *? Of course printf would not work, but (const char *)a[i] could, since a cast from enum to const char * does not have to go through int.

    1. Why no enum to const char *? Of course printf would not work, but (const char *)a[i] could, since a cast from enum to const char * does not have to go through int.

      What do you mean with casting enum to const char pointer?

      1. The Compiler could calculate the Text value of the enum. In case of boolean “true” or “false”. Just, what is declared for this value or NULL, if the value is not declared in the enum. This generates a Lot of Strings, If used often, but Most compilers can handle This.

Leave a Reply

Your email address will not be published.

You may subscribe to this blog by email.