1. Changelog
1.1. Revision 0 - December 16th, 2025
-
Initial release. ✨
2. Introduction and Motivation
selection will produce an error when trying to actually use the input value in a manner that is not plausible with all branches. This typically comes up when using macros that allow repeated use of the created generic selection and its expression. For example, this code:
int use_int_ptr ( int * pi ); int use_dbl_ptr ( double * pd ); #define USE_THING(THING) _Generic(&(typeof(THING)){ THING }, \ int*: use_int_ptr(&v), \ double*: use_dbl_ptr(&v): \ default: 0 \ ) int main () { int c = 0 ; int v = USE_THING ( c ); return v ; }
will spit out this error from latest trunk GCC (December 16th, 2025):
source>: Infunction 'main' : <source>:6:30: error: passing argument1 of'use_dbl_ptr' from incompatible pointer type[ -Wincompatible-pointer-types] 6 | double*: use_dbl_ptr( & v) :\ | ^~| | | int * <source>:12:17: note:in expansion of macro'USE_THING' 12 | intv = USE_THING( c) ; | ^~~~~~~~~ <source>:2:25: note: expected'double *' but argument is of type'int *' 2 | int use_dbl_ptr( double* pd) ; | ~~~~~~~~^~ <source>:6:33: error: expected')' before':' token6 | double*: use_dbl_ptr( & v) :\ | ^ <source>:12:17: note:in expansion of macro'USE_THING' 12 | intv = USE_THING( c) ; | ^~~~~~~~~ <source>:4:34: note: to match this'(' 4 | #define USE_THING(THING) _Generic(&(typeof(THING)){ THING }, \ | ^ <source>:12:17: note:in expansion of macro'USE_THING' 12 | intv = USE_THING( c) ; | ^~~~~~~~~
(Godbolt.)
In the case above, one can just cast a pointer to and then cast to the proper type to get rid of the errors, but this does not generalize when performing other operations on the input expressions. Additionally, can have nested macro problems, provoking multiple token expansions and replacements that can significantly increase compile-times.
A generic solution is putting a secondary that effectively "launders" the input into the correct type after the expression, a technique Martin Uecker calls (short for contravariant transform). This allows the use of two completely disparate types (e.g., an integer versus a structure) without provoking an error:
#include <stdio.h>#define contrav(T, x) _Generic(typeof(x), T: (x), default: (T){ }) #define info(x) \ _Generic(x, \ int: printf("%d", contrav(int, x)), \ struct foo: printf("%s", contrav(struct foo, x).name) \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; info ( 3 ); info ( f ); // prints: // 3 // test }
(Godbolt.) Unfortunately, this deepens the issue of multiple recursive expansions of a macro and still leaves open the potential of having an expression used multiple difference times. It also requires the user understand the technique that performs, which most programmers absolutely do not know how to do given that an extremely frequent request for is the ability to "ignore untaken branches". Unfortunately, one of the chief benefits of all of a generic selection’s branches is that they are type-checked, which is different from the way C++ templates and similar generic constructs behave.
A viable solution for expression-based is to allow the creation of an l-value or an r-value of the right type based on the input of a selection that uses an controlling expression rather than a controlling type. This allows all branches to be type-checked, but checked with a type that is appropriate rather than allowing a single use of a generic macro or similar to completely and utterly destroy any hope at proper compilation.
This paper proposes adding declarator as one of the new productions of generic-association, which will add an as the name of the inserted controlling expression as an r-value or an l-value. It also modifies the rules of a controlling expression to match against both the exact type first (to allow for an l-value expression that may be qualified to be selected) and then the type after typical l-value conversion strips the qualifiers and does other removals.
3. Design
The design is simple. There will be three productions for a generic-association grammar term rather than just two, alongside a change for the branch, to allow for an identifier of the proper type, like so:
generic-association:
type-name
assignment-expression: declarator
assignment-expression:
identifieroptdefault assignment-expression:
The use of declarator is primarily to allow a non-optional identifier to appear in the grammar in the usual expected place, with type-name being the same just without the identifier in its grammar. That makes the previously shown code samples look as follows:
int use_int_ptr ( int * pi ); int use_dbl_ptr ( double * pd ); #define USE_THING(THING) _Generic(THING, \ int pi: use_int_ptr(pi), \ double pd: use_dbl_ptr(pd): \ default: 0 \ ) int main () { int c = 0 ; int v = USE_THING ( c ); return v ; }
#define info(x) _Generic(x, \ int d: printf("%d\n", d), \ struct foo s: printf("%s\n", s.name) \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; info ( 3 ); info ( f ); }
This allows us to have access to the result of the generic selection’s controlling expression. Additionally, the scope of the introduced is from the expression-introducing colon to the terminating comma . This keeps things finely scoped. If ISO C had statement expressions, we could state that it is as-if there is a statement expression that introduces this variable and surrounds the of the generic selection.
3.1. Existing Practice: Allowing Modification / L-values
An important part of upholding existing practice is to allow for modification through an l-value. Using example code from Martin Uecker that shows modification working, the following code are things that plausibly exist in the real world already:
#include <stdio.h>#define contrav(T, x) _Generic(typeof(x), T: (x), default: (T){ }) #define bar(x) _Generic(x, \ int: contrav(int, x), \ struct foo: contrav(struct foo, x).name \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; bar ( 3 ); // 3 bar ( f ) = "something" ; // f.name puts ( f . name ); // prints "something" }
(Godbolt). Therefore, it is important that this code, when transferred to the next syntax, also works. The proposed look and semantics would be as so:
#include <stdio.h>#define bar(x) _Generic(x, \ int v: v, \ struct foo v: v.name \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; bar ( 3 ); bar ( f ) = "something" ; puts ( f . name ); // prints "something" }
Effectively, if the input value into a generic selection expression would be an lvalue, then it remains as such after a generic association branch is chosen. It applies similarly if it is an r-value. Because generic selection is an expression, the use of any created temporary values will always last for the duration of the "full expression", and so this should not cause any lifetime issues at all in the confines of both existing code and newly written code.
3.2. Evaluation of the Expression
Currently, the controlling operand of a generic selection expression is not evaluated if it’s an expression. In the case of a generic association that uses the declarator form rather than the type-name form, the expression is only evaluated after selection chooses a branch that has the declarator form. This will prevent the addition of new generic associations within a list of existing generic associations forcing unexpected evaluations that were not previously considered. It is also only evaluated once, even if the identifier provided is used multiple times. Therefore, under the new feature, the following code returns from :
int result = 0 ; int change_global ( void ) { result += 3 ; return 2 ; } int main ( void ) { int one = _Generic ( change_global (), // not evaluated int : 1 , double : 0xBAD , default : 0xBAD ); int two = _Generic ( change_global (), // evaluated int a : ( a * a ) / 2 , // only evaluated once double : 0xBAD , default : 0xBAD ); int three = _Generic ( change_global (), // not evaluated double : 0xBAD , default : 3 ); int four = _Generic ( change_global (), // not evaluated double d : ( int ) d + 0xBAD , default : 4 ); int five = _Generic ( change_global (), // evaluated int a [[ maybe_unused ]] : 5 , // evaluated even if `a` is unused double : 0xBAD , default : 0xBAD ); if ( one != 1 ) { return 1 ; } if ( two != 2 ) { return 2 ; } if ( three != 3 ) { return 3 ; } if ( four != 4 ) { return 4 ; } if ( five != 5 ) { return 5 ; } if ( result != 9 ) { return 9 ; } return 0 ; }
3.3. Direct Type Matching for Expressions
One of the problems with expressions is that it undergoes "l-value conversion" (qualifiers stripped, arrays converted to pointers, and similar) before the generic branch is matched. The problem was partially fixed by directly matching on an existing type, introduced by Aaron Ballman earlier into C2y. That allows directly matching on types that are e.g. -qualified, without having to make a pointer out of it.
The problem comes back again when we want to have that exact matching but still want to produce a named identifier for whatever expression has gone in The use of type-name as the generic-controlling-operand means that there is no expression to evaluate and therefore nothing to set the expression to. Therefore, with an expression, one would need to morph the input expression to have a different type than what is put in, just to be able to match qualifiers or arrays. This, of course, presents the immediately problem that this is creating a new expression of a different type than what is intended, resulting in unneeded temporaries or other issues.
3.4. Identifier Scope and Lifetime
The scope of the identifier introduced by this is from the and to the terminating . This is different from every other kind of identifier, so it needs a special carve out in §6.2.1 Scopes. We’re calling it generic association scope, for now. Just like labels, they’re a one-use-case kind of variable. This is just going to be called "generic lifetime" for the time being, because that’s the simplest possible way to call it. If we had statement expressions, we could simply say the scope is as-if the identifier was declared at the top of a statement expression that surrounds the assignment-expression that comes after the and before the , like so:
int r = 0 ; _Generic ( r , int v : v * v + 35 / ( v += 2 ) )
becomes...
int r = 0 ; _Generic ( r , int : ({ int * __lvalue_v = & r ; * __lvalue_v * * __lvalue_v + 35 / ( * __lvalue_v += 2 ); }) )
Unfortunately, we do not have statement expressions so we have to spend wording specifying something for this. Hopefully, once we get statement expressions, we can do away with the wording and just say it’s as-if a statement expression with normal block semantics exists in that space.
The lifetime of the backing storage for the identifier is regular automatic storage duration.
As an example, the identifier can be used for all of the generic association branches because the identifier’s scope does not bleed or leak out of the assignment-expression portion of that branch:
#include <stdio.h>#define bar(x) _Generic(x, \ int v: v, /* first use */ \ struct foo v: v.name /* perfectly fine reuse */ \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; bar ( 3 ); bar ( f ) = "something" ; puts ( f . name ); // prints "something" }
3.5. Other Potential Syntaxes
We settled on simply adding the declarator branch to the generic-association syntax because it is, as we understand it, the least problematic and easiest to handle.
There are other ways to introduce the identifier in the sequence.
One possible choice is to use a real introducer for the identifier, so it doesn’t look like a declarator. An example of this is with a , so that the type-name is separated from the identifier, as in: . Other than being conflicting with C++-style syntax of , it also seems a bit unnecessary given the solution we’ve settled on. It might get rid of the disambiguation rule that anything that can be parsed as a type-name should be parsed as a type-name rather than a declarator, where possible. Other than this, there are little benefits, unfortunately.
It could also be something along the lines of for a generic association branch, but a single does not really provide enough information to the parser to know if the following tokens will be the special new syntax versus just the existing syntax where is meant to be parsed as an . One could delay the parsing and decide whether or not its either the new syntax or the old syntax by waiting for the first non-parenthesized from showing up and then deciding how to interpret two top-level colons, but it can be hard to deal with given the use of e.g. nakedly as part of an assignment-expression. (This can make it such that the compiler might have to consider and identifier and an error before the portion, for example.)
4. Prior Art
Other than the "contravariant transformation" detailed in § 2 Introduction and Motivation, there is no direct prior art for this feature in compilers as a language feature.
5. Wording
The wording is relative to the latest Working Draft at time of publication, [n3685].
5.1. Intent
The intent of this wording is to provide:
-
an optional identifier to a generic association branch on the left hand side before the
;: -
an optional identifier to a
generic association branch on the left hand side before thedefault ;: -
both strict type matching and post-lvalue-conversion matching for generics that use a controlling operand where possible;
-
the evaluation of the input expression if and only if a generic branch with an identifier actually matches;
-
and, the ability to potentially modify an l-value if the identifier if the expression produces an l-value.
5.2. Modify "§6.2.1 Scopes of identifiers"
6.2.1 Scopes of identifiersAn identifier can denote:
a standard attribute, an attribute prefix, or an attribute name;
an object;
a function;
a tag or a member of a structure, union, or enumeration;
a typedef name;
- a generic association name;
a label name;
a macro name;
or a macro parameter.
The same identifier can denote different entities at different points in the program. A member of an enumeration is called an enumeration constant. Macro names and macro parameters are not considered further here, because prior to the semantic phase of program translation any occurrences of macro names in the source file are replaced by the preprocessing token sequences that constitute their macro definitions.
For each different entity that an identifier designates, the identifier is visible (i.e. can be used) only within a region of program text called its scope. Different entities designated by the same identifier either have different scopes or are in different name spaces. There are
fourfive kinds of scopes: function, file, block, generic association, and function prototype. (A function prototype is a declaration of a function.)A generic association name is the only kind of identifier that has generic association scope. This scope begins at the
after the declarator or: identifier and ends at the terminatingdefault or, of that generic selection’s generic association (6.5.2.1).) ...
5.3. Modify §6.5.2.1 Generic selection
📝 EDITOR’S NOTE: The constraints text of the generic selection section is moved into three paragraphs to break it up. 📝
6.5.2.1 Generic selectionSyntaxgeneric-selection:
_Generic generic-controlling-operand( generic-assoc-list, ) generic-controlling-operand:
assignment-expression
type-name
generic-assoc-list:
generic-association
generic-assoc-list
generic-association, generic-association:
type-name
assignment-expression: - declarator
assignment-expression:
identifieroptdefault assignment-expression: DescriptionThe controlling type of a generic selection is the type or set of types used to select a branch in the list of generic associations in the generic selection. The generic association type is the type indicated by a generic association’s type name or declarator, if present. The generic association name is either the identifier in the declarator or the identifier after
, if present.default ConstraintsA generic selection shall have no more than one
generic association. No two generic associations in the same generic selection shall specify compatible types.default If the generic controlling operand is an assignment expression, the controlling type of the generic selection expression is the type of the assignment expression as if it had undergone an lvalue conversion,FN) array to pointer conversion, or function to pointer conversion. Otherwise, the controlling type of the generic selection expression is the type designated by the type name. The controlling type shall be compatible with at most one of the types named in the generic association list. If a generic selection has no default generic association, its controlling type shall be compatible with exactly one of the types named in its generic association list.The controlling type shall be compatible with at most one of the types named in the generic association list. If a generic selection has no default generic association, its controlling type shall be compatible with exactly one of the types named in its generic association list.
SemanticsIf the generic controlling operand is an assignment expression, the controlling type of the generic selection expression is first the type of the expression. If no generic association type has a compatible type with this first type, then the controlling type of the generic selection expression is as if it had undergone an lvalue conversion,FN) array to pointer conversion, or function to pointer conversion.
Otherwise, if the generic controlling operand is a type name, the controlling type of the generic selection expression is the type designated by the type name.
The generic controlling operand, sizeSize expressions,and typeof operators contained in the type names of generic associations are not evaluated.If a generic selection has a generic association with a type name that is compatible with the controlling type, then the result expression of the generic selection is the expression in that generic association. Otherwise, the result expression of the generic selection is the expression in the
generic association. None of the expressions from any other generic association of the generic selection is evaluated. The generic association whose expression is evaluated is the selected generic association.default The generic controlling operand is evaluated if and only if both the generic controlling operand is an assignment expression and the selected generic association:
has a declarator with an identifier;
or, is a
generic association with an identifier.default Otherwise, it is not evaluated.
The identifier of a
generic association has the type of the generic controlling operand’s expression and is an lvalue, a function designator, or a void expression if the generic controlling operand is. The identifier of a generic association with a declarator has a type compatible with the generic association type and is an lvalue, a function designator, or a void expression if the generic controlling operand is.default NOTE If evaluated, the generic controlling operand’s expression is evaluated once and only once, regardless of the number of times the generic association name is used or if its even used at all.
The type and value of a generic selection are identical to those of its result expression. It is an lvalue, a function designator, or a void expression if its result expression is, respectively, an lvalue, a function designator, or a void expression.
...
EXAMPLE The following generic selection within a macro appropriately evaluates the incoming expression and uses it to print out information.
#include <stdio.h>#define info(x) _Generic(x, \ int d: printf("[info] %d\n", d), \ struct foo s: printf("[info] %s\n", s.name) \ default v: printf("[info] %p\n", (void*)&v) \ ) struct foo { char * name ; }; double get_d ( void ) { return 0.5 ; } int main () { struct foo f = { "test" }; info ( 3 ); info ( f ); info ( get_d ()); } EXAMPLE If the incoming operand of a generic selection expression is a modifiable lvalue, then such an expression can be used for modification.
#include <stdio.h>#define bar(x) _Generic(x, \ int v: v, \ struct foo v: v.name \ ) struct foo { char * name ; }; int main () { struct foo f = { "test" }; bar ( 3 ); bar ( f ) = "something" ; puts ( f . name ); // prints "something" } EXAMPLE The generic controlling operand has the type of the expression itself when the branch is selected, which may be not exactly the generic association type, even if that branch matches.
#include <stdio.h>#define is_it_const(x) _Generic(x, \ int v: puts("the integer expression is not const"), \ const int: puts("the integer expression is const") \ ) int main () { const int var = 0 ; _Generic ( var , int v : is_it_const ( v ) ); // prints "the integer expression is const" }