.@ Tony Finch – blog


One of the new features in C99 was tgmath.h, the type-generic mathematics library. (See section 7.22 of n1256.) This added ad-hoc polymorphism for a subset of the library, but C99 offered no way for programmers to write their own type-generic macros in standard C.

In C11 the language acquired the _Generic() generic selection operator. (See section 6.5.1.1 of n1570.) This is effectively a type-directed switch expression. It can be used to implement APIs like tgmath.h using standard C.

(The weird spelling with an underscore and capital letter is because that part of the namespace is reserved for future language extensions.)

When const is ugly

It can often be tricky to write const-correct code in C, and retrofitting constness to an existing API is much worse.

There are some fearsome examples in the standard library. For instance, strchr() is declared:

    char *strchr(const char *s, int c);

(See section 7.24.5.2 of n1570.)

That is, it takes a const string as an argument, indicating that it doesn't modify the string, but it returns a non-const pointer into the same string. This hidden de-constifying cast allows strchr() to be used with non-const strings as smoothly as with const strings, since in either case the implicit type conversions are allowed. But it is an ugly loop-hole in the type system.

Parametric constness

It would be much better if we could write something like,

    const<A> char *strchr(const<A> char *s, int c);

where const<A> indicates variable constness. Because the same variable appears in the argument and return types, those strings are either both const or both mutable.

When checking the function definition, the compiler would have to treat parametric const<A> as equivalent to a normal const qualifier. When checking a call, the compiler allows the argument and return types to be const or non-const, provided they match where the parametric consts indicate they should.

But we can't do that in standard C.

Or can we?

When I mentioned this idea on Twitter a few days ago, Joe Groff said "_Generic to the rescue", so I had to see if I could make it work.

Example: strchr()

Before wrapping a standard function with a macro, we have to remove any existing wrapper. (Standard library functions can be wrapped by default!)

    #ifdef strchr
    #undef strchr
    #endif

Then we can create a replacement macro which implements parametric constness using _Generic().

    #define strchr(s,c) _Generic((s),                    \
        const char * : (const char *)(strchr)((s), (c)), \
        char *       :               (strchr)((s), (c)))

The first line says, look at the type of the argument s.

The second line says, if it is a const char *, call the function strchr and use a cast to restore the missing constness.

The third line says, if it is a plain char *, call the function strchr leaving its return type unchanged from char *.

The (strchr)() form of call is to avoid warnings about attempts to invoke a macro recursively.

    void example(void) {
        const char *msg = "hello, world\n";
        char buf[20];
        strcpy(buf, msg);

        strchr(buf, ' ')[0] = '\0';
        strchr(msg, ' ')[0] = '\0';
        strchr(10,20);
    }

In this example, the first call to strchr is always OK.

The second call typically fails at runtime with the standard strchr, but with parametric constness you get a compile time error saying that you can't modify a const string.

Without parametric constness the third call gives you a type conversion warning, but it still compiles! With parametric constness you get an error that there is no matching type in the _Generic() macro.

Conclusion

That is actually pretty straightforward, which is nice.

As well as parametric constness for functions, in the past I have also wondered about parametric constness for types, especially structures. It would be nice to be able to use the same code for read-only static data as well as mutable dynamic data, and have the compiler enforce the distinction. But _Generic() isn't powerful enough, and in any case I am not sure how such a feature should work!