Applying C requirements to unselected _Generic cases

Viewed 163

(Note this is a language-lawyer question.)

Deprecated Question

Update: I botched Question 0. When I wrote this, I was looking at the parameter-argument type rules in C 2018 6.5.2.2 6, which are not in a Constraints section and so may be neglected by a compiler. I overlooked 6.5.2.2 2, which is in a Constraints section, so a compiler is required to diagnose mismatched types. I would not have asked Question 0 if I had noticed this.

In this question, we desire code such as:

int AddVersion0(int a, int b       ) { return a+b;   }
int AddVersion1(int a, int b, int c) { return a+b+c; }

typedef int (*TypeVersion0)(int, int);
typedef int (*TypeVersion1)(int, int, int);

#define Foo(f, a, b)    _Generic((f),  \
        TypeVersion0: (f)((a), (b)),   \
        TypeVersion1: (f)((a), (b), 0) \
    )

#include <stdio.h>

int main(void)
{
    printf("%d\n", Foo(AddVersion0, 3, 4));
    printf("%d\n", Foo(AddVersion1, 3, 4));
}

(Foo has been parameterized with a function f to facilitate demonstration and analysis. In the original context, this is not needed.)

With default switches, optionally adding -std=c18, both GCC 10.2 and Apple Clang 11.0 reject this code complaining as an error, not a warning, there are too many arguments to one function call (AddVersion0 in the second case of the first use of Foo) and too few to another (AddVersion1 in the first case of the second).

Question 0: Is this code strictly conforming to the C standard, so GCC and Clang are wrong to reject it? Not only are the mismatched cases never evaluated in an executing program, they effectively do not exist after the _Generic is processed because _Generic is defined to produce a “result expression,” not a “result value,” in C 2018 6.5.1.1 3. GCC and Clang are applying run-time constraints on a function call to a function call that does not become part of the program. 6.5.1.1 3 includes:

If a generic selection has a generic association with a type name that is compatible with the type of the controlling expression, then the result expression of the generic selection is the expression in that generic association.

Question 1

Next, consider this workaround:

int AddVersion0(int a, int b       ) { return a+b;   }
int AddVersion1(int a, int b, int c) { return a+b+c; }

typedef int (*TypeVersion0)(int, int);
typedef int (*TypeVersion1)(int, int, int);

int NeverCalled();
#define Sanitize(Type, f)   _Generic((f), Type: (f), default: NeverCalled)
#define Foo(f, a, b)    _Generic((f),  \
        TypeVersion0: Sanitize(TypeVersion0, (f))((a), (b)),   \
        TypeVersion1: Sanitize(TypeVersion1, (f))((a), (b), 0) \
    )

#include <stdio.h>

int main(void)
{
    printf("%d\n", Foo(AddVersion0, 3, 4));
    printf("%d\n", Foo(AddVersion1, 3, 4));
}

Neither GCC 10.2 nor Apple Clang 11.0 complain about this.

Question 1: Could a compiler have grounds to complain about this? As NeverCalled is not declared with a prototype, C 2018 6.5.2.2 6 does not say any call has undefined behavior unless the function is defined with a type that does not include a prototype and the argument types do not match the parameter types. But the function is not defined at all, so that condition is not triggered.

(I ask whether the compiler has grounds for complaint because of course a compiler is permitted to complain about anything, as a warning that does not prevent compiling the program, but the question is whether a compiler could deduce that some aspect of this code violates some aspect of the C standard.)

2 Answers

Your first code snippet does not form a valid standards-conforming translation unit.

When Foo(AddVersion0, 3, 4) is expanded, it basically becomes:

_Generic((AddVersion0),
    TypeVersion0: (AddVersion0)((3), (4)),
    TypeVersion1: (AddVersion0)((3), (4), 0)
)

which for the purposes of this question is equivalent to:

_Generic(1,
    int: AddVersion0(3, 4),
    void*: AddVersion0(3, 4, 0)
)

The syntax of generic selection is defined (in section 6.5.1.1) as:

Syntax
generic-selection:

_Generic ( assignment-expression, generic-assoc-list )

generic-assoc-list:

generic-association
generic-assoc-list , generic-association

generic-association:

type-name : assignment-expression
default : assignment-expression

Now the second invalid case's assignment-expression is parsed as a function call postfix-expression (§6.5.2) (where postfix-expressions are also assignment-expressions):

postfix-expression:

[...]
postfix-expression ( argument-expression-listopt )

The section on function calls later on (§6.5.2.2p2) says in the constraints paragraphs:

If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters.

(Where the "called function" is AddVersion0 implicitly converted to a function pointer with a prototype with 2 parameters, and the number of arguments is 3)

Thus the expression in the second branch violates a "shall" requirement since a different number of arguments is supplied.

The standard only has this to say about the other generic associations (Excerpt from §6.5.1.1p3):

None of the expressions from any other generic association of the generic selection is evaluated.

It does not say that they are allowed to be invalid expressions, so no exception is made.


As for workarounds, you could cast to the correct function type, which wouldn't be UB since the wrong type function call will never be evaluated:

#define Foo(f, a, b)    _Generic((f),  \
        TypeVersion0: ((TypeVersion0)(f))((a), (b)),   \
        TypeVersion1: ((TypeVersion1)(f))((a), (b), 0) \
    )

But this still brings up warnings in gcc (but not clang) for "function called through a non-compatible type". Changing to ((int(*)())(f) seems to be a pessimisation, changing the calling convention if the function is in a different translation unit.

You could also use your Sanitize with a null function pointer:

#define Sanitize(Type, f)   _Generic((f), Type: (f), default: (Type) 0)

Your workaround works for the same reason that this works (i.e., links and executes properly):

int NeverCalled();
int main() {
    if (0) NeverCalled();
}

It is UB, since the generic selection still "uses" NeverCalled. In annex J, Undefined behavior, this is written:

The behavior is undefined in the following circumstances:

  • [...]
  • An identifier with external linkage is used, but in the program there does not exist exactly one external definition of the identifier

Question 0: Is this code strictly conforming to the C standard, so GCC and Clang are wrong to reject it?

No.

Not only are the mismatched cases never evaluated in an executing program,

Whether unselected expressions are evaluated is not relevant. The expressions still need to be assignment-expressions according to the grammar presented in the standard. Unwinding that production to find how it applies to the code at issue, we find that it exercises one of the options for a postfix-expression (section 6.5.2). Section 6.5.2.2 applies, and among the language constraints it it specifies is that

If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. Each argument shall have a type such that itsvalue may be assigned to an object with the unqualified version of the type of its corresponding parameter.

I do not take the wording "the expression that denotes the called function" to mean that the constraint applies only in the event that the function call is evaluated, but rather as a reasonable choice among the various awkward ways of referring to the expression corresponding to the postfix-expression production in the formal grammar.

they effectively do not exist after the _Generic is processed because _Generic is defined to produce a “result expression,” not a “result value,” in C 2018 6.5.1.1

No, a generic selection is not defined to produce an expression. The term "result expression" is defined in section 6.5.1.1 as the one that is selected based on the type of the controlling expression, but its significance is in determining the result of evaluating the generic selection, per paragraph 6.5.1.1/4:

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.

The result expression is not itself, as an expression, the result of evaluating the generic selection.

Although the result expression for any given generic selection expression is known statically, that does not make the expressions with other associations, which will never be evaluated, fail to be part of the program. This is a consequence of generic selection being part of the language proper, as opposed to being a preprocessing feature.

  1. GCC and Clang are applying run-time constraints on a function call to a function call that does not become part of the program.

I do not accept that expressions present in the source after preprocessing fail to be part of the program on account of being dead code. I am not aware of any significant sentiment that other forms of dead code fail to be "part of the program" in the sense that they are excused from conforming to language constraints.

On the other hand, although compilers are obligated to diagnose constraint violations, they are not obligated to reject programs containing them. I would consider it an eminently reasonable behavior in this case to emit warnings about the argument count mismatch for the unselected expressions, but accept the code nevertheless, since the mismatches are of no particular account.

Question 1: Could a compiler have grounds to complain about this? As NeverCalled is not declared with a prototype, C 2018 6.5.2.2 6 does not say any call has undefined behavior unless the function is defined with a type that does not include a prototype and the argument types do not match the parameter types. But the function is not defined at all, so that condition is not triggered.

I agree that since NeverCalled is not defined or even declared with a prototype, it provides no basis for even a potential violation of the constraints in section 6.5.2.2/2. Since the inner generic selection evaluates to a function designator (which is allowed), not a function call, and since that does not end up producing a call to NeverCalled(), I also see no bona fide basis for even a warning about calling a function without an in-scope prototype. I could imagine a compiler issuing such a warning anyway if its expression analysis were not up to snuff, but I would consider such a warning spurious.

Update:

However, having reviewed @Artyer's answer, I am convinced that the second example exhibits undefined behavior, which constitutes at least a reasonable pretext for a compiler that recognizes it to complain. The relevant provision is from paragraph 6.9/5:

If an identifier declared with external linkage is used in an expression (other than as part of the operand of a sizeof or _Alignof operator whose result is an integer constant), somewhere in the entire program there shall be exactly one external definition for the identifier

That the function designator represented by the external identifier NeverCalled is not used to form a function call expression is not among the exclusions from that requirement, nor is there any other basis for an exclusion. Therefore, if there is no external definition of that function in the program then the program violates a "shall" requirement outside a constraint, and so has undefined behavior. That would constitute grounds for a compiler to complain, though I certainly hope none would reject the program on those grounds.

Of course, you could just provide a definition of NeverCalled() to work around that. The definition does not need to be in scope anywhere that the generic selection appears, and since the function is never called, it is in particular never called with the wrong number or type of arguments.

Related