[[nullptr]] is the modern C++ way of saying "this pointer points at nothing". It replaced the older practice of initialising pointers with the integer literal [[0]], which created a trap: a pointer to [[0]] is false, but so is a pointer to any address whose value happens to be zero, and the compiler happily converts the integer [[0]] into a pointer without complaint. [[nullptr]] is a typed null that only converts to pointers.
Why 0 and NULL were problems
In pre-C++11 code, an uninitialised pointer was often set to NULL or 0. Because 0 is an integer, the compiler allowed this:
#include
int main() {
int* pointer = 0; // the old way: an integer literal, not a pointer
if (pointer == 0) {
std::cout << "pointer is empty\n";
}
return 0;
}It compiles, and it works, but it is wrong in a subtle way. The literal 0 is an int, and the comparison works only because the compiler converts it to a null pointer constant. The moment the code deals with real addresses the special case breaks down.
C++11 introduced nullptr as a dedicated value with its own type, std::nullptr_t, which converts to any pointer type and to no arithmetic type at all.
#include
int main() {
int* pointer = nullptr;
if (pointer == nullptr) {
std::cout << "pointer is empty\n";
}
// This is a compile error: nullptr does not convert to an integer.
// int value = nullptr;
std::cout << "still safe\n";
return 0;
}The output shows the check working:
pointer is empty
still safeThe commented-out line is the point of the whole change. With nullptr the compiler will not let you treat a null pointer as a number.
Checking pointers safely
The standard pattern is to compare against nullptr before dereferencing.
#include
#include
struct Track {
std::string title;
int seconds;
};
int duration_of(const Track* track) {
if (track == nullptr) {
return 0;
}
return track->seconds;
}
int main() {
Track track = {"Tide", 210};
std::cout << "with track: " << duration_of(&track) << '\n';
std::cout << "without one: " << duration_of(nullptr) << '\n';
return 0;
}The output shows both paths:
with track: 210
without one: 0Because a pointer can be null, any function that takes one must either check it or document that it must not be null. std::optional and smart pointers give you better options, which is the next point.
Prefer optional pointers over raw ones
A raw pointer parameter says "this might be null, and I am trusting you to check". A std::optional reference says the same thing in a way the compiler understands.
#include
#include
#include
struct Track {
std::string title;
int seconds;
};
std::optional find_track(const std::string& title) {
if (title == "Tide") {
return Track{"Tide", 210};
}
return std::nullopt;
}
int main() {
auto found = find_track("Tide");
if (found.has_value()) {
std::cout << "found: " << found->title << '\n';
} else {
std::cout << "not found\n";
}
auto missing = find_track("Unknown");
std::cout << "missing has value: " << missing.has_value() << '\n';
return 0;
}The output shows the two cases:
found: Tide
missing has value: 0An std::optional carries the "maybe not there" meaning in its type, so a caller cannot accidentally ignore it.
Conventions that make pointer code readable
- Use
nullptr, never0orNULL, to initialise a pointer. - Compare against
nullptr, never against0. - Name pointer variables clearly. A trailing underscore, or a name that says what it points at, such as
current_track, helps. - Return
nullptrfrom a function that may not find what it was asked for. - Keep ownership obvious. A function that takes
T*does not own the object; a function that takesT&certainly does not; a function that takesstd::unique_ptr<T>does. - Do not
deletea pointer you did not allocate. - Prefer
std::unique_ptrfor exclusive ownership andstd::shared_ptrfor shared ownership.
The nullptr_t type in detail
std::nullptr_t is the type of the nullptr literal. It is useful when you write a template that must distinguish "no pointer" from any real address.
#include
#include
void describe(std::nullptr_t) {
std::cout << "the argument was a null pointer constant\n";
}
int main() {
describe(nullptr);
std::cout << "size of nullptr_t: " << sizeof(std::nullptr_t) << " bytes\n";
return 0;
}The output shows the specialised overload being selected:
the argument was a null pointer constant
size of nullptr_t: 8 bytesIn practice you will rarely name the type directly, but knowing it exists explains why nullptr behaves differently from the integer 0 in overload resolution.
Smart pointers are the modern default
Every raw pointer in a modern C++ program should have an answer to the question "who deletes this?", and the answer should not be a comment. std::unique_ptr says exactly one owner exists and the memory is freed when the owner dies. std::shared_ptr says several owners exist and the memory is freed when the last one dies.
#include
#include
#include
#include
struct Track {
std::string title;
int seconds;
};
int main() {
auto owned = std::make_unique(Track{"Owned", 200});
std::cout << owned->title << ' ' << owned->seconds << '\n';
auto shared = std::make_shared(Track{"Shared", 240});
auto another_view = shared; // two owners of the same track
std::cout << shared.use_count() << " owners of " << shared->title << '\n';
std::vector> queue;
queue.push_back(shared);
std::cout << "after queueing: " << shared.use_count() << " owners\n";
return 0;
}The output shows the ownership count changing as more owners appear:
Owned 200
2 owners of Shared
after queueing: 3 ownersNote that there is no delete anywhere, and no way for the memory to leak. When the program ends, every owner has gone out of scope and every track has been freed.
Quick reference
nullptris a typed null that converts to pointers and to nothing else.- Never initialise a pointer with
0and never compare one with0. - Always check a pointer before dereferencing it, or use
std::optional. - A function taking
T*may receivenullptr; a function takingT&may not. - Use
std::unique_ptrfor exclusive ownership,std::shared_ptrfor shared ownership. std::nullptr_tis the underlying type of the literal.
Try it yourself: write a function that looks up a track by title and returns a pointer to it, or [[nullptr]] if it is not found. Then write a version that returns a [[std::optional]] instead and call both. Finally, write a function that takes a pointer and prints whether it is null, and pass it [[nullptr]] without ever dereferencing it.