C11 nested generics

63 Views Asked by At

I am writing a math library and want to have a call like "add" which is generic, takes two parameters v1 and v2, and calls the right function. If v1 is vec2 and v2 is vec2 then it will call vec2_add, if v1 is vec2 and v2 is float it will call vec2_add_float. But if v1 is vec3 and v2 is vec3 it will call vec3_add etc... A small graph is drawn on the bottom

v1 -> vec2, v2-> vec2: call vec2_add v1 -> vec2, v2-> float: call vec2_add_float

v1 -> vec3, v2-> vec3: call vec3_add v1 -> vec3, v2-> float: call vec3_add_float

I did write a small generic code like so:

#define add(v1, v2) _Generic((v1),                  \
                 vec2: _Generic((v2),           \
                         vec2: vec2_add,        \
                         float: vec2_add_float  \
                         ),             \
                 svec3: _Generic((v2),          \
                         vec3: vec3_add,        \
                         float: vec3_add_float  \
                         )              \
                 )(v1, v2)

For some reason, this works with add_float types but not when I try to add vec2 to vec2 or vec3 to vec3, gives me the error message:

‘_Generic’ selector of type ‘vec2’ is not compatible with any association
   30 |                              vec3: _Generic((v2),                      \

What am I doing wrong here?

2

There are 2 best solutions below

1
tstanisl On BEST ANSWER

The problem is caused by a peculiar feature of "generic selection" that all selection expressions must be valid. When v2 is vec2 then expression _Generic(v2, vec3: vec3_add, float: vec3_add_float) is invalid because neither vec3 nor float is compatible with vec2.

IMO, this is a serious design flaw of generic selection.

The workaround is using default to handle the common case which would be vec2_add is v1 is vec2, or vec3_add if v1 is vec3.

#define add(v1, v2) _Generic((v1),              \
                 vec2: _Generic((v2),           \
                         default: vec2_add,     \
                         float: vec2_add_float  \
                         ),                     \
                 vec3: _Generic((v2),           \
                         default: vec3_add,     \
                         float: vec3_add_float  \
                         )                      \
                 )(v1, v2)
0
Eric Postpischil On

Grammatically, _Generic is an operator, and each of its operands must be a valid expression, even if that operand is not selected by the _Generic.

When v1 and v2 are both vec2, the vec2 case in the outer _Generic is selected, but the vec3 case (I presume svec3 is a typo) must still be valid. The operand in that case is:

_Generic((v2),
    vec3: vec3_add,
    float: vec3_add_float
)

That _Generic expression has no case compatible with v2, a vec2, so the compiler complains.

To fix that, give it a default case. You could use a separate default case with a null function pointer or an error-handling function, such as:

_Generic((v2),
    vec3: vec3_add,
    float: vec3_add_float,
    default: (void (*)(void)) 0
)

or

_Generic((v2),
    vec3: vec3_add,
    float: vec3_add_float,
    default: ErrorHandlingFunction
)

or fold it into one of the other cases, such as:

_Generic((v2),
    vec3: vec3_add,
    default: vec3_add_float,
)

In the future, always provide a minimum reproducible example with questions like this. That will save other people from having to write additional code to test possible solutions.