如何解决将C要求应用于未选定的_Generic案例
(请注意,这是一个语言律师问题。)
不建议使用的问题
更新:我对问题0进行了修改。撰写本文时,我正在查看C 2018 6.5.2.2 6中的参数-参数类型规则,这些规则不在“约束”部分中,因此可能被编译器忽略。我忽略了“约束”部分中的6.5.2.2 2,因此需要编译器来诊断不匹配的类型。如果我注意到这一点,我就不会问0问题。
在this question中,我们需要以下代码:
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,4));
}
({Foo
已通过函数f
进行了参数设置,以方便演示和分析。
在原始上下文中,这不是必需的。)
使用默认开关,还可以选择添加-std=c18
,GCC 10.2和
Apple Clang 11.0 reject this code抱怨是一个错误,而不是警告,一个函数调用的参数太多(在第一个AddVersion0
的第二种情况下,_Generic
),而另一个函数调用的参数太少(在第二种情况下,是AddVersion1
。
问题0:该代码是否严格符合C标准,所以GCC和Clang拒绝该代码是错误的?不匹配的情况不仅不会在执行程序中进行评估,而且在处理_Generic
之后它们实际上不存在,因为_Generic
被定义为生成“结果表达式”而不是“结果值”在C 2018 6.5.1.1中3. GCC和Clang正在对不属于程序一部分的函数调用在函数调用上应用运行时约束。 6.5.1.1 3包括:
如果通用选择的通用名称类型与控制表达式的类型兼容,则通用选择的结果表达式就是该通用关联中的表达式。
问题1
接下来,请考虑以下解决方法:
int AddVersion0(int a,int);
int NeverCalled();
#define Sanitize(Type,f) _Generic((f),Type: (f),default: NeverCalled)
#define Foo(f,\
TypeVersion0: Sanitize(TypeVersion0,(f))((a),\
TypeVersion1: Sanitize(TypeVersion1,4));
}
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)
)
在这个问题上,它等同于:
_Generic(1,int: AddVersion0(3,4),void*: AddVersion0(3,4,0)
)
通用选择的语法在6.5.1.1节中定义为:
语法
通用选择:
_Generic
(
赋值表达式,通用关联列表)
通用关联列表:
通用关联
泛型关联列表,
泛型关联通用关联:
类型名称
:
赋值表达式
default
:
任务表达
现在,第二个无效案例的赋值表达式被解析为函数调用postfix-expression(第6.5.2节)(其中postfix-expressions也是赋值表达式):
后缀表达式:
[...]
后缀表达式(
参数表达式列表 opt)
稍后(第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) \
)
但是,这仍然会在gcc(而不是clang)中发出“通过非兼容类型调用的函数”的警告。更改为((int(*)())(f)
似乎是一种悲观,如果函数位于不同的翻译单元中,则更改调用约定。
您还可以将Sanitize
与空函数指针一起使用:
#define Sanitize(Type,f) _Generic((f),Type: (f),default: (Type) 0)
您的解决方法之所以起作用,是因为它起作用(即,链接并正确执行)的原因:
int NeverCalled();
int main() {
if (0) NeverCalled();
}
它是UB,因为通用选择仍然“使用” NeverCalled
。在附件J,未定义行为中,写为:
,在以下情况下该行为未定义:
- [...]
- 使用了具有外部链接的标识符,但是在程序中并没有确切的标识符外部定义
问题0 :此代码是否严格符合C标准,因此GCC和Clang拒绝它是错误的?
否。
不仅不匹配 案例从未在执行程序中评估过,
是否评估未选择的表达式无关紧要。根据标准中提供的语法,表达式仍需要为 assignment-expressions 。解开该产品以发现其如何应用于所讨论的代码,我们发现它行使了 postfix-expression 的选项之一(第6.5.2节)。适用第6.5.2.2节,其中指定的语言限制包括
如果表示被调用函数的表达式的类型为 包括原型,论据的数量应与 参数数量。每个参数的类型应使得 其值可以分配给具有非限定版本的对象 相应参数的类型。
我不将“表示被调用函数的表达式”一词理解为约束仅在评估函数调用的情况下适用,而是作为各种笨拙的方式中的合理选择。形式语法中 postfix-expression 生成所对应的表达式。
他们实际上没有
_Generic
被定义为 在C 2018 6.5.1.1中产生一个“结果表达式”,而不是“结果值”
否,没有定义泛型选择来生成表达式。在第6.5.1.1节中,术语“结果表达式”定义为根据控制表达式的类型选择的结果,但其含义是根据第6.5.1.1/4段确定确定通用选择的结果:
通用选择的类型和值与 其结果表达式。它是左值,函数指示符或 如果void的结果表达式分别是一个左值,则为void表达式, 函数标识符或空表达式。
作为表达式,结果表达式本身并不是评估通用选择的结果。
尽管任何给定通用选择表达式的结果表达式都是静态已知的,但这不会使该表达式与其他关联(永远不会被评估)不能成为程序的一部分。这是因为通用选择是语言本身的一部分,而不是预处理功能。
- GCC和Clang正在将运行时约束应用于不属于程序一部分的函数调用。
我不接受预处理后源代码中存在的表达式由于死代码而无法成为程序的一部分。我不知道有什么明显的感觉,就某种形式的死代码而言,它们不符合语言约束是可以原谅的。
另一方面,尽管编译器有义务诊断约束违例,但编译器无义务拒绝包含约束的程序。在这种情况下,我会发出关于未选择的表达式的参数计数不匹配的警告,这是一种非常合理的行为,但是仍然可以接受代码,因为不匹配的原因并不特殊。
问题1 :编译器能否有理由对此进行抱怨?由于
_Generic
未使用原型声明,因此C 2018 6.5.2.2 6会声明 除非该函数已已定义,否则不会说任何调用具有未定义的行为 不包含原型和参数类型的类型 与参数类型不匹配。但是函数没有定义在 全部,这样就不会触发条件。
我同意,由于NeverCalled
未被定义,甚至没有通过原型声明,因此即使有可能违反6.5.2.2/2节中的约束,也无法提供依据。由于内部泛型选择的计算结果是函数指定符(允许的),而不是函数调用,并且由于最终不会生成对NeverCalled
的调用,因此我也没有发现善意甚至是有关在没有范围内原型的情况下调用函数的警告的基础。我可以想象,如果编译器的表达式分析不成功,反而会发出这样的警告,但是我认为这样的警告是虚假的。
更新:
但是,在回顾了@Artyer的回答后,我确信第二个示例表现出未定义的行为,这至少构成了认可它的编译器抱怨的合理借口。有关规定来自第6.9 / 5段:
如果在外部链接中使用通过外部链接声明的标识符 表达式(不是作为操作数的一部分
NeverCalled()
或sizeof
运算符,其结果是整数常量), 在整个程序的某处,应该只有一个外部 标识符的定义
由外部标识符_Alignof
表示的函数指示符未用于形成函数调用表达式,不在该要求的排除范围之内,也没有任何其他排除依据。因此,如果程序中没有该函数的外部定义,则该程序违反了约束之外的“必须”要求,因此具有未定义的行为。这将构成编译器投诉的理由,尽管我当然希望没有人会基于这些理由拒绝该程序。
当然,您可以提供NeverCalled
的定义来解决此问题。该定义不需要在泛型选择出现的任何地方的范围内,并且由于永远不会调用该函数,因此尤其是永远不要使用错误的参数数量或类型来调用该函数。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。