Modern C++: nullptr

Saying Goodbye to NULL

Dagang Wei
4 min readJun 24, 2024

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 and 0 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 the nullptr literal.

Resolving Ambiguity:

  • Without the third process(std::nullptr_t) overload, passing nullptr to the function would be ambiguous. The compiler wouldn't know whether to call process(int*) or process(double*) since both could implicitly accept a null pointer.

Distinct Type:

  • The process(std::nullptr_t) overload demonstrates that std::nullptr_t is a separate, unique type. It isn't implicitly convertible to any pointer type. This avoids the potential pitfalls of the old NULL macro, which was essentially a zero integer.

Type Information:

  • The typeid operator is used to display the underlying type of nullptr. The output confirms that it's indeed of type std::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 on NULL or 0.
  • 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.

--

--