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

将C要求应用于未选择的通用案例,c,language-lawyer,C,Language Lawyer,(请注意,这是一个语言问题。) 不赞成的问题 更新:我把问题0搞糟了。当我写这篇文章时,我正在看C 2018 6.5.2.2 6中的参数参数类型规则,这些规则不在约束部分,因此编译器可能会忽略它们。我忽略了约束部分中的6.5.2.2,因此需要编译器来诊断不匹配的类型。如果我注意到这一点,我就不会问问题0 在中,我们希望代码如下: int AddVersion0(int a, int b ) { return a+b; } int AddVersion1(int a, int b,

(请注意,这是一个语言问题。)

不赞成的问题 更新:我把问题0搞糟了。当我写这篇文章时,我正在看C 2018 6.5.2.2 6中的参数参数类型规则,这些规则不在约束部分,因此编译器可能会忽略它们。我忽略了约束部分中的6.5.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));
}
GCC 10.2和Apple Clang 11.0都没有对此抱怨

问题1:编译器是否有理由对此提出投诉?由于
NeverCalled
不是用原型声明的,C 2018 6.5.2.2 6不会说任何调用都有未定义的行为,除非函数是用不包含原型的类型定义的,并且参数类型与参数类型不匹配。但是函数根本没有定义,所以条件是 没有触发


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

您的第一个代码段未构成有效的符合标准的翻译单元

当扩展
Foo(AddVersion0,3,4)
时,它基本上变成:

\u通用((AddVersion0),
TypeVersion0:(AddVersion0)((3)、(4)),
TypeVersion1:(AddVersion0)((3),(4),0)
)
就本问题而言,相当于:

\u通用(1,
int:AddVersion0(3,4),
void*:AddVersion0(3,4,0)
)
通用选择的语法定义(见第6.5.1.1节)如下:

语法
一般选择:

\u Generic
赋值表达式,Generic assoc list

一般助理名单:

泛型关联
通用关联列表
通用关联

通用关联:

键入名称
赋值表达式
默认值
赋值表达式

现在,将第二个无效案例的赋值表达式解析为函数调用后缀表达式(§6.5.2)(其中后缀表达式也是赋值表达式):

后缀表达式:

[…]
后缀表达式
参数表达式列表opt

后面关于函数调用的章节(§6.5.2.2p2)在约束段落中指出:

如果表示被调用函数的表达式具有包含原型的类型,则参数的数量应与参数的数量一致

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

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

本标准仅对其他通用关联进行了说明(摘自§6.5.1.1p3):

不会计算泛型选择的任何其他泛型关联中的任何表达式

它并没有说它们可以是无效的表达式,所以也没有例外


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

定义Foo(f,a,b)和泛型((f)\ TypeVersion0:((TypeVersion0)(f))((a)、(b))\ TypeVersion1:((TypeVersion1)(f))((a)、(b)、0)\ ) 但这仍然会在gcc中为“通过不兼容类型调用的函数”发出警告(但不是叮当声)。更改为
((int(*)())(f)
似乎是一种悲观,如果函数位于不同的翻译单元中,则更改调用约定

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

#定义清理(类型,f)_泛型((f),类型:(f),默认值:(类型)0)
您的变通方法的工作原理与此相同(即链接和正确执行):

int-NeverCalled();
int main(){
如果(0)未调用();
}
它是UB,因为通用选择仍然“使用”
NeverCalled

在以下情况下,该行为未定义:

  • [……]
  • 使用带有外部链接的标识符,但在程序中,标识符的外部定义并不完全相同
问题0:该代码是否严格符合C标准,因此GCC和Clang拒绝该代码是错误的

没有

不仅是不匹配 从未在执行程序中计算过的案例

是否计算未选择的表达式并不相关。根据标准中的语法,表达式仍然需要是赋值表达式。展开该结果以了解其如何应用于有争议的代码,我们发现它使用了后缀表达式的选项之一(第6.5.2节).第6.5.2.2节适用,其规定的语言限制包括:

如果表示被调用函数的表达式的类型 包括原型,参数数量应与 参数数量。每个参数的类型应确保 它的值可以指定给具有不合格版本的对象 其相应参数的类型

我并不认为“表示被调用函数的表达式”这一措辞意味着约束仅适用于函数调用被评估的情况,而是作为各种awkwa中的合理选择
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));
}