Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
What is C23 and why should I care?
The C language has gone through many iterations and the latest one not yet released is informally called "C23", supposedly because they hoped to release it in 2023. The latest draft version N3096 is available for free from the ISO C working group.
The previous standard C17 (ISO 9899:2018) didn't really affect the average C programmer. It was a minor revision and mostly concerned with language error fixes in non-trivial areas of the language, which are likely of peripheral interest to the average user.
The version before that, C11, was a bigger revision but probably went by mostly unnoticed by the average C programmer as well. Some might have picked up _Generic
, anonymous structs, static asserts and similar features.
What is new in C23 and how will it affect the average programmer?
1 answer
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
Lover of Structure | (no comment) | Aug 17, 2023 at 20:52 |
C23 will be a major revision, certainly the biggest one since C99. It contains lots of changes and new features, both big and small. The linked draft in the question contains a complete list of changes, but it might be a bit overwhelming to read unless you are a "C nerd" and if you aren't used at reading technical standards. There's a lot of error fixes, for example volatile
access getting fixed and re-defined (it has actually been quite broken since the first C version). But this answer will focus on new features that are might have an impact on the way the average C programmer writes code.
-
bool
,false
andtrue
are now proper keywords in the language. No need to includestdbool.h
(which will get phased out)._Bool
will remain a keyword. The way C treats booleans does not change though - the result of logical/relational expressions (a == b
,a && b
,a < b
and the like) will still be of typeint
and of value1
or0
. But otherwise C will now mostly behave like C++ has always done. -
Similarly
static_assert
,alignas
,alignof
and other keywords that exist as keywords in C++ but only existed through header inclusion in C are now proper keywords.static_assert
also gets a little upgrade - the string provided as second argument to it is now optional. So it can be used just like old-schoolassert
but is evaluated at compile-time instead of run-time. -
nullptr
is a new keyword also included mainly for C++ compatibility. It is a constant guaranteed to be a null pointer constant much likeNULL
has always been, but without all the subtle stuff like0
being a either a null pointer constant or an integer constant (see What's the difference between null pointers and NULL?). A new typenullptr_t
was added and anullptr
constant is of this type. Compilers are already starting to implement error/warnings for cases where you pass anullptr_t
to a function - instead of silently accepting it.NULL
is still available like before. -
typeof
keyword added to the language. Like many others of these features, this has been available as a non-standard extension for quite a while. Along with existing_Generic
, it is handy for type safety and type-generic programming, designing with inheritance, maintaining troublesome function API in existing code in a safe manner and so on.The usage of
typeof
is quite easy - given someint x;
you can declare another variable or expression astypeof(x) y;
and that's the same asint y;
. These are resolved at compile-time. There is also a correspondingtypeof_unqual
keyword which works the same but also discards qualifiers (const
,volatile
etc). -
New keyword
constexpr
(which does not quite work as C++) allows you to write initializer lists, compound literals and the like which will then definitely get treated as constant expressions. The exact impact of this is still fuzzy to me, since the mainstream compilers gcc and clang are the only ones so far with significant C23 support and both of these prior C23 had questionable standard compliance when it comes to constant expressions. So I haven't yet been able to test it out with any confidence myself. -
Binary integer constants (like
0b01010101
) were added to the language. This has been a common compiler extension forever, but it's now part of the standard and portable.printf
/scanf
family of functions now support%b
/%B
. -
Digit separator
'
added to the language. The main rationale for not including binary constants in standard C was always because something like a 32 bit constant0b10101010111010100010101110100011
is an unreadable mess and unhelpful for any purpose.So the pre-condition for allowing binary numbers was to also add a digit separator to the language - we can now write
0b10101010'11101010'00101011'10100011
or0b1010'1010'1110'1010'0010'1011'1010'0011
. Digit separators are purely aestetic and are otherwise ignored by the compiler, so we can place them anywhere. But they are also helpful for making large decimal or hex numbers more readable:1'000'000
or0xAA'BB'CC'DD
. -
Exotic signed number formats are removed. For various confused and historical reasons, C has until now supported exotic signed number formats such as one's complement and signed magnitude. These are no longer supported - the only signed format now supported is two's complement. Which is what 99.99% of all computers happen to use.
-
Function declarations without prototypes ("K&R style" functions) removed. The feature of declaring a function as
void func()
("no prototype") and then later defining it asvoid func (x) int x;{}
has been flagged by ISO as obsolescent since forever - it is now finally removed from the language. This will probably only affect those maintaining really old code bases, as nobody should have been using these in new code during the past 30 years.Until C23,
void f()
has been regarded as "a function accepting any parameter". In C23, this will finally become equivalent to "function taking no parameter -void
".Because of this, the clang compiler enforced some nasty changes. In newer version of the compiler, when compiling against an older C standard, clang will whine if you write something like
int main()
and say "error: a function declaration without a prototype is deprecated in all versions of C". This is true, even Dennis Ritchie who invented C back in the days recognized that this was a design flaw to be fixed in the future. But existing code might have a hard time if you compile in new versions clang, as anything but C23. -
Function attributes added to the language. This is a pretty big one, with quite diverse applications. Various compilers have non-standard syntax for "function attributes" and the like (gcc/GNU C in particular) and some of these are now standardized, as well as the syntax. Attributes aren't limited to functions but can be used for variables, types and a lot of other language items. The main purpose of the standardized attributes introduced with C23 is to enable better compiler diagnostics and self-documenting code. I won't go into all of these in detail but some of them are quite handy - see below.
-
Function attribute
[[nodiscard]]
. Prior to C23 there's a big quality of implementation issue with compilers for situations like calling the function
important_t returns_important_info (void);
. "PC-like" compilers like gcc and clang traditionally have a lax approach and won't complain if you call this function likereturns_important_info();
and silently ignore the returned value. Whereas more higher integrity compilers like embedded systems ones will complain about that. While on the other hand, the same compilers they will also whine if you ignore the return value of printf etc.Declaring the function as
[[nodiscard]] important_t returns_important_info (void);
will ensure that the compiler always warns when someone tries to call the function asreturns_important_info();
without handling the return value. -
Similarly, the
[[maybe_unused]]
function attribute can be used for disabling diagnostics when some parameters or variables aren't used. A common trick is to cast such variables to(void)
which is quite portable but clutters up the code.Also this attribute isn't restricted to variables but can be used for functions, enums etc.
volatile int* my_register; ... int dummy = *my_register;
is valid code that can be used for a read access that is discarded. Compilers might whine aboutdummy
not getting used, which we can now block with[[maybe_unused]] int dummy = *my_register;
. -
The
[[deprecated]]
function attribute can be added to give the user a warning when they for example use some old function in your library that has been replaced with a better alternative but left around for backwards compatibility. This will probably be great for maintaining old code bases. -
The
[[fallthrough]]
attribute can be used to explicitly document a certain shady practice of writing aswitch
-case
where onecase
"falls through" to the next one without abreak
.For example if I write something fishy like
switch(1){ case 1: puts("1"); case 2: puts("2"); }
then any half-decent compiler should warn me "huh didn't you mean to have a break there?". In case we want something to fall through intentionally, we'd usually add a comment about it and then maybe go disable a specific compiler warning. Now we can writeswitch(1){ case 1: puts("1"); [[fallthrough]]; case 2: puts("2"); }
and get self-documenting code as well as the warning surpressed all at once. -
Write labels everywhere. Arguably nobody should be writing labels (
example:
) to begin with, since the main use of labels isgoto
spaghetti programming. However,case
statements are also considered labels by C and this rule means that we can now finally declare variables insidecase
statements, without adding a{}
compound statement.That sounds nice at first, though be aware that a variable declared inside a
case
without{}
has the same scope as the wholeswitch
. Which means that they are accessible throughout the entireswitch
and that aswitch
with variable declarations inside now comes with the same undefined behavior problems likegoto
spaghetti "jumping across a variable declaration", for exampleswitch(2){ case 1: int x=5; printf("%d",x); case 2:printf("%d",x); /* UNDEFINED BEHAVIOR! */ }
So the previous best practice of always using{}
for eachcase
is still the best practice. -
New functions
strdup
/strndup
were added tostring.h
. These are widely used non-standard extensions until now. These functions heap allocate room for a copy of a string and then copies it. Until now the only portable way to do so was to type out all ofchar* dst = malloc(strlen(src)+1); strcpy(dst, src);
. -
A new function
memccpy
was added tostring.h
. This function copies any data until a certain byte has been found and is likely to be more efficient than the hand-made equivalent. Likememcpy
it does not require data to be aligned, but does not support copying between overlapping data. It returns a null pointer if the character was not found within the specifiedn
interval but copiesn
bytes of data still.This function might be handy in various scenarios like parsing input until a new line character has been found. And it is also the final nail in the coffin for the dangerous
strncpy
function (Is strcpy dangerous and what should be used instead?), since we can now use the safer, faster and less ambigousmemccpy(dst, src, '\0', n)
in every situation where one might have previously been tempted to usestrncpy(dst, src, n)
. -
realloc(ptr,0)
is now explicitly undefined behavior. This was always poorly-defined and discouraged, but compilers were allowed to handle this in compiler-specific ways until now. -
New pre-processor directives
#elifdef
/#elifndef
. Previously we had to write#elif defined(x)
. -
New pre-processor directive
#warning
. This one has been available as a non-standard extension for quite a while, whereas#error
has been standardized. Now both are available. -
New pre-processor directive
#embed
. This is used for inclusion of the contents of "binary blob" files directly into the initialization lists of your source without having to involve make files and/or linker scripts/tricks. -
The end (hopefully) to the debate about if C should support variable-length arrays (VLA) as an optional or mandatory feature. Now declaring a VLA object is an optional feature, but declaring pointers to VLA ("variable types") is a mandatory feature. This makes sense since VLA objects aren't often very helpful, as they tend to be allocated on the stack. But pointers to VLA is a very useful tool in many situations such as when declaring function interfaces or working with multi-dimensional arrays.
-
Bug fixes for a lot of library functions (search functions in particular): we can now pass a const-qualified pointer parameter to a library function and assign the result of the function to a similarly const-qualified pointer. These new features are named
QChar
andQVoid
and are only used forchar*
andvoid*
parameters to certain library functions.A lot of library functions in C have been broken by design since the dawn of time - for example it was not possible to write a string parser using
strstr
like:current = strstr(current, key)
in casecurrent
was properly const correct asconst char*
... becausestrstr
returns achar*
. A lot of C library functions had this "broken by design" API which has now been corrected.So now these functions look like
QChar *strstr(QChar *s1, const char *s2);
whereQChar
orQVoid
is a fix which means that the function will return the same type as was passed tos1
. -
Empty initialization lists
int arr[3] = {};
is now part of the standard and equivalent to initializing everything to zero. This was a non-standard extension/C++-only syntax until now. -
Trigraphs are removed from the language??! If you know what trigraphs are, then you know that they need to go. If you don't know what trigraph are, then you are lucky! :) This should have no impact on the average program unless you are maintaining some old, ugly code base in a non-English country. (Digraphs are still there, for all your obfuscated C code needs.)
-
A new library
stdbit.h
(as of this moment I have not yet seen a compiler implement it alas). This library contains a way to check CPU endianess, and functions to perform various bit handling tricks like counting leading/trailing zeroes/ones in raw binary. -
A new library
stdckdint.h
for checked integer arithmetic. Meaning functions that perform integer overflow/underflow checks. The functions supported beingckd_add()
,ckd_sub()
andckd_mul()
that will be type-generic for all integer types. They will perform the operation and return true if successful, false in case of overflow/underflow. -
__STDC_VERSION__
for C23 is confirmed to be202311L
. gcc/clang are so far using a placeholder value202200L
when you compile with-std=c2x
.If I have missed something, do let me know. I would imagine that this post will be a work in progress and C23 is not yet formally released. The malloc/realloc parts in N3096 are for example filled with errors and inconsistencies even in the latest draft. Also the ISO wheels of bureaucracy grind slowly - rumors say that they might not be able to release it until 2024.
0 comment threads