Index RSS

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.

n3037.pdf

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:

Code on godbolt.org

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.