C23 Tricks: Improved Tag compatibility
After all this time, and even though I know that I should just prefer C++ (or
Rust) for all projects where C is a viable choice, C still has a huge attraction
for me. I can not really explain it, I just like how it can do so much with so
few concepts. C is a simple language, but that does not mean that coding in C is
simple by any means. There are very good reasons to avoid the language.
And still sometimes the C bug (heh ...) just bites me. And for these times, I
really like that C still gets new features without loosing its identity and
becoming C++ yet again. C23 brings some nice little things to the table and I am
very looking forward to the huge improvements that might come with C2y.
One of these C23 features is N3037 "Improved Rules for Tag Compatibility". This
feature is implemented with Clang 21 and GCC 14.1 (at least) and allows using
identical definitions of tagged types (e.g. structs) with identical tag name
without conflicts.
This makes it possible to redefine simple helper types whenever you need
them. Previously, you would have to define them once and ensure that this
definition is always included before you try to work with the helper type. It
also makes it more feasible to create such types on-the-fly using macros (which
are absolutely fine in C, even though you rightfully should avoid them in modern
C++).
Result_t
The following idea is not my own; I read about it somewhere in the past, but I
do not know where. I think that it is a very neat pattern, so I want to conserve
it here and spread it that way. I believe that as C23 becomes more widely used
(as far as a modern C standard ever becomes widely used at all nowadays), this
pattern will find many interesting use cases, and I am quiet curious how it
might develop if more cool features land with C2y.
The example code defines a helper type named Result_t, which can be seen as the
moral equivalent of Result from Rust, Result.t from Ocaml or std::expected from
C++. Like in Rust, you can use Ok(foo) or Err(foo) to create a successful or a
failed result value. The helper macros allow creating Result_t types for many
different types, as if C would support simple templates.
You can also see a minor flaw in this kind of handling: Since the struct tag
needs to be a single valid identifier, I had to define a typedef ErrorMessage_t
for the error message. Otherwise, "const char*" as a string would contain both a
space and an asterisk, which both cannot be used in identifiers.
Example code
#include <stdio.h> #define Result_t(T, E) \ struct Result_##T##_##E { \ bool is_ok; \ union { \ T value; \ E error; \ }; \ } #define Ok(T, E) \ (struct Result_##T##_##E) { \ .is_ok = true, .value = (T)_OK_IMPL #define _OK_IMPL(...) \ __VA_ARGS__ \ } #define Err(T, E) \ (struct Result_##T##_##E) { \ .is_ok = false, .error = (E)_ERR_IMPL #define _ERR_IMPL(...) \ __VA_ARGS__ \ } typedef const char* ErrorMessage_t; Result_t(int, ErrorMessage_t) my_func(int i) { if (i == 42) return Ok(int, ErrorMessage_t)(255); else return Err(int, ErrorMessage_t)("an error occurred"); } void check_result(Result_t(int, ErrorMessage_t) * result) { if (result->is_ok) { printf("Ok: %d\n", result->value); } else { printf("Error: %s\n", result->error); } } int main() { printf("my_func(42): "); Result_t(int, ErrorMessage_t) x = my_func(42); check_result(&x); printf("my_func(17): "); Result_t(int, ErrorMessage_t) y = my_func(17); check_result(&y); }
You can also play with it on godbolt, if you want to:
Makefile
My makefile is very simple and I include it here just for completeness' sake.
PRGNAME=n3037 OBJECTS=n3037.o CFLAGS=-std=c23 $(PRGNAME): $(OBJECTS) $(CC) -o $(PRGNAME) $(OBJECTS) clean: -rm $(PRGNAME) -rm $(OBJECTS)
Summary
As mentioned before, I believe that this feature has a lot of potential. I am
interested in seeing what (if anything at all) the C community will do with
it.
As before with C11, not everyone is happy with such changes to the C
language. It fractures the C community, since a library that uses C23 features
can only be used with compilers that support these features. A library that uses
such types as Result_t from above might even force it on its users. I can
understand this positions.
I believe that careful extensions to the C language can help us modernize C
projects and making them safer and simpler without having to rewrite them, which
would be problematic in its own ways. I especially find that types like Result_t
help here, since they make it more obvious whether an error occurred or
not. Therefore, I am in favor of such extensions, and I believe that new
features like defer in C2y will also improve C more that it will suffer from it.