MSVC:为什么“extern void x;” 是“非法使用类型'void'”吗?

Pav*_*kin 5 c declaration void visual-c++ language-lawyer

为什么这段代码:

\n
extern void x;\n
Run Code Online (Sandbox Code Playgroud)\n

导致:

\n
$ cl t555.c /std:c11 /Za\nt555.c(1): error C2182: 'x': illegal use of type 'void'\n
Run Code Online (Sandbox Code Playgroud)\n

这里什么是非法的?

\n

UPD。使用案例:

\n
$ cat t555a.c t555.p.S\n#include <stdio.h>\n\nextern void x;\n\nint main(void)\n{\n    printf("%p\\n", &x);\n    return 0;\n}\n\n       .globl  x\nx:\n        .space 4\n\n$ 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\nt555a.c: In function \xe2\x80\x98main\xe2\x80\x99:\nt555a.c:7:20: warning: taking address of expression of type \xe2\x80\x98void\xe2\x80\x99\n    7 |     printf("%p\\n", &x);\n      |                    ^\n0x1004010c0\n\n$ 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\nt555a.c:7:20: warning: ISO C forbids taking the address of an expression of type 'void' [-Wpedantic]\n    printf("%p\\n", &x);\n                   ^~\n1 warning generated.\n00007FF76E051120\n
Run Code Online (Sandbox Code Playgroud)\n

Eri*_*hil 5

这是一个有趣的案例。x声明具有外部链接的 void 类型标识符似乎没有违反任何约束,但它几乎无法使用。

\n

void\xe2\x80\x9c 是一个不完整的对象类型,无法完成\xe2\x80\x9d (C 2018 6.2.5 19)。当对象的标识符声明为无链接时,类型必须在其声明符末尾 \xe2\x80\x9c 处完成 (6.7 7)。但对于具有外部链接的标识符来说情况并非如此;我们可以声明extern int a[]; extern struct foo b;a定义b甚至可以在另一个翻译单元中。

\n

如果x不使用,我不认为它违反了任何约束。如果程序尝试使用它,则 6.9 5 将适用:

\n
\n

\xe2\x80\xa6 如果在表达式中使用使用外部链接声明的标识符(而不是作为 asizeof或的操作数的一部分)_Alignof运算符的操作数的一部分),则在整个程序中的某处应恰好有一个外部定义对于标识符;否则,不得超过一个。

\n
\n

但是我们不能x在C代码中定义,因为它有一个不完整的类型,并且它的类型无法完成。只要它没有定义,我们就不能x在表达式中使用它,而不是作为sizeofor的操作数_Alignof,因为上面的段落,我们也不能将它与sizeofor一起使用_Alignof,因为这些运算符需要完整的类型。

\n

我们可以想象它x是在 C 之外定义的并与此 C 代码链接。x因此,某些汇编模块可能会提供C 代码未知的定义。当然,如果没有类型的定义,C 代码就无法使用对象的值。但它可以使用 的地址x。例如,它可以充当指针值的哨兵或其他标记。例如,我们可以将指针列表的列表作为指针列表传递给另一个例程,其中子列表由 分隔&x,整个列表的末尾由空指针标记。(因此两个子列表 ( &a, &b, &c) 和 ( &d, &e, &f) 将作为 传递(void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };。)

\n

但是,printf("%p\\n", &x);使用 Clang 进行编译并使用-pedantic会产生错误消息 \xe2\x80\x9cISO C 禁止获取类型为 'void'\xe2\x80\x9d 的表达式的地址。其核心原因似乎是 6.3.2.1 1 排除了该void类型的对象成为左值:

\n
    \n
  • 是一个可能指定对象的表达式(具有除 之外的对象类型void);\xe2\x80\xa6
  • \n
\n

6.5.3.2 1 要求一元操作数&为左值:

\n
    \n
  • 一元运算符的操作数&应为函数指示符、a[]或 一元运算*符的结果,或者指定对象\xe2\x80\xa6 的左值
  • \n
\n

这可能是 C 标准设计不完整的部分,因为它并不排除 aconst void是左值,并且 Clang 编译时extern const void x; printf("%p\\n", &x);没有任何抱怨,但标准似乎没有理由在这方面进行不同的const void处理。void

\n

一方面,微软可能已经得出结论,没有办法使用它x,因此一旦extern void x发现它就会发出诊断,而不是在代码尝试使用它时发生错误x。然而,虽然编译器可以自由地发出额外的诊断消息,但它应该接受符合要求的程序。也就是说,对于符合C标准的编译器,诊断可能是警告,但可能不是阻止编译的错误。

\n

补充说明

\n

注意到一元约束&允许 \xe2\x80\x9c[]或一元*运算符 \xe2\x80\x9d 的结果,我对此进行了测试:

\n
static void foo(void *p)\n{\n    printf("%p\\n", &*p);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这里,*p它本身是一个类型的左值void,这是允许的&,因为约束明确允许它,而&x似乎是一个非常相似的表达式,采用 a 的地址void,但约束不允许它,因为x它既不是左值也不是 的结果*。好奇的。

\n