MSVC:为什么 "extern void x;" 是 "illegal use of type 'void'"?
MSVC: why "extern void x;" is "illegal use of type 'void'"?
为什么这个代码:
extern void x;
导致:
$ cl t555.c /std:c11 /Za
t555.c(1): error C2182: 'x': illegal use of type 'void'
这里有什么是违法的?
更新。用例:
$ cat t555a.c t555.p.S
#include <stdio.h>
extern void x;
int main(void)
{
printf("%p\n", &x);
return 0;
}
.globl x
x:
.space 4
$ gcc t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && gcc t555a.o t555.p.o && ./a.exe
t555a.c: In function ‘main’:
t555a.c:7:20: warning: taking address of expression of type ‘void’
7 | printf("%p\n", &x);
| ^
0x1004010c0
$ clang t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && clang t555a.o t555.p.o && ./a.exe
t555a.c:7:20: warning: ISO C forbids taking the address of an expression of type 'void' [-Wpedantic]
printf("%p\n", &x);
^~
1 warning generated.
00007FF76E051120
这是一个有趣的案例。声明带有外部链接的 void 类型的标识符 x
似乎没有违反任何约束,但它几乎无法使用。
void
“是一个不完整的对象类型,无法完成”(C 2018 6.2.5 19)。当一个对象的标识符在没有链接的情况下被声明时,该类型必须“在其声明符的末尾完成”(6.7 7)。但对于具有外部链接的标识符来说,情况并非如此;我们可以稍后声明 extern int a[]; extern struct foo b;
并定义 a
和 b
,甚至在另一个翻译单元中也是如此。
如果不使用x
,我看不出它违反任何约束。如果程序尝试使用它,则 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; otherwise, there shall be no more than one.
但是我们不能在C代码中定义x
,因为它有一个不完整的类型,它的类型无法完成。只要未定义,我们就不能在表达式中使用 x
,而不是作为 sizeof
或 _Alignof
的操作数。我们也不能将它与 sizeof
或 _Alignof
一起使用,因为这些运算符需要一个完整的类型。
我们可以想象 x
是在 C 之外定义的,并与此 C 代码链接。因此,某些汇编模块可能会提供 C 代码未知的 x
定义。当然,C 代码不能在没有类型定义的情况下使用对象的值。但是它可以使用x
的地址。例如,它可以用作指针值的哨兵或其他标记。例如,我们可以将指针列表的列表作为指针列表传递给另一个例程,其中子列表由 &x
分隔,并且整个列表的末尾由空指针标记。 (因此两个子列表 (&a
、&b
、&c
) 和 (&d
、&e
、&f
) 将作为 (void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };
.)
然而,使用 Clang 编译 printf("%p\n", &x);
并使用 -pedantic
会产生错误消息“ISO C 禁止获取类型 'void' 的表达式的地址”。我在 C 标准中看不到这样做的原因。它不是 &
运算符的约束(在 6.5.3.2 中)。 6.5.3.2 3 表示“……结果是指向其操作数指定的对象或函数的指针”,所以我们可以说没有对象 x
,所以 &x
不能产生指向任何对象的指针或功能。但这有点牵强。
我认为Clang在这一点上可能是错误的。 6.7 4 说“同一作用域中引用同一对象或函数的所有声明应指定兼容的类型。”但是我没有看到禁止在一个翻译单元中声明 extern void x;
并在另一个翻译单元中声明 int x = 0;
。那么第一个翻译单元应该能够获取对象的地址x
,它甚至应该能够将&x
转换为char *
并读写它的字节。
一方面,Microsoft 可能已经断定无法使用此 x
,因此正在为其发布诊断。然而,虽然编译器可以自由地发出额外的诊断消息,但它应该接受一个符合要求的程序。也就是说,诊断可能是警告,但可能不是阻止编译的错误。
为什么这个代码:
extern void x;
导致:
$ cl t555.c /std:c11 /Za
t555.c(1): error C2182: 'x': illegal use of type 'void'
这里有什么是违法的?
更新。用例:
$ cat t555a.c t555.p.S
#include <stdio.h>
extern void x;
int main(void)
{
printf("%p\n", &x);
return 0;
}
.globl x
x:
.space 4
$ gcc t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && gcc t555a.o t555.p.o && ./a.exe
t555a.c: In function ‘main’:
t555a.c:7:20: warning: taking address of expression of type ‘void’
7 | printf("%p\n", &x);
| ^
0x1004010c0
$ clang t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && clang t555a.o t555.p.o && ./a.exe
t555a.c:7:20: warning: ISO C forbids taking the address of an expression of type 'void' [-Wpedantic]
printf("%p\n", &x);
^~
1 warning generated.
00007FF76E051120
这是一个有趣的案例。声明带有外部链接的 void 类型的标识符 x
似乎没有违反任何约束,但它几乎无法使用。
void
“是一个不完整的对象类型,无法完成”(C 2018 6.2.5 19)。当一个对象的标识符在没有链接的情况下被声明时,该类型必须“在其声明符的末尾完成”(6.7 7)。但对于具有外部链接的标识符来说,情况并非如此;我们可以稍后声明 extern int a[]; extern struct foo b;
并定义 a
和 b
,甚至在另一个翻译单元中也是如此。
如果不使用x
,我看不出它违反任何约束。如果程序尝试使用它,则 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; otherwise, there shall be no more than one.
但是我们不能在C代码中定义x
,因为它有一个不完整的类型,它的类型无法完成。只要未定义,我们就不能在表达式中使用 x
,而不是作为 sizeof
或 _Alignof
的操作数。我们也不能将它与 sizeof
或 _Alignof
一起使用,因为这些运算符需要一个完整的类型。
我们可以想象 x
是在 C 之外定义的,并与此 C 代码链接。因此,某些汇编模块可能会提供 C 代码未知的 x
定义。当然,C 代码不能在没有类型定义的情况下使用对象的值。但是它可以使用x
的地址。例如,它可以用作指针值的哨兵或其他标记。例如,我们可以将指针列表的列表作为指针列表传递给另一个例程,其中子列表由 &x
分隔,并且整个列表的末尾由空指针标记。 (因此两个子列表 (&a
、&b
、&c
) 和 (&d
、&e
、&f
) 将作为 (void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };
.)
然而,使用 Clang 编译 printf("%p\n", &x);
并使用 -pedantic
会产生错误消息“ISO C 禁止获取类型 'void' 的表达式的地址”。我在 C 标准中看不到这样做的原因。它不是 &
运算符的约束(在 6.5.3.2 中)。 6.5.3.2 3 表示“……结果是指向其操作数指定的对象或函数的指针”,所以我们可以说没有对象 x
,所以 &x
不能产生指向任何对象的指针或功能。但这有点牵强。
我认为Clang在这一点上可能是错误的。 6.7 4 说“同一作用域中引用同一对象或函数的所有声明应指定兼容的类型。”但是我没有看到禁止在一个翻译单元中声明 extern void x;
并在另一个翻译单元中声明 int x = 0;
。那么第一个翻译单元应该能够获取对象的地址x
,它甚至应该能够将&x
转换为char *
并读写它的字节。
一方面,Microsoft 可能已经断定无法使用此 x
,因此正在为其发布诊断。然而,虽然编译器可以自由地发出额外的诊断消息,但它应该接受一个符合要求的程序。也就是说,诊断可能是警告,但可能不是阻止编译的错误。