clanguage-lawyernull-pointer

Is (int *)0 a null pointer?


This could be thought of as an extension to this question (I'm interested in C only, but adding C++ to complete the extension)

The C11 standard at 6.3.2.3.3 says:

An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.

What my take on this personally is that 0 and (void *)0 represent the null pointer, whose integer value may not actually be 0, but that doesn't cover 0 cast to any other type.

But, the standard then continues:

If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, ...

which covers (int *)0 as null pointer since cast is an explicit conversion (C11, 6.3) which is listed under conversion methods.

However, what still makes me wonder is the following phrase

... or such an expression cast to type void * ...

With the above semantics, this phrase seems completely useless. The question is, is this phrase completely useless? If not, what implications does it have? Consequently, is (int *)0 the null pointer or not?


Another question that can help the discussion is the following. Is (long long)123 considered "123 converted to long long", or "123 with type long long". In other words, is there any conversion in (long long)123? If there is none, then the second quote above doesn't cover (int *)0 as a null pointer.


Solution

  • Short answer:

    In both C and C++, (int *)0 is a constant expression whose value is a null pointer. It is not, however, a null pointer constant. The only observable difference between a constant-expression-whose-value-is-a-null-pointer and a null-pointer-constant, that I know of, is that a null-pointer-constant can be assigned to an lvalue of any pointer type, but a constant-expression-whose-value-is-a-null-pointer has a specific pointer type and can only be assigned to an lvalue with a compatible type. In C, but not C++, (void *)0 is also a null pointer constant; this is a special case for void * consistent with the general C-but-not-C++ rule that void * is assignment compatible with any other pointer-to-object type.

    For example:

    long *a = 0;           // ok, 0 is a null pointer constant
    long *b = (long *)0;   // ok, (long *)0 is a null pointer with appropriate type
    long *c = (void *)0;   // ok in C, invalid conversion in C++
    long *d = (int *)0;    // invalid conversion in both C and C++
    

    And here's a case where the difference between the null pointer constant (void *)0 and a constant-expression-whose-value-is-a-null-pointer with type void * is visible, even in C:

    typedef void (*fp)(void);  // any pointer-to-function type will show this effect
    
    fp a = 0;                  // ok, null pointer constant
    fp b = (void *)0;          // ok in C, invalid conversion in C++
    fp c = (void *)(void *)0;  // invalid conversion in both C and C++
    

    Also, it's moot nowadays, but since you brought it up: No matter what the bit representation of longĀ *'s null pointer is, all of these assertions behave as indicated by the comments:

    // 'x' is initialized to a null pointer
    long *x = 0;
    
    // 'y' is initialized to all-bits-zero, which may or may not be the
    // representation of a null pointer; moreover, it might be a "trap
    // representation", UB even to access
    long *y;
    memset(&y, 0, sizeof y);
    
    assert (x == 0);         // must succeed
    assert (x == (long *)0); // must succeed
    assert (x == (void *)0); // must succeed in C, unspecified behavior in C++
    assert (x == (int *)0);  // invalid comparison in both C and C++
    
    assert (memcmp(&x, &y, sizeof y) == 0); // unspecified
    
    assert (y == 0);         // UNDEFINED BEHAVIOR: y may be a trap representation
    assert (y == x);         // UNDEFINED BEHAVIOR: y may be a trap representation
    

    "Unspecified" comparisons do not provoke undefined behavior, but the standard doesn't say whether they evaluate true or false, and the implementation is not required to document which of the two it is, or even to pick one and stick to it. It would be perfectly valid for the above memcmp to alternate between returning 0 and 1 if you called it many times.


    Long answer with standard quotes:

    To understand what a null pointer constant is, you first have to understand what an integer constant expression is, and that's pretty hairy -- a complete understanding requires you to read sections 6.5 and 6.6 of C99 in detail. This is my summary:

    The C++98 definition appears to be more or less equivalent, modulo C++ features and deviations from C. For instance, the stronger separation of character and boolean types from integer types in C++ means that the C++ standard speaks of "integral constant expressions" rather than "integer constant expressions", and then sometimes requires not just an integral constant expression, but an integral constant expression of integer type, excluding char, wchar_t, and bool (and maybe also signed char and unsigned char? it's not clear to me from the text).

    Now, the C99 definition of null pointer constant is what this question is all about, so I'll repeat it: 6.3.2.3p3 says

    An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

    Standardese is very, very literal. Those two sentences mean exactly the same thing as:

    An integer constant expression with the value 0 is called a null pointer constant.
    An integer constant expression with the value 0, cast to type void *, is also a null pointer constant.
    When any null pointer constant is converted to a pointer type, the resulting pointer is called a null pointer and is guaranteed to compare unequal ...

    (Italics - definition of term. Boldface - my emphasis.) So what that means is, in C, (long *)0 and (long *)(void *)0 are two ways of writing exactly the same thing, namely the null pointer with type long *.

    C++ is different. The equivalent text is C++98 4.10 [conv.ptr]:

    A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to zero.

    That's all. "Integral constant expression rvalue of integer type" is very nearly the same thing as C99's "integer constant expression", but there are a few things that qualify in C but not C++: for instance, in C the character literal '\x00' is an integer constant expression, and therefore a null pointer constant, but in C++ it is not an integral constant expression of integer type, so it is not a null pointer constant either.

    More to the point, though, C++ doesn't have the "or such an expression cast to void *" clause. That means that ((void *)0) is not a null pointer constant in C++. It is still a null pointer, but it is not assignment compatible with any other pointer type. This is consistent with C++'s generally pickier type system.


    C++ 2011 and C 2023 both revised the concept of "null pointer", adding a special type for them (nullptr_t) and a new keyword which evaluates to a null pointer constant (nullptr). I do not fully understand the changes and am not going to try to explain them, but I am pretty sure that a bare 0 is still a valid null pointer constant in both.