<- Back to blog

C23 - new features added to the good old C


C language development

C programming language is one of the most popular programming languages over the last few decades. It is used in a wide range of systems: from embedded applications running on low-cost MCUs to the modern HPC systems running heavy-computation software to train AI models. Such a remarkably broad scope of uses is explained by several factors:

Nevertheless, C language keeps slowly changing. Its standard has had four revisions in the past 30 years: C99, C11, C17 and C23. Despite its name, C23 standard was released in 2024. Now, in 2025, it is a good time to break down the changes made in C23.

C23 brought a lot of changes. Not all of them are implemented in modern compilers. If you are willing to try out new C23 features and to compile examples from this post, I would recommend to use GCC version 15 and newer, or LLVM Clang version 19 and newer.

Let's take a closer look at some of the major changes in C23.

bit-precise integer types

_BitInt(N) allows to define N-bit integer where N can be up to BITINT_MAXWIDTH defined in <limits.h>. For example, GCC 15 has BITINT_MAXWIDTH=65535 which allows operating with quite large values.

This feature might be useful when performing modular arithmetic calculations in cryptography, or implementing network protocol with field lengths of several bits.

An example of how _BitInt(N) can be used to implement N-bit counter:

unsigned _BitInt(2) counter;
for (int i = 0; i < 8; i++)
{
    /* prints 0 1 2 3 0 1 2 3 due to wrapping of 2-bit counter */
    printf("%u ", counter++);
}

See the code snippet for another example of _BitInt(N).

#embed

Replaces `xxd -i` command and simplifies build system when you need to insert external file content to source code. It can be used when there is need to insert media (icons, sound etc) to the executable, or when processing some data which can be compiled-in.

Simple example of using the #embed to load text data:

const char settings_json[] =
{
    #embed "compile_time_settings.json"
};

int main(void)
{
    load_settings(settings_json);
    // ...
}

See a code snippet demonstrating how to parse .wav file #embed-ded into source code.

enums with fixed underlying types

Let's assume we have a library communicating by serial protocol with the following message header type:
typedef struct __attribute__((packed))
{
    enum
    {
        _BROADCAST_MSG_TYPE,
        _UNICAST_MSG_TYPE,
    } msg_type;
    char msg_len;
} msg_header_pre_t;

The intention of the header definition is clear. It contains two fields: `msg_type` and `msg_len`. However, this type definition is ambiguous when it comes to the fields sizes. The second field has a length of 1 byte regardless of a compiler or platform because it uses char type. But the `msg_type` field length is not defined properly because enum type doesn`t guarantee a specific fixed size. The standard only requires a compiler to use a type that can fit all the possible enum values. In practice when compiling with GCC the msg_header_pre_t size can be 2 (if --short-enums option is used), or 5. Unfortunately this ambiguity makes usage of such a type for communication between different libraries complicated because they can be compiled with different flags, so the field sizes will be different and the communication will fail.

C23 solves this issue by allowing a developer to explicitly set an integer type used for the given enum field. This option was already available in many compilers as an extension, but adding this feature to the standard makes code portable and the language more reliable in general. Underlying type can be defined using the following syntax:

enum : char
{
    _BROADCAST_MSG_TYPE,
    _UNICAST_MSG_TYPE,
} msg_type;

See full example of enums with underlying types here.

constexpr

Allows to calculate values of scalar objects during compile-time. Unlike C++ constexpr, the C constexpr can't be used to evaluate function values, it can only be used for variables.

auto and typeof

Allows to improve code maintainability by assigning types for related objects implicitly. In the following code snippet foo(), bar(), i and m have int type:

int foo(void)
{
    return 10;
}

typeof(foo()) bar(void)
{
    auto i = foo();
    typeof(i) m = i / 2;
    return m;
}

A couple more things

Small code readability improvements:

See a code snippet demonstrating constexpr, auto, typeof and other small things.

Summary

C23 brought many changes to the language.

Some of them standardized the practices that were already implemented in modern compilers as extensions. As an example, underlying enum types, or binary integer constants were already available in GCC, but now they became part of the standard, which makes the code using these extensions portable and easier to maintain.

Other features are completely new. For example, the #embed preprocessor directive. It simplifies certain use-cases and makes life of developers easier.

Only time will tell, which of the changes will be widely adopted, which of the changes will not be understood and not used by developers. And while developer community slowly starts using the new C23 features, the WG14 working group is already working on the next revision of the standard - C2Y - which will bring even more changes.

Links