[RFC] Generic selection expression with a type operand

TL;DR

I would like to extend _Generic selection expressions to accept a type argument to enable more straight-forward type generic programming in C. This feature will be proposed to WG14 but has not been viewed by the committee yet.

The review patch for this functionality can be found at: ⚙ D149904 Generic selection expressions that accept a type operand

Problem

Currently, generic selection expressions require the first operand to be an expression. This expression is not evaluated, but the type of the expression is compared to the types supplied by the association operands to determine which association matches (if any). However, the type used is the type after lvalue conversion which means it is not possible to match qualified types directly. Because lvalue conversion only drops top-level qualifiers, you might instead try to take the address of the expression and use pointer types as the associations. In other words, your code would start out looking something like this:

#define EXPR_HAS_TYPE(Expr, Type) _Generic(&(Expr), Type * : 1, default : 0)

const int i = 12;
_Static_assert(EXPR_HAS_TYPE(i, const int));

However, this won’t work if the expression isn’t an lvalue, so EXPR_HAS_TYPE(12, int) does not expand to valid code. It turns out to be surprisingly difficult to write a macro that will work in a type-generic way to provide “type traits” in C, and so users are left with partial or overly complex solutions.

Clang (and GCC, etc) have a builtin that comes close to what we need, __builtin_types_compatible_p, however this also strips qualification from the given types. So it’s close, but has the same struggles as _Generic. Further, the builtin __is_same is only exposed in C++, and so it also doesn’t solve the issue.

Proposed Solution

C has a few operators that take either a type or an expression, such as sizeof. It is natural to extend that idea to _Generic so that it can also accept a type for the first operand. This type does not undergo any conversions, which allows it to match qualified types, incomplete types, and function types. C2x has the typeof operator as a way to get the type of an expression before lvalue conversion takes place, and so it keeps the qualification. This makes typeof a straightforward approach to determining a type operand for _Generic that considers qualifiers. Now our macro becomes:

#define EXPR_HAS_TYPE(Expr, Type) _Generic(typeof(Expr), Type : 1, default : 0)

which can be called with an expression of any value category (no need to be an lvalue) and will test against (almost) any type. This is a conforming extension to C (it’s defining the behavior of invalid syntax), and I believe this is a natural way to extend this functionality. Many thanks to Thiago Adams for suggesting this approach!

Other Differences Worth Noting

_Generic with a type operand will relax the requirements of what can be a valid association. Specifically, it allows incomplete types and non-object types (but still prevents use of variably-modified types). This relaxation only happens for the type operand form; the expression operand form continues to behave as it always has.

This extension allows incomplete and non-object types because the goal is to better enable type-generic programming in C, and so we want to allow any typed construct we can statically determine the type of. There is no reason to prevent matching against void or function types, but this does explain why we continue to prohibit variably-modified types.

Further, allowing incomplete types enables “tag dispatch” functionality without requiring a complete type, which can be quite useful for generic programming. e.g.,

#define TAG_TO_INDEX(tag) _Generic(tag, \
  struct red_channel : 0, \
  struct green_channel : 1, \
  struct blue_channel : 2)

#define GET_TAGGED_VALUE(array, tag) array[TAG_TO_INDEX(tag)]

...
int colors[3];
int blue = GET_TAGGED_VALUE(colors, struct blue_channel);

How to Surface the Functionality

I’ve discussed this feature with some other C committee members and C users, and the feedback has been almost entirely positive. So far, the only criticism received is about reusing the _Generic keyword with semantics that differ based on use of a type- vs expression operand. It was correctly noted that existing C features taking a type or an expression have the same semantics regardless of whether the type or expression form is used, but this extension would give different semantics. However, it’s too early to tell whether the larger committee agrees or disagrees with that position.

I believe that introducing a new keyword is unnecessary as the semantics of the two forms are sufficiently distinguishable and a new keyword would be heavy-handed. Values can have conversion operations applied to them which modify the type, but a type by itself has no such chance for an implicit conversion, so it seems defensible that the semantics of a type inspection feature be tied to the operand form. That said, the functionality of this feature is not materially changed by use of a new keyword, so if the community agrees with this criticism, we can introduce a new keyword.

Plan

Once we’ve had some time to discuss and potentially modify the RFC, I will be proposing the resulting feature to WG14. Unfortunately, the committee is unlikely to consider the proposal any time soon because we are trying to wrap up C2x this year. So I will ask the committee for early feedback on the feature via the committee reflectors. This should hopefully give us slightly more confidence that the committee will not elect to “improve upon” the extension in ways that would break our users moving forward. However, if the committee shows an interest in the feature, we can claim this extension is unstable and subject to change to give us some latitude.

1 Like

Is the proposed _Generic(typeof(Expr), Type : 1, default : 0) equivalent to _Generic((typeof(Expr)*)nullptr, Type* : 1, default : 0)?

I guess this is a “conforming extension” in C because C doesn’t have type/expression ambiguity like C++? If you allow C++ syntax, we have to worry about changing the interpretation of expression like int().

Almost, but not quite. Consider:

#define FOO(Expr, Type) _Generic((typeof(Expr)*)nullptr, Type* : 1, default : 0)
#define BAR(Expr, Type) _Generic(typeof(Expr), Type : 1, default : 0)

void func(int);

int main() {
  _Static_assert(FOO(func, void(int))); // error: expected ':'
  _Static_assert(BAR(func, void(int))); // OK
}

It’s a conforming extension because C doesn’t allow this syntax so it cannot break well-formed code.

We already allow _Generic in C++ as an extension and I was planning to follow suit with this extension as well, and it would follow the usual disambiguation rules that already exist for other purposes, so int() is always a function type when it could be a type or an expression ([dcl.ambig.res]):

F:\source\llvm-project>cat "C:\Users\aballman\OneDrive - Intel Corporation\Desktop\test.c"
static_assert(_Generic(int(), int : 0, int() : 1) == 1);

F:\source\llvm-project>llvm\out\build\x64-Debug\bin\clang.exe -cc1 -std=c2x -fsyntax-only "C:\Users\aballman\OneDrive - Intel Corporation\Desktop\test.c"

F:\source\llvm-project>llvm\out\build\x64-Debug\bin\clang.exe -cc1 -fsyntax-only -x c++ "C:\Users\aballman\OneDrive - Intel Corporation\Desktop\test.c"

We’ve discussed this privately, and now I’ve read through the review: I think I’m in favor of this, once we get a decent positive reception (hopefully on that reflector list?) from WG14.

1 Like

You could also wrap Type with typeof:

#define EXPR_HAS_TYPE(Expr, Type) _Generic((typeof(Expr))nullptr, typeof(Type) : 1, default : 0)

But I also like this feature.

2 Likes

Thank you to everyone who has chimed in!

It sounds like there’s support for this feature and no opposition, so unless I hear concerns about the design (I’m especially curious if anyone thinks this should be exposed via a new keyword rather than through an extension to _Generic), I’ll plan to start the discussing on the WG14 reflectors sometime next week. If I hear anything back that changes the design, I’ll post that feedback here.

The public and private feedback from the WG14 reflectors has thus far been very positive, with the majority of people supporting the feature as-is. There were two concerns raised:

  1. There was a question of whether there are other extensions in this same syntactic space that have different semantics than what I’ve proposed. I could not find any such extensions nor was anyone on the committee aware of any. So this concern seems to be addressed.

  2. There is a sustained concern from one committee member that this feature should use a new keyword (they proposed _GenericType). The reasoning is: other operators that take an expression or a type name have the same semantics regardless of whether an expression or a type operand is passed, and my proposed design has semantic differences in what associations can be matched. They are worried that because _Generic is often hidden behind a macro, it will be too easy for users to not know which semantics they will get. If users are expected to use either the expression form or the type form, but not mix them into a single use case, then it stands to reason that these use different keywords.

(Before posting my own thoughts) I’d like to hear explicitly whether anyone within the Clang community would like to argue in favor of point 2.

Should we use a separate keyword for this extension rather than extending _Generic?

  • Yes
  • No
  • Don’t care

0 voters

1 Like

I don’t have a rich C experience, but having two language facilities that are named similarly and do similar things doesn’t feel good to me. I also feel like C and C++ has history long enough to find something relevant there, and see how it went.

2 Likes

Thank you everyone for the discussion and feedback! I’ve landed the changes in: [C] Support _Generic expressions with a type operand · llvm/llvm-project@12728e1 · GitHub