Search code examples
cvisual-c++language-lawyerdeclarationvoid

MSVC: why "extern void x;" is "illegal use of type 'void'"?


Why this code:

extern void x;

leads to:

$ cl t555.c /std:c11 /Za
t555.c(1): error C2182: 'x': illegal use of type 'void'

What is illegal here?

UPD. Use case:

$ 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

Solution

  • This is an interesting case. It does not appear to violate any constraints to declare an identifier x of type void with external linkage, but it is nearly unusable.

    void “is an incomplete object type that cannot be completed” (C 2018 6.2.5 19). When an identifier for an object is declared with no linkage, the type must “be complete by the end of its declarator” (6.7 7). But the same is not true for identifiers with external linkage; we can declare extern int a[]; extern struct foo b; and define a and b later, even in another translation unit.

    If x is not used, I do not see that it violates any constraint. If the program attempted to use it, then 6.9 5 would apply:

    … 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.

    But we cannot define x in C code because it has an incomplete type, and its type cannot be completed. As long as it is not defined, we cannot use x in an expression other than as the operand of sizeof or _Alignof, due the above paragraph, and neither can we use it with sizeof or _Alignof, because those operators require a complete type.

    We could imagine that x is defined outside of C and linked with this C code. So some assembly module might provide a definition for x that is unknown to the C code. Of course, the C code cannot use the value of the object without having a definition for the type. But it could use the address of x. For example, it could serve as a sentinel or other token for pointer values. E.g., we could pass a list of lists of pointers to another routine as a list of pointers where the sublists were separated by &x and the end of the whole list was marked by a null pointer. (So two sublists (&a, &b, &c) and (&d, &e, &f) would be passed as (void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };.)

    However, compiling printf("%p\n", &x); with Clang and using -pedantic produces the error message “ISO C forbids taking the address of an expression of type 'void'”. The core reason for this appears to be that 6.3.2.1 1 excludes an object of void type from being an lvalue:

    • An lvalue is an expression (with an object type other than void) that potentially designates an object;…

    and 6.5.3.2 1 requires the operand of unary & to be an lvalue:

    • The operand of the unary & operator shall be either a function designator, the result of a [] or unary * operator, or an lvalue that designates an object…

    This is likely an incompletely designed part of the C standard, as it does not preclude a const void from being an lvalue, and Clang compiles extern const void x; printf("%p\n", &x); without complaint, but there seems to be no reason for the standard to treat const void and void differently in this regard.

    On the one hand, Microsoft may have concluded there is no way to use this x and so are issuing a diagnostic for it as soon as the extern void x is found rather than letting an error happen when code attempts to use this x. However, while a compiler is free to issue additional diagnostic messages, it ought to accept a conforming program. That is, for a compiler that conforms to the C standard, the diagnostic may be a warning but may not be an error that prevents compilation.

    Supplementary Note

    Noting that the constraint for unary & allows “the result of a [] or unary * operator”, I tested this:

    static void foo(void *p)
    {
        printf("%p\n", &*p);
    }
    

    Here, *p by itself is an lvalue of type void, and this is allowed for & because the constraint specifically allows it, while &x would seem to be a very similar expression, taking the address of a void, but the constraint does not allow it since x is neither an lvalue nor a result of *. Curious.