22

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.

Community
  • 1
  • 1
Shahbaz
  • 46,337
  • 19
  • 116
  • 182

2 Answers2

35

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:

  • A constant expression is any C expression which the compiler can evaluate to a constant without knowing the value of any object (const or otherwise; however, enum values are fair game), and which has no side effects. (This is a drastic simplification of roughly 25 pages of standardese and may not be exact.)

  • Integer constant expressions are a restricted subset of constant expressions, conveniently defined in a single paragraph, C99 6.6p6 and its footnote:

    An integer constant expression96 shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof operator.

    96 An integer constant expression is used to specify the size of a bit-field member of a structure, the value of an enumeration constant, the size of an array, or the value of a case constant. Further constraints that apply to the integer constant expressions used in [#if] are discussed in 6.10.1.

    For purpose of this discussion, the important bit is

    Cast operators ... shall only convert arithmetic types to integer types

    which means that (int *)0 is not an integer constant expression, although it is a constant expression.

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.

zwol
  • 135,547
  • 38
  • 252
  • 361
  • I would appreciate it if you would mention which standard you're talking about, i.e. C or C++. Also, can you give an example of a constant expression that can have `(void *)0` but not `(int *)0`? – Shahbaz Jan 27 '14 at 17:15
  • C, specifically C99; I have not read C11 yet, and I do not remember offhand whether `(void *)0` is still a null pointer *constant* in C++. There *are* differences between C and C++ in this area, for instance `NULL` is *not* allowed to expand to `((void *)0)` in C++. – zwol Jan 27 '14 at 17:16
  • @Zack Both C and C++ require `NULL` to be a null pointer constant. In C99 this includes `(void *)0`, but C++ defines a null pointer constant as "an integral constant expression rvalue of integer type that evaluates to zero." – D Krueger Jan 27 '14 at 18:53
  • @DKrueger I couldn't remember if C++ excluded `((void *)0)` from the definition of a null pointer constant, or if it just said that `NULL` was not allowed to expand to that expression. It seems it was the former. – zwol Jan 28 '14 at 01:57
  • 2
    I'm not sure why `fp c = (void *)(void *)0` is invalid. Can anyone explain? – iBug Nov 22 '17 at 11:31
  • @iBug Because there is no implicit conversion from `void *` to any pointer-to-function type, and the double cast to `void *` makes the right-hand side of the assignment not be a _null pointer constant_ (even though it is still a _constant expression whose value is a null pointer_), so the implicit conversion of null pointer constants to _any_ pointer type is also unavailable. `fp c = (fp)(void *)(void *)0` would be valid again. – zwol Nov 22 '17 at 14:41
  • @zwol, just a small detail. `(void *)(void *)0` is not a null-pointer-constant, but something like `int *x = (void *)(void *)0;` is still fine. That's just the usual `void *` to `any *` assignment that's allowed in C. I haven't verified with the standard, but `gcc -std=c99 -pedantic` actually singles out *function pointers* where assignment from `void *` is invalid: _warning: ISO C forbids initialization between function pointer and 'void *' \[-Wpedantic\]_. Your point is still correct though, just wanted to point this out. – Shahbaz Jun 11 '18 at 18:04
  • @Shahbaz Yes, see https://stackoverflow.com/questions/47446430/how-can-converting-a-pointer-to-void-twice-be-invalid/47446963#47446963 for a bunch of further exposition on this point. – zwol Jun 11 '18 at 18:33
  • @zwol Sir, don't you have a conflict btw first and third examples. In first you said, _long *d = (int *)0; // invalid conversion in both C and C++_ but in third one _assert (x == (int *)0); // must succeed in C, unspecified in C++_ ?? If not, why? Would you mind explaining? – Soner from The Ottoman Empire Mar 31 '19 at 10:44
  • @zwol and my english is not very well as yours, what do you mean by saying "trap" and "trap representation" in this context? – Soner from The Ottoman Empire Mar 31 '19 at 10:46
  • @snr First question: That's an error, thank you for noticing. I will correct the answer. Second question: see https://stackoverflow.com/questions/6725809/trap-representation/6725981 . – zwol Mar 31 '19 at 14:30
12

Evaluating the expression (int*)0 yields a null pointer of type int*.

(int*)0 is not a null pointer constant.

A null pointer constant is a particular kind of expression that may appear in C source code. A null pointer is a value that may occur in a running program.

C and C++ (being two distinct languages) have slightly different rules in this area. C++ doesn't have the "or such an expression cast to type void*" wording. But I don't think that affects the answer to your question.

As for your question about (long long)123, I'm not sure how it's related, but the expression 123 is of type int, and the cast specifies a conversion from int to long long.

I think the core confusion is an assumption that the cast in (int*)0 does not specify a conversion, since 0 is already a null pointer constant. But a null pointer constant is not necessarily an expression of pointer type. In particular, the expression 0 is both a null pointer constant and an expression of type int; it is not of any pointer type. The term null pointer constant needs to be thought of as a single concept, not a phrase whose meaning depends on the individual words that make it up.

Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • However (using GCC-4.8.1/MinGW-32 under Windows 7) `(int *)0 == NULL` does appear to evaluate to `1` (i.e. true). So it does compare equal to a null pointer constant. Is this standard or implementation-defined? – Kninnug Jan 27 '14 at 17:13
  • I fixed the case of `NULL`. But is that your answer to a `[language-lawyer]` tag, or should I be expecting 10 edits per minute before starting to read the answer? – Shahbaz Jan 27 '14 at 17:13
  • @Kninnug Yes, that is standard, but by virtue of a different section of the standard: the definition of `==` says that all null pointers compare equal, regardless of their precise type. – zwol Jan 27 '14 at 17:15
  • @KeithThompson, I'm sorry, I saw a two line answer (which is not very precise and standard-quoting!) and I thought you may be one of them [fastest gun in the west](http://meta.stackexchange.com/q/9731/169090) people. – Shahbaz Jan 27 '14 at 17:18
  • @n.m., I have no quarrel with that. But is `(int *)0` a null pointer? Keith says yes, but no reference or reasoning is given with that. He might as well be just giving his personal opinion. – Shahbaz Jan 27 '14 at 17:22
  • @Shahbaz: You quoted the relevant portion of the standard yourself. `0` is a *null pointer constant*. Converting it to any pointer type yields a *null pointer*. – Keith Thompson Jan 27 '14 at 17:27
  • @KeithThompson, that's where the second question comes in. If `(long long)123` is an `int` converted to `long long`, then yes `(int *)0` is a null pointer converted to `int *`. But if `(long long)123` is straightforward a `long long` number with value 123 (no conversion involved), then `(int *)0` would also be a pointer of `int *` with value 0. I'm searching in the standard for this myself. – Shahbaz Jan 27 '14 at 17:30
  • In both cases, the cast specifies a noon-trivial conversion. Although `0` is a *null pointer constant*, it's still of type `int`. There is no `int*` with the value `0`, since `0` is not of type `int*`. – Keith Thompson Jan 27 '14 at 17:35