Modern C++: nullptr
Saying Goodbye to NULL
This blog post is part of the series Modern C++.
Introduction
C++ has evolved significantly over the years, and one of the most welcome additions in C++11 is the introduction of nullptr
. If you're still using NULL
or the integer 0
for null pointers in your C++ code, it's time for an upgrade. Let's dive into why nullptr
is the preferred choice for null pointers in modern C++.
The Problem with NULL and 0
Historically, C++ inherited NULL
from C. NULL
was often defined as 0
or (void*)0
, and it served as a way to indicate that a pointer didn't point to a valid memory location. However, this approach had a few drawbacks:
- Ambiguity: Using
0
for null pointers could be confusing, as it could also be interpreted as the integer value zero. - Type Safety Issues:
NULL
could be implicitly converted to integral types, potentially leading to unexpected behavior or errors. - Maintenance Challenges: The use of
NULL
and0
lacked clarity and could make code harder to understand and maintain.
Enter nullptr
nullptr
is a keyword in C++ that explicitly represents a null pointer value. It has its own distinct type (std::nullptr_t
), ensuring type safety and eliminating the ambiguity associated with NULL
and 0
.
int* ptr = nullptr; // Clear indication of a null pointer
Here’s an example that demonstrates the unique nature of std::nullptr_t
:
#include <iostream>
#include <typeinfo>
void process(int* ptr) {
std::cout << "Called process(int*) with: ";
if (ptr) {
std::cout << *ptr << std::endl;
} else {
std::cout << "nullptr\n";
}
}
void process(double* ptr) {
std::cout << "Called process(double*) with: ";
if (ptr) {
std::cout << *ptr << std::endl;
} else {
std::cout << "nullptr\n";
}
}
void process(std::nullptr_t) {
std::cout << "Called process(std::nullptr_t)\n";
}
int main() {
int x = 5;
double y = 3.14;
process(&x);
process(&y);
process(nullptr);
// Check the type to confirm:
std::cout << "\nType of nullptr: " << typeid(nullptr).name() << std::endl;
return 0;
}
Output:
Called process(int*) with: 5
Called process(double*) with: 3.14
Called process(std::nullptr_t)
Type of nullptr: St11nullptr_t
Explanation:
Overloaded Functions:
- We define three overloaded functions named
process
: process(int*)
: Accepts a pointer to an integer.process(double*)
: Accepts a pointer to a double.process(std::nullptr_t)
: Accepts thenullptr
literal.
Resolving Ambiguity:
- Without the third
process(std::nullptr_t)
overload, passingnullptr
to the function would be ambiguous. The compiler wouldn't know whether to callprocess(int*)
orprocess(double*)
since both could implicitly accept a null pointer.
Distinct Type:
- The
process(std::nullptr_t)
overload demonstrates thatstd::nullptr_t
is a separate, unique type. It isn't implicitly convertible to any pointer type. This avoids the potential pitfalls of the oldNULL
macro, which was essentially a zero integer.
Type Information:
- The
typeid
operator is used to display the underlying type ofnullptr
. The output confirms that it's indeed of typestd::nullptr_t
.
Advantages of nullptr
- Type Safety:
nullptr
cannot be implicitly converted to integral types, preventing accidental type mismatches and errors. - Improved Readability: Code using
nullptr
is more self-explanatory and easier to understand than code that relies onNULL
or0
. - Compiler Support: Modern compilers provide better diagnostics and error messages when you use
nullptr
incorrectly.
Why Not Using nullptr
Can Cause Problems
Here’s an example where using NULL could cause a problem, and replacing it with nullptr would avoid it:
#include <iostream>
void foo(int* ptr) {
std::cout << "foo(int*) called" << std::endl;
}
void foo(long ptr) {
std::cout << "foo(long) called" << std::endl;
}
int main() {
// Using NULL
foo(NULL); // Ambiguous: Could call foo(int*) or foo(long)
// Using nullptr
foo(nullptr); // Unambiguous: Always calls foo(int*)
return 0;
}
In this example, we have two overloaded functions named `foo`: one that takes an `int*` parameter and another that takes a `long` parameter.
The problem arises when we try to call `foo(NULL)`. In C++, `NULL` is typically defined as `0` or `0L`, which can be implicitly converted to both a pointer type and an integer type. This creates ambiguity for the compiler, as it doesn’t know which overload of `foo` to call.
On the other hand, `nullptr` is a keyword introduced in C++11 that represents a null pointer. It has its own type (`std::nullptr_t`) and can only be converted to pointer types, not to integer types.
When we call `foo(nullptr)`, there’s no ambiguity. The compiler knows to call the `foo(int*)` overload because `nullptr` can only be converted to a pointer type.
This example demonstrates why using `nullptr` is safer and more precise in modern C++ code. It avoids potential ambiguities and makes the intent clearer.
Best Practices for Using nullptr
Use nullptr Consistently: In your modern C++ projects, make nullptr
your default choice for representing null pointers. Avoid using NULL
or 0
.
Check for nullptr: Always check if a pointer is nullptr
before dereferencing it to prevent runtime errors.
if (ptr != nullptr) { // Safely dereference the pointer *ptr = 5; }
Overloaded Functions and nullptr: When you have overloaded functions that accept pointers and integral types, nullptr
provides a way to resolve potential ambiguities:
void process(int value) { /* ... */ }
void process(char* str) { /* ... */ }
process(nullptr); // Calls the function accepting char*
process(0); // Calls the function accepting int
nullptr and Smart Pointers: If you’re using C++ smart pointers (std::unique_ptr
, std::shared_ptr
), they handle null pointer situations gracefully. You don't need to explicitly check for nullptr
when using smart pointers.
#include <iostream>
#include <memory>
int main() {
std::unique_ptr<int> ptr1 = nullptr;
std::shared_ptr<int> ptr2 = std::make_shared<int>(42);
if (!ptr1) { // Checking for nullptr
std::cout << "ptr1 is nullptr\n";
}
if (ptr2) {
std::cout << "ptr2 points to: " << *ptr2 << "\n";
}
}
Conclusion
Embracing nullptr
is a simple yet crucial step towards writing safer, more maintainable, and more modern C++ code. By leaving behind the legacy of NULL
and 0
, you'll eliminate potential pitfalls and ensure your code adheres to best practices. So, bid farewell to NULL
, welcome nullptr
, and elevate the quality of your C++ projects.