将 C 要求应用于未选择的 _Generic 案例

Applying C requirements to unselected _Generic cases

(注意这是一个语言律师问题。)

已弃用的问题

更新:我把问题0搞砸了。写这个的时候,我在看C 2018 6.5.2.2 6中的parameter-argument type rules,不在a中约束部分等可能会被编译器忽略。我忽略了 Constraints 部分中的 6.5.2.2 2,因此需要编译器来诊断不匹配的类型。如果我注意到这一点,我就不会问问题 0。

中,我们需要这样的代码:

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参数化了一个函数f,方便演示分析。 在原来的上下文中,这是不需要的。)

使用默认开关,可选择添加 -std=c18,GCC 10.2 和 Apple Clang 11.0 reject this code 抱怨是一个错误,而不是警告,一个函数调用的参数太多(AddVersion0 在第一次使用 Foo 的第二种情况下)和太少到另一个(AddVersion1 在第二种情况下的第一种情况)。

问题0:这段代码是否严格符合C标准,所以GCC和Clang拒绝它是错误的?不匹配的情况不仅不会在执行程序中评估,而且在处理 _Generic 之后它们实际上也不存在,因为 _Generic 被定义为产生“结果表达式”,而不是“结果值”, in C 2018 6.5.1.1 3. GCC 和 Clang 正在对未成为程序一部分的函数调用应用 运行 时间约束。 6.5.1.1 3 包括:

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.

问题 1

接下来,考虑这个解决方法:

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));
}

GCC 10.2 和 Apple Clang 11.0 都没有抱怨这个。

问题 1:编译器有理由抱怨这个吗?由于 NeverCalled 未使用原型声明,因此 C 2018 6.5.2.2 6 没有说明任何调用具有未定义的行为,除非函数是 defined 且类型不包含原型和参数类型与参数类型不匹配。但是函数根本没有定义,所以条件是 未触发。

(我问编译器是否有投诉的理由,因为当然允许编译器投诉任何事情,作为不会阻止编译程序的警告,但问题是编译器是否可以推断出某些方面此代码的一部分违反了 C 标准的某些方面。)

您的第一个代码片段没有构成有效的 standards-conforming 翻译单元。

展开Foo(AddVersion0, 3, 4)后,基本变成:

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

就此问题而言等同于:

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

通用选择的语法定义(在第 6.5.1.1 节中)为:

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

现在第二个无效案例的 assignment-expression 被解析为函数调用 postfix-expression (§6.5.2)(其中 postfix-expression 也是 assignment-expression):

postfix-expression:

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

后面关于函数调用的部分 (§6.5.2.2p2) 在约束段落中说:

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.

(其中“被调用函数”AddVersion0隐式转换为函数指针,原型为2个参数,参数个数为3)

因此,第二个分支中的表达式违反了“应”要求,因为提供了不同数量的参数。

该标准仅对其他通用关联有此说明(摘自 §6.5.1.1p3):

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

并没有说允许它们是无效的表达式,所以也不例外。


至于解决方法,您可以转换为正确的函数类型,这不是 UB,因为永远不会评估错误类型的函数调用:

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

但这仍然会在 gcc(但不是 clang)中针对“通过 non-compatible 类型调用的函数”发出警告。更改为 ((int(*)())(f) 似乎是一种悲观,如果函数在不同的翻译单元中,则更改调用约定。

您还可以将 Sanitize 与空函数指针一起使用:

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

您的解决方法与此方法的工作原理相同(即正确链接和执行):

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

它是 UB,因为通用选择仍然“使用”NeverCalled。在附件 J,未定义的行为中,这是这样写的:

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?

没有

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

是否计算未选择的表达式是不相关的。根据标准中提供的语法,表达式仍然需要 assignment-expressions。展开该产生式以找出它如何应用于有问题的代码,我们发现它执行了 postfix-expression 的选项之一(第 6.5.2 节)。第 6.5.2.2 节适用,在它指定的语言约束中,

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.

我不认为“表示被调用函数的表达式”的措辞意味着约束仅适用于对函数调用求值的情况,而是在各种笨拙的引用方式中的合理选择到形式文法中 postfix-expression 产生式对应的表达式。

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

不,泛型选择未定义为生成表达式。术语“结果表达式”在第 6.5.1.1 节中定义为根据控制表达式的类型选择的表达式,但其重要性在于根据第 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.

作为表达式,结果表达式本身不是评估泛型选择的结果。

虽然任何给定的泛型选择表达式的结果表达式都是静态已知的,但这并不会使具有其他关联的表达式(永远不会被计算)不能成为程序的一部分。这是泛型选择成为语言本身的一部分的结果,而不是预处理功能。

  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.

我不接受预处理后源中存在的表达式由于是死代码而不能成为程序的一部分。我不知道有什么重要的观点认为其他形式的死代码不能成为“程序的一部分”,因为它们可以免于遵守语言限制。

另一方面,虽然编译器有义务诊断约束违规,但他们没有义务拒绝包含它们的程序。我认为在这种情况下发出关于未选择的表达式的参数计数不匹配的警告是非常合理的行为,但仍然接受代码,因为不匹配不是特定原因。

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.

我同意,由于 NeverCalled 没有定义,甚至没有用原型声明,它没有为甚至可能违反第 6.5.2.2/2 节中的约束提供依据。由于内部通用选择的计算结果为函数指示符(这是允许的),而不是函数调用,并且由于最终不会产生对 NeverCalled() 的调用,我也看不到 善意 甚至是关于在没有 in-scope 原型的情况下调用函数的警告的基础。我可以想象如果编译器的表达式分析不合格,编译器无论如何都会发出这样的警告,但我会认为这样的警告是虚假的。

更新:

但是,在查看了@Artyer 的回答后,我确信第二个示例表现出未定义的行为,这至少构成了编译器识别它的抱怨的合理借口。相关条文来自第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

由外部标识符 NeverCalled 表示的函数指示符不用于形成函数调用表达式不属于该要求的排除项,也没有任何其他排除项的依据。因此,如果程序中没有该函数的外部定义,则程序违反了约束外的“应”要求,因此具有未定义的行为。这将构成编译器抱怨的理由,尽管我当然希望 none 会基于这些理由拒绝该程序。

当然,您可以只提供 NeverCalled() 的定义来解决这个问题。定义不需要在泛型选择出现的任何地方的范围内,并且由于永远不会调用该函数,特别是永远不会用错误的参数数量或类型调用它。