?

Log in

No account? Create an account

fanf

Experimenting with _Generic() for parametric constness in C11

« previous entry | next entry »
1st Jun 2016 | 19:40

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!

| Leave a comment | Share

Comments {8}

Gerald the cuddly duck

from: gerald_duck
date: 1st Jun 2016 19:15 (UTC)

C++ gets you some of the way there simply by allowing function overloading:
inline const char *strchr(const char *s, char c) { return strchr(const_cast<char *>(s),c); }
You can go further and template the function:
template <typename T, typename C>
inline T strchr(T t, C c) { for(;*t;++t) if (*t==c) return t; return T(); }
…though you're beginning to reinvent std::string::find() and std::find() at that point.

One thing I'd very much like to see in C++ to support more sophisticated cases, however, is the ability to make code generic over CV-qualifiers. Then you could write something like:
template <cv_qualifier CV, typename A, typename B>
inline CV A &first(CV std::pair<A,B> &p) { return p.first; }
…and have it Just Work.

Reply | Thread

Tony Finch

The GNU C version

from: fanf
date: 1st Jun 2016 20:51 (UTC)

Juli Mallett pointed out on Twitter that there is a neat way to do some kinds of type parametrization using the GNU typeof extension.

    #define strchr(s, c) ((__typeof__(&(s)[0]))strchr((s), (c)))


Edited at 2016-06-01 08:51 pm (UTC)

Reply | Thread

Simon Tatham

from: simont
date: 1st Jun 2016 22:02 (UTC)

I'm always in two minds about the feature of typeof where it preserves the cv-qualifiers of the thing you apply it to.

For this kind of job, and for other applications involving making a pointer that can address the object you were given, that's exactly what you want. But for another entirely sensible kind of job, it's exactly not what you want – namely, the case in which the whole reason you wanted typeof in the first place was in order to make a mutable copy of the potentially-const object you were given. In that situation, insisting on keeping the const leaves me back where I started! I'd prefer a means of saying which kind of situation is which.

Reply | Parent | Thread

Gerald the cuddly duck

from: gerald_duck
date: 1st Jun 2016 23:46 (UTC)

Turn to the dark side: embrace C++!

Seriously, the type traits library in C++ and Boost makes this kind of thing a doddle. std::remove_cv_t<T> is your friend.

Reply | Parent | Thread

Gerald the cuddly duck

from: gerald_duck
date: 1st Jun 2016 23:42 (UTC)

Am I overlooking some subtle reason to prefer __typeof__(&(s)[0]) over __typeof((s)) ?

Reply | Parent | Thread

Tony Finch

from: fanf
date: 1st Jun 2016 23:45 (UTC)

Ensures it is a pointer type not an array, maybe?

Reply | Parent | Thread

Gerald the cuddly duck

from: gerald_duck
date: 1st Jun 2016 23:56 (UTC)

Hmm. True. I'm clearly getting rusty on my C macro trickery after decades of concentrating on C++ template trickery instead.

Even so, how about __typeof(&*(s)) ? That's somewhat simpler, and equivalent until you're messing with C++ smart pointer types. By the time you're doing that you shouldn't be using such macros anyway!

Reply | Parent | Thread

juli

from: caladri
date: 2nd Jun 2016 08:39 (UTC)

That was the magical-think reasoning, yeah. I wasn't sure how the accepted behaviour of typeof might vary between compilers in that regard. I'd like compilers to aggressively propagate and check static array sizes more aggressively, not rely on them doing it less. So we really want what a pointer to one character in the string looks like, not what the string pointer itself looks like. I mean, that's how C semantics represent the operation we're doing anyway, so I felt that was a good idiom to use.

Reply | Parent | Thread