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); // calls f(int) (or was that a null pointer?)
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 type 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 (nullptr_t, double) int *q = nullptr; // p is a nullptr int *p = 0; // p is a nullptr (for compatibility reasons) int *np = &nullptr; // ERROR: nullptr is an rvalue and can't be referenced 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.