Skip to content

Modern C++ • Null pointer

One of the most controversial features of C++, historically, regarded the use of “null” pointers in a consistent and type-safe manner. Intuitively, a null pointer should have a unique meaning: an empty value that does not point to any object. This has important consequences as it allows to “reset” a pointer variable to a specific value that turns a potentially dangerous delete operation into a no-op.
In older C++ versions the null pointer has been a huge source of ambiguities, directly supported by the standard itself, which quotes that (C++03)

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

thus associating two different meanings to the literal 0 which indicates both an int value and a null pointer constant. This source of ambiguity is one of the legacies that the language inherited from C, which is notoriously bugged with type-safety loopholes. In an attempt to make things look prettier, a name for null pointers was introduce: the NULL macro, which is implementation-dependent but that typically evaluates to the integer constant 0.

This meant that in old C++ you could use the null pointer in all sorts of arithmetic expressions and write something like the following

int t = NULL;                      // is t an int or a pointer ?
double i = NULL + pow(v,2) * k;    // a pointer with momentum
if (NULL < -p*log(p)) {}           // a pointer with not enough entropy
bool b = NULL;                     // a false pointer

But there is also a bigger issue. The ambiguity produced by a type that doesn’t have a unique semantics leads to problems when it comes to function overload resolution. Consider the following code

void f(int a) {}
void f(char* a) {}
 
f(NULL);                        // calls f(int) (wasn't that a pointer?)
f(static_cast<char*>(NULL));    // calls f(char*) (ok but quite verbose)
f(0);

As can be seen, there are a couple of problems stemming from the aforementioned ambiguity that could result in misuses leading to subtle bugs or unwanted effects since there is no error detection (some compilers issue warnings, some other don’t).

C++11 has introduced a more robust and consistent null pointer semantics by introducing a nullptr object with the following characteristics:

1. It is a keyword of the language with its own identity, not a constant integer value
2. It has a type of its own
3. It can be converted to any other pointer or pointer to a class member
4. It cannot be converted to int, bool or any other type
5. It cannot be used in arithmetic expressions and be assigned and compared to integers (except to 0 for backwards compatibility)
6. Its address cannot be taken

Specifically, nullptr is a constant value (rvalue) of type std::nullptr_t (declared in <cstddef>), which is a typedef for decltype(nullptr). The following code shows some use cases and how it eliminates many issues common to old-style C++

void f(int a) {}
void f(char* a) {}
void g(int* a) {}
void g(std::nullptr_t np) {}
 
double i = nullptr + pow(v, 2) * k;  // ERROR: expression must be arithmetic or pointer
int t = nullptr;                     // ERROR: can't assign a nullptr_type to an int
bool b = nullptr;                    // ERROR: can't assign a nullptr_type to a bool
if (nullptr < -p*log(p)) {}          // ERROR: incompatible operands
 
int *q = nullptr;                    // p is a nullptr
int *p = 0;                          // p is a nullptr (for compatibility reasons)
int *np = &nullptr;                  // ERROR: nullptr can't be referenced (rvalue)
 
if (q);                              // evaluates to false
if (q == 0);                         // evaluates to true (for compatibility reasons)
if (p == nullptr);                   // evaluates to true
if (p == q);                         // evaluates to true
     
size_t nps = sizeof(nullptr);        // nullptr has a type and its size can be taken
 
f(nullptr);                          // calls f(char*)
f(0);                                // calls f(int)
g(static_cast<int*>(nullptr));       // calls g(int*)
g(nullptr);                          // calls g(nullptr_t)

Although some inconsistencies must be necessarily kept for backwards compatibility, such as comparing with 0 or NULL, the new design has brought significant improvements over old-style C++ by preventing many misuses that made the code ambiguous and potentially buggy.

So, always use the new nullptr whenever the semantics requires pointer operations and, unless you’re writing code that needs backward compatibility with legacy systems, drop the use of the NULL macro altogether.

Published inModern C++