C23 features

The course compiles with the GCC 15 default, which is C23, so the features in this lesson are available without a flag and were used in passing in earlier lessons. Here they are on their own: type inference for declarations, compile-time constants, a typed null pointer, standardised attributes and readable numeric literals.

typeof

typeof names the type of an expression, which saves a repeated declaration when the type is long or comes from a macro. typeof_unqual drops the qualifiers, so a const source gives a writable copy. Both are resolved at compile time and cost nothing at run time.

#include <stdio.h> int main(void) { int value = 5; typeof(value) copy = value + 1; typeof_unqual(const int) plain = 2; printf("%d %d %zu\n", copy, plain, sizeof copy); return 0; }

gcc -Wall -Wextra typeof.c -o typeof
./typeof

6 2 4

The copy has the type of value, an int of four bytes, and the plain variable drops the const of its source. The keyword is useful in macros, where the type of an argument is otherwise unknown, and it replaces the __typeof__ extension.

constexpr

constexpr declares an object whose value is computed at compile time and can be used where a constant expression is required, such as an array size. Unlike a macro, it has a type, and unlike a plain const, it is guaranteed to be constant.

#include <stdio.h> constexpr int LIMIT = 10; int main(void) { int values[LIMIT] = {0}; printf("limit: %d\n", LIMIT); printf("size: %zu\n", sizeof values); return 0; }

gcc -Wall -Wextra constexpr.c -o constexpr
./constexpr

limit: 10
size: 40

The array of ten ints takes forty bytes, and the size is a compile-time constant because LIMIT is one. A const int would not work in that position in C, which is why the macro or the enumeration was the old answer. The constexpr object still has an address if the program takes one.

auto and nullptr

auto in C23 means type inference, not the old storage class: the type comes from the initialiser. nullptr is a null pointer constant of type nullptr_t, which converts to any pointer type without the integer ambiguity of NULL. Both are resolved by the compiler.

#include <stddef.h> #include <stdio.h> int main(void) { auto value = 2.5; auto pointer = (int *) nullptr; printf("%.1f %zu\n", value, sizeof value); printf("%d\n", pointer == NULL); return 0; }

gcc -Wall -Wextra autonull.c -o autonull
./autonull

2.5 8
1

The inferred type of value is double, which is why the size is eight. The pointer is null and compares equal to NULL, because both are the null pointer value; the difference is that nullptr cannot be mistaken for the integer zero.

Attributes

C23 standardises the double-bracket attributes that compilers used as extensions. [[nodiscard]] warns when a result is ignored, [[maybe_unused]] silences the unused warning, and [[deprecated]] warns at every use. The program below triggers the deprecation warning on purpose.

#include <stdio.h> [[nodiscard]] static int compute(void) { return 42; } [[deprecated("use compute")]] static int old_compute(void) { return 0; } int main(void) { int value [[maybe_unused]] = old_compute(); printf("%d\n", compute()); return 0; }

gcc -Wall -Wextra attrs.c -o attrs
./attrs

attrs.c: In function 'main':
attrs.c:15:5: warning: 'old_compute' is deprecated: use compute [-Wdeprecated-declarations]
15 | int value [[maybe_unused]] = old_compute();
| ^~~
attrs.c:8:42: note: declared here
8 | [[deprecated("use compute")]] static int old_compute(void)
| ^~~~~~~~~~~
42

The maybe_unused attribute keeps the compiler quiet about value, while the deprecated function still produces its warning: the two attributes answer different questions, and one does not suppress the other. The call to compute is used, so the nodiscard attribute stays silent.

Digit separators

A single quote inside a number is ignored by the compiler and exists for the reader. C23 also standardised the binary prefix 0b and made the separator legal in every base, so a mask or a large count can be written in groups of four.

#include <stdio.h> int main(void) { int million = 1'000'000; int bits = 0b1010'0101; int hex = 0xFF'FF; printf("%d %d %d\n", million, bits, hex); return 0; }

gcc -Wall -Wextra digits.c -o digits
./digits

1000000 165 65535

The three values are one million, the binary 10100101 which is 165, and sixteen ones in hex, which is 65535. The separators do not change the value; they only make the groups visible, which matters for masks and byte counts more than for round decimal numbers.

A constexpr without a value

constexpr requires an initialiser: the value must be known at compile time, and a declaration without one cannot satisfy that. The compiler reports the missing initialiser on the declaration and then treats the name as undeclared at every use.

#include <stdio.h> constexpr int LIMIT; int main(void) { printf("%d\n", LIMIT); return 0; }

gcc -Wall -Wextra badconst.c -o badconst

badconst.c:3:1: error: 'constexpr' requires an initialized data declaration
3 | constexpr int LIMIT;
| ^~~~~~~~~
badconst.c: In function 'main':
badconst.c:7:20: error: 'LIMIT' undeclared (first use in this function)
7 | printf("%d\n", LIMIT);
| ^~~~~

The second error is a consequence of the first: the failed declaration defines no name. The fix is the initialiser from the constexpr section. The same rule applies to an array size: without the value the compiler cannot size the array.

Exercises

  1. Use typeof in a macro that swaps two values of any type, with a temporary of the right type.
  2. Declare the array size of the first section with constexpr auto and check that it compiles.
  3. Write a function marked [[nodiscard]], call it once ignoring the result and once using it, and compare the warnings.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does typeof_unqual add to typeof?

    Answer

    It drops qualifiers such as const

    The unqualified form gives the same type without qualifiers, so a const source can produce a writable copy.

  2. Why can constexpr size an array while const int cannot?

    Answer

    constexpr is a compile-time constant

    A constexpr object is guaranteed to be a constant expression, while a const object is only read-only.

  3. What type does auto take from an initialiser?

    Answer

    The type of the initialising expression

    C23 auto infers the type from the initialiser, so auto value = 2.5; declares a double.

  4. What does [[nodiscard]] ask the compiler to report?

    Answer

    A call whose result is ignored

    The attribute marks the result as important, and a discarded call produces a warning.

  5. This program is built with gcc -std=gnu23 -Wall -Wextra typeof.c -o typeof. What does it print? #include <stdio.h> int main(void){int value=5;typeof(value) copy=value+1;typeof_unqual(const int) plain=2;printf("%d %d %zu\n",copy,plain,sizeof copy);return 0;}

    Answer

    6 2 4

    copy becomes 6, plain is 2, and the type of copy is a four-byte int.

  6. What size does the constexpr array print?

    Answer

    size: 40

    Ten int elements of four bytes each occupy forty bytes.

  7. This program is built with gcc -std=gnu23 -Wall -Wextra autonull.c -o autonull. What does it print? #include <stddef.h> #include <stdio.h> int main(void){auto value=2.5;auto pointer=(int*)nullptr;printf("%.1f %zu %d\n",value,sizeof value,pointer==NULL);return 0;}

    Answer

    2.5 8 1

    The inferred type is double, so its size is eight, and the null pointer compares equal to NULL.

  8. This program is built with gcc -std=gnu23 -Wall -Wextra digits.c -o digits. What does it print? #include <stdio.h> int main(void){int million=1'000'000;int bits=0b1010'0101;int hex=0xFF'FF;printf("%d %d %d\n",million,bits,hex);return 0;}

    Answer

    1000000 165 65535

    The binary literal is 165, the hexadecimal one is 65535, and the separators do not change the values.

  9. What does GCC report for constexpr int LIMIT; without an initialiser?

    Answer

    An error that the declaration needs an initialiser

    constexpr requires a compile-time value, so the declaration is rejected and later uses report the name as undeclared.

  10. Does [[maybe_unused]] silence a deprecated-function warning?

    Answer

    No, the deprecation warning still appears

    The attributes answer different questions, so maybe_unused covers the unused variable while the deprecated call still warns.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image