(请注意,这是一个语言律师问题。)
更新:我搞砸了问题 0。当我写这篇文章时,我正在查看 C 2018 6.5.2.2 6 中的参数-参数类型规则,这些规则不在约束部分中,因此可能会被编译器忽略。我忽略了约束部分中的 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));
}
Run Code Online (Sandbox Code Playgroud)
(Foo已经参数化了一个函数f,方便演示和分析。在原来的上下文中,这是不需要的。)
使用默认开关,可选择添加-std=c18,GCC 10.2 和 Apple Clang 11.0 都拒绝此代码,抱怨为错误,而不是警告,一个函数调用的参数太多(AddVersion0在第一次使用的第二种情况下Foo)和太少另一个(AddVersion1在第二种情况下的第一种情况)。
_Generic处理之后实际上不存在,因为_Generic在 C 2018 6.5.1.1 3. GCC 和Clang 正在将函数调用的运行时约束应用于未成为程序一部分的函数调用。6.5.1.1 3 包括:
如果泛型选择具有与与控制表达式的类型兼容的类型名称的泛型关联,则泛型选择的结果表达式是该泛型关联中的表达式。
接下来,请考虑以下解决方法:
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));
}
Run Code Online (Sandbox Code Playgroud)
GCC 10.2 和 Apple Clang 11.0 都没有抱怨这一点。
问题 1:编译器有理由抱怨这个吗?由于NeverCalled没有用原型声明,C 2018 6.5.2.2 6 没有说任何调用具有未定义的行为,除非函数是用不包含原型的类型定义的,并且参数类型与参数类型不匹配。但是该函数根本没有定义,因此不会触发该条件。
(我问编译器是否有理由抱怨,因为当然允许编译器抱怨任何事情,作为不会阻止编译程序的警告,但问题是编译器是否可以推断出此代码的某些方面违反了某些C 标准的方面。)
您的第一个代码片段不构成有效的符合标准的翻译单元。
当Foo(AddVersion0, 3, 4)展开时,它基本上就变成了:
_Generic((AddVersion0),
TypeVersion0: (AddVersion0)((3), (4)),
TypeVersion1: (AddVersion0)((3), (4), 0)
)
Run Code Online (Sandbox Code Playgroud)
就这个问题而言,这相当于:
_Generic(1,
int: AddVersion0(3, 4),
void*: AddVersion0(3, 4, 0)
)
Run Code Online (Sandbox Code Playgroud)
泛型选择的语法定义(在第 6.5.1.1 节中)为:
语法
generic-selection:
_Generic(赋值表达式,通用关联列表)通用关联列表:
泛型关联
泛型 关联列表,泛型关联通用关联:
类型名称
:赋值表达式赋值表达式
default:
现在,第二个无效案例的赋值表达式被解析为函数调用后缀表达式(第 6.5.2 节)(其中后缀表达式也是赋值表达式):
后缀表达式:
[...]
后缀表达式(参数表达式列表选择)
稍后的函数调用部分(第 6.5.2.2p2 节)在约束段落中说:
如果表示被调用函数的表达式的类型包含原型,则参数数量应与参数数量一致。
(其中“被调用函数”AddVersion0隐式转换为带有2个参数原型的函数指针,参数个数为3个)
因此,第二个分支中的表达式违反了“应”要求,因为提供了不同数量的参数。
该标准仅对其他通用关联进行了说明(摘自 §6.5.1.1p3):
不会评估来自通用选择的任何其他通用关联的表达式。
它并没有说它们可以是无效的表达式,因此也不例外。
至于解决方法,您可以转换为正确的函数类型,这不会是 UB,因为永远不会评估错误的类型函数调用:
#define Foo(f, a, b) _Generic((f), \
TypeVersion0: ((TypeVersion0)(f))((a), (b)), \
TypeVersion1: ((TypeVersion1)(f))((a), (b), 0) \
)
Run Code Online (Sandbox Code Playgroud)
但这仍然会在 gcc(但不是 clang)中为“通过不兼容类型调用的函数”带来警告。更改为((int(*)())(f)似乎是一种悲观,如果函数在不同的翻译单元中,则更改调用约定。
您还可以将您的Sanitize与空函数指针一起使用:
#define Sanitize(Type, f) _Generic((f), Type: (f), default: (Type) 0)
Run Code Online (Sandbox Code Playgroud)
您的解决方法的工作原因与此相同(即链接并正确执行):
int NeverCalled();
int main() {
if (0) NeverCalled();
}
Run Code Online (Sandbox Code Playgroud)
它是 UB,因为通用选择仍然“使用” NeverCalled。在附件 J,未定义的行为中,这样写:
在以下情况下,行为未定义:
- [...]
- 使用了具有外部链接的标识符,但在程序中不存在完全一个标识符的外部定义