C++ nullptr vs NULL: The Overload Bug NULL Causes and What std::nullptr_t Fixes
Key takeaways
Passing NULL to an overloaded function can call the int version instead of the pointer one. The post shows how that happens, how nullptr and nullptr_t fix it, how nullptr behaves in boolean checks and with smart pointers, and how to convert older NULL-heavy code.
What is nullptr?
nullptr is a type-safe null pointer literal introduced in C++11. It solves the problems of NULL and 0, providing a clear null value that can only be used with pointer types.
// Before C++03
int* p1 = NULL; // Macro (usually 0)
int* p2 = 0; // Integer 0
// After C++11
int* p3 = nullptr; // Type-safe
Why is it needed?:
- Type Safety: Prevents confusion between integers and pointers
- Overload Resolution: Clear selection in function overloading
- Template Safety: Solves type deduction problems in templates
- Clarity: Clearly expresses code intent
// ❌ NULL's problem: interpreted as integer
#define NULL 0
void func(int x) { std::cout << "int\n"; }
void func(int* ptr) { std::cout << "pointer\n"; }
func(NULL); // Outputs "int" (not intended!)
// ✅ nullptr: clearly interpreted as pointer
func(nullptr); // Outputs "pointer" (as intended)
nullptr, NULL, 0 — What’s the Difference?
Only nullptr is a literal specialized for “pointer null”. 0 is an integer literal, and NULL is usually a macro defined as 0, so it overlaps with integer overloads in priority.
| Expression | Meaning | Selects f(int) | Pointer Intent | Assign to int variable |
|---|---|---|---|---|
0 | Integer 0 or null pointer constant | Possible | Converts to pointer depending on context | int x = 0 OK |
NULL | Implementation-defined (0, 0L, etc.) | Often integer side | May differ from intent | Possible depending on implementation (warning) |
nullptr | std::nullptr_t | Generally No | Matches pointer/nullptr_t overload | Impossible (type-safe) |
In C, NULL is often ((void*)0), but C++ forbids that definition because void* does not implicitly convert to other pointer types in C++. So C++ implementations pick an integer form: MSVC uses plain 0, while GCC and Clang on Linux use the builtin __null, which behaves like an integer of pointer width. The practical consequence is that f(NULL) with f(int) and f(int*) overloads silently calls f(int) on MSVC, but on GCC/Clang it is usually a hard error (call of overloaded 'f(NULL)' is ambiguous), because converting __null to int and to int* rank the same. Code that “works” on one compiler breaks on another, which is the real portability argument for nullptr: its type is fixed by the standard, not by a header.
Function Overloading Problem (Advanced)
APIs that accept both int and int* are common in legacy code. In this case, NULL and 0 tend to bind to the “integer” side, and bugs occur where the int overload is called when the intent is “no pointer”. nullptr is passed as std::nullptr_t, so it matches well with pointer-related overloads.
If you have a std::nullptr_t-specific overload, you can separate a branch that handles “only null literals”.
#include <cstddef>
#include <iostream>
// Example execution
void f(int) { std::cout << "int\n"; }
void f(int*) { std::cout << "int*\n"; }
void f(std::nullptr_t) { std::cout << "nullptr_t\n"; }
int main() {
f(0); // Usually int
f(NULL); // Usually int (implementation-dependent)
f(nullptr); // nullptr_t overload
int* p = nullptr;
f(p); // int*
}
Type of nullptr:
The type of nullptr is std::nullptr_t. It can be implicitly converted to all pointer types, but cannot be converted to integer types.
#include <cstddef>
// Type of nullptr
using nullptr_t = decltype(nullptr);
int* p1 = nullptr; // OK: converts to pointer
char* p2 = nullptr; // OK: converts to pointer
void* p3 = nullptr; // OK: converts to pointer
// int x = nullptr; // Error: cannot convert to integer
How nullptr Works:
nullptr is a prvalue of the special type std::nullptr_t. The compiler implicitly converts nullptr to all pointer types, but does not convert it to integer types.
// Conceptual implementation
struct nullptr_t {
// Can convert to all pointer types
template<typename T>
operator T*() const { return 0; }
// Can also convert to member pointers
template<typename C, typename T>
operator T C::*() const { return 0; }
// Cannot convert to int
operator int() const = delete;
};
const nullptr_t nullptr = {};
Problems with NULL
#define NULL 0
void func(int x) {
cout << "int: " << x << endl;
}
void func(int* ptr) {
cout << "pointer" << endl;
}
int main() {
func(NULL); // int version with a plain-0 NULL; ambiguity error with GCC's __null
func(nullptr); // pointer version called
}
Either outcome is bad. The silent case is worse, because the call compiles and passes 0 as a count or an ID where the author meant “no buffer”. I have seen this kind of bug survive review precisely because the call site reads like it passes a null pointer; only stepping into the function in a debugger shows the int overload running.
The template forwarding problem
The overload issue is the famous one, but the failure that actually makes 0/NULL unusable in modern C++ is template argument deduction. Once 0 passes through a template, it is no longer a null pointer constant; it is just a value of type int, and an int variable does not convert to a pointer:
#include <memory>
void consume(int* p);
template<class F, class Arg>
void callWith(F f, Arg&& arg) {
f(std::forward<Arg>(arg));
}
callWith(consume, 0); // error: invalid conversion from 'int' to 'int*'
callWith(consume, NULL); // error (or a warning plus a bogus call), same reason
callWith(consume, nullptr); // OK: std::nullptr_t converts to int*
The same thing happens with std::make_shared, emplace_back, std::thread and anything else that perfect-forwards its arguments. nullptr survives forwarding because its type, std::nullptr_t, carries the “null pointer” meaning with it, while the literal 0 only has that meaning at the exact spot where it is written.
Advantages of nullptr
// 1. Type Safety
int* p = nullptr; // OK
int x = nullptr; // Error: cannot assign to integer
// 2. Overload Resolution
void func(int);
void func(char*);
func(0); // int version
func(NULL); // int version or ambiguity error, depending on the compiler (problem!)
func(nullptr); // char* version (as intended)
// 3. Safe in Templates
template<typename T>
void process(T* ptr) {
if (ptr == nullptr) {
// ...
}
}
Basic Usage
int* ptr = nullptr;
// nullptr check
if (ptr == nullptr) {
cout << "null pointer" << endl;
}
if (!ptr) {
cout << "null pointer" << endl;
}
// Function argument
void func(int* p = nullptr) {
if (p) {
*p = 10;
}
}
Practical Examples
Example 1: Safe Pointer Usage
class Node {
public:
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
class LinkedList {
private:
Node* head;
public:
LinkedList() : head(nullptr) {}
~LinkedList() {
while (head != nullptr) {
Node* temp = head;
head = head->next;
delete temp;
}
}
void append(int data) {
Node* newNode = new Node(data);
if (head == nullptr) {
head = newNode;
return;
}
Node* current = head;
while (current->next != nullptr) {
current = current->next;
}
current->next = newNode;
}
void print() const {
Node* current = head;
while (current != nullptr) {
cout << current->data << " -> ";
current = current->next;
}
cout << "nullptr" << endl;
}
};
int main() {
LinkedList list;
list.append(1);
list.append(2);
list.append(3);
list.print();
}
Example 2: Optional Parameters
class Logger {
private:
ofstream* file;
public:
Logger(const string* filename = nullptr) : file(nullptr) {
if (filename != nullptr) {
file = new ofstream(*filename);
}
}
~Logger() {
if (file != nullptr) {
file->close();
delete file;
}
}
void log(const string& message) {
if (file != nullptr) {
*file << message << endl;
} else {
cout << message << endl;
}
}
};
int main() {
// File logging
string filename = "log.txt";
Logger fileLogger(&filename);
fileLogger.log("Write to file");
// Console logging
Logger consoleLogger;
consoleLogger.log("Output to console");
}
Example 3: Factory Pattern
class Shape {
public:
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Circle" << endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
cout << "Rectangle" << endl;
}
};
Shape* createShape(const string& type) {
if (type == "circle") {
return new Circle();
} else if (type == "rectangle") {
return new Rectangle();
}
return nullptr; // Unknown type
}
int main() {
Shape* shape = createShape("circle");
if (shape != nullptr) {
shape->draw();
delete shape;
} else {
cout << "Unknown shape" << endl;
}
}
Example 4: With Smart Pointers
#include <memory>
class Resource {
public:
Resource() {
cout << "Resource created" << endl;
}
~Resource() {
cout << "Resource destroyed" << endl;
}
void use() {
cout << "Resource used" << endl;
}
};
int main() {
unique_ptr<Resource> ptr1 = nullptr;
if (ptr1 == nullptr) {
cout << "ptr1 is nullptr" << endl;
}
ptr1 = make_unique<Resource>();
if (ptr1 != nullptr) {
ptr1->use();
}
// Reset to nullptr
ptr1 = nullptr; // Resource destroyed
}
unique_ptr has a dedicated operator=(std::nullptr_t) and a constructor taking std::nullptr_t, which is why ptr1 = nullptr compiles while ptr1 = 0 only works by accident of 0 being a null pointer constant, and ptr1 = NULL fails on GCC. Assigning nullptr is equivalent to ptr1.reset(): the old object is destroyed immediately, not at the end of the scope. Compare with the Logger in Example 2, which owns a raw ofstream* and has to pair every new with a delete; with std::unique_ptr<std::ofstream> the destructor and the nullptr checks around close() would disappear, and a copied Logger would no longer double-delete the stream.
nullptr_t Type
#include <cstddef>
// Type of nullptr
using nullptr_t = decltype(nullptr);
void func(nullptr_t) {
cout << "Received nullptr" << endl;
}
void func(int*) {
cout << "Received pointer" << endl;
}
int main() {
func(nullptr); // nullptr version
int* p = nullptr;
func(p); // pointer version
}
nullptr_t with Templates and Auto Conversion
std::nullptr_t is defined in <cstddef> and is the same as decltype(nullptr). It’s useful when you want to accept “only null pointers” in template arguments or auto deduction.
#include <cstddef>
#include <type_traits>
template<class T>
void reset(T* p) {
delete p;
}
// Separate overload for nullptr_t-specific handling if needed
void take(std::nullptr_t) { /* no-op */ }
void take(int* p) { /* ... */ }
static_assert(std::is_same<decltype(nullptr), std::nullptr_t>::value, "");
// C++17+: std::is_same_v<decltype(nullptr), std::nullptr_t>
nullptr is also used the same way for member pointers, and the big advantage over 0/NULL is no implicit conversion to integers.
Practical Example: nullptr in API Design
Return Values: While patterns using optional/expected (C++23) are also common, if it’s a raw pointer API, it’s typical to document that nullptr is the only failure value.
// Returns nullptr on lookup failure — caller must always check
const Widget* findWidget(int id) const;
// Default argument: "no options"
void connect(const Options* opts = nullptr);
Smart Pointers: std::unique_ptr<T>/shared_ptr<T> have clear semantics when reset to nullptr, and if (ptr) naturally connects with operator bool.
Migrating Pre-C++11 Code
- Pointer Initialization/Comparison:
= NULL,== NULL,== 0→ nullptr (when the meaning is “pointer null”). - Remove
NULLMacro:nullptris a keyword and needs no header at all; only the type namestd::nullptr_trequires<cstddef>. ReplacingNULLcan therefore also drop includes that existed only to define the macro. - Check for Broken Overloads: If
f(NULL)previously calledint, after changing tof(nullptr), check unit tests and call sites. - Boundary with C Code:
NULLmay remain in C compilation units. Gradually unify only the C++ side tonullptr. - Compiler Warnings: Use
-Wzero-as-null-pointer-constant(GCC/Clang) to catch0null constant usage and fix to nullptr.
// Before (C++03 style)
void bar(int* p);
bar(0);
bar(NULL);
// After (C++11+)
void bar(int* p);
bar(nullptr);
Do not do this migration with a blind search-and-replace of NULL. Some code uses NULL where it means the character '\0' or an integer zero (char c = NULL;, memset(buf, NULL, n)); after replacement those lines stop compiling, which is actually useful, since each error points at a latent type confusion. The safer route is clang-tidy -checks=modernize-use-nullptr -fix, which only rewrites expressions that are used as pointers. Run it, then turn on -Wzero-as-null-pointer-constant so new 0-as-pointer uses fail the build.
Where nullptr still needs care
autodeducesstd::nullptr_t, not a pointer.auto p = nullptr;givespthe typestd::nullptr_t, sop = &x;later fails to compile. Writeint* p = nullptr;when you mean a pointer variable.- Variadic C functions. Arguments passed through
...get no conversion to the parameter type.execl("/bin/ls", "ls", NULL)is technically wrong on 64-bit platforms whenNULLis a 32-bit0, because the callee reads a pointer-sized value. Astd::nullptr_targument passed through...is converted tovoid*by the language, soexecl(..., nullptr)or(char*)nullptris correct. - Integer comparisons do not compile.
if (count == nullptr)is an error, which is the point, but it also means generic code comparing an arbitraryTagainstnullptronly works whenTis a pointer, smart pointer,std::function, or another type that defines that comparison.
Common Problems
Problem 1: Using NULL
// ❌ Still using NULL after C++11
void func(int* ptr) {
if (ptr == NULL) { // Old-fashioned
// ...
}
}
// ✅ Use nullptr
void func(int* ptr) {
if (ptr == nullptr) {
// ...
}
}
Problem 2: Confusion with Integers
// ❌ Using 0 as null pointer
int* ptr = 0;
if (ptr == 0) {
// ...
}
// ✅ Use nullptr
int* ptr = nullptr;
if (ptr == nullptr) {
// ...
}
Problem 3: Overloading Issues
void process(int x) {
cout << "int" << endl;
}
void process(int* ptr) {
cout << "pointer" << endl;
}
// ❌ Using NULL (ambiguous)
process(NULL); // int version called (not intended)
// ✅ Use nullptr
process(nullptr); // pointer version called
nullptr Check Methods
int* ptr = nullptr;
// Method 1: Explicit comparison
if (ptr == nullptr) {
cout << "null" << endl;
}
// Method 2: Implicit conversion
if (!ptr) {
cout << "null" << endl;
}
// Method 3: Logical operation
if (ptr) {
*ptr = 10;
} else {
cout << "null" << endl;
}
NULL vs nullptr Comparison
// NULL (C++03)
#define NULL 0 // or ((void*)0)
int* p1 = NULL;
int x = NULL; // OK (problem!)
// nullptr (C++11)
int* p2 = nullptr;
// int y = nullptr; // Error (type-safe)
// In templates
template<typename T>
void func(T value) {
if (value == nullptr) { // OK
// ...
}
}
func(NULL); // Compile error (NULL is int)
func(nullptr); // OK
Practical Patterns
Pattern 1: Safe Pointer Return
template<typename T>
class Repository {
std::map<int, T> storage_;
public:
// Safe return using nullptr
T* find(int id) {
auto it = storage_.find(id);
if (it != storage_.end()) {
return &it->second;
}
return nullptr; // Not found
}
// Usage
void process(int id) {
T* item = find(id);
if (item != nullptr) {
// Use safely
item->process();
} else {
std::cout << "Item not found\n";
}
}
};
Pattern 2: Optional Callback
class EventHandler {
using Callback = void(*)();
Callback onSuccess_ = nullptr;
Callback onError_ = nullptr;
public:
void setOnSuccess(Callback cb) {
onSuccess_ = cb;
}
void setOnError(Callback cb) {
onError_ = cb;
}
void execute() {
try {
// Perform work
if (onSuccess_ != nullptr) {
onSuccess_();
}
} catch (...) {
if (onError_ != nullptr) {
onError_();
}
}
}
};
// Usage
EventHandler handler;
handler.setOnSuccess([] { std::cout << "Success\n"; });
handler.execute();
Pattern 3: RAII Guard
template<typename T>
class ScopedPtr {
T* ptr_;
public:
explicit ScopedPtr(T* p = nullptr) : ptr_(p) {}
~ScopedPtr() {
if (ptr_ != nullptr) {
delete ptr_;
}
}
// Disable copying
ScopedPtr(const ScopedPtr&) = delete;
ScopedPtr& operator=(const ScopedPtr&) = delete;
// Allow moving
ScopedPtr(ScopedPtr&& other) noexcept : ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
ScopedPtr& operator=(ScopedPtr&& other) noexcept {
if (this != &other) {
if (ptr_ != nullptr) {
delete ptr_;
}
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T* get() const { return ptr_; }
T* operator->() const { return ptr_; }
T& operator*() const { return *ptr_; }
explicit operator bool() const {
return ptr_ != nullptr;
}
};
// Usage
ScopedPtr<int> ptr(new int(42));
if (ptr) {
std::cout << *ptr << '\n';
}
The if (ptr_ != nullptr) guards before delete are harmless but redundant: delete on a null pointer is defined to do nothing. What matters in this class is the other direction, setting other.ptr_ = nullptr in the move operations. Without it, both the moved-from and the moved-to object would delete the same pointer, and the program would crash with a double free at the end of the scope. This “null out the source after moving” step is where nullptr earns its place in resource-owning classes.
FAQ
Q1: nullptr vs NULL?
A:
- nullptr: Type-safe, C++11+, pointer-only
- NULL: Macro (usually
0), type-unsafe, can be interpreted as integer
// NULL's problem
void func(int x) { std::cout << "int\n"; }
void func(int* ptr) { std::cout << "pointer\n"; }
func(NULL); // Outputs "int" (problem!)
func(nullptr); // Outputs "pointer" (as intended)
Recommendation: Always use nullptr in C++11 and later
Q2: nullptr vs 0?
A:
- nullptr: Pointer-only, type-safe
- 0: Both integer and pointer possible, can be confusing
int* p1 = 0; // OK (but confusing)
int* p2 = nullptr; // OK (clear)
int x = 0; // OK
// int y = nullptr; // Error: cannot assign to integer
Q3: What type is nullptr?
A: It’s of type std::nullptr_t. It can be implicitly converted to all pointer types, but cannot be converted to integer types.
#include <cstddef>
std::nullptr_t null = nullptr;
int* p1 = null; // OK
char* p2 = null; // OK
// int x = null; // Error
Q4: Can you assign nullptr to an integer?
A: No. A compile error occurs. This is nullptr’s type safety.
int* ptr = nullptr; // OK
// int x = nullptr; // Error: cannot assign to integer
// Explicit casting also not possible
// int y = static_cast<int>(nullptr); // Error
Q5: When should you use nullptr?
A: Always. In C++11 and later, it’s recommended to use nullptr instead of NULL or 0.
// ❌ Old-fashioned
int* p1 = NULL;
int* p2 = 0;
// ✅ Modern
int* p3 = nullptr;
Q6: Is nullptr evaluated as false?
A: Yes. nullptr is evaluated as false in boolean context.
int* ptr = nullptr;
if (ptr) {
// Not executed
}
if (!ptr) {
std::cout << "null pointer\n"; // Executed
}
// Explicit comparison
if (ptr == nullptr) {
std::cout << "null pointer\n"; // Executed
}
Q7: How do you use nullptr in function overloading?
A: nullptr clearly selects the pointer overload.
void process(int x) {
std::cout << "int: " << x << '\n';
}
void process(int* ptr) {
std::cout << "pointer\n";
}
void process(std::nullptr_t) {
std::cout << "nullptr\n";
}
process(0); // "int: 0"
process(NULL); // "int: 0" on MSVC, ambiguity error on GCC/Clang
process(nullptr); // "nullptr" (exact match beats the conversion to int*)
int* p = nullptr;
process(p); // "pointer"
Q8: Learning resources for nullptr?
A:
- “Effective Modern C++” (Item 8: Prefer nullptr to 0 and NULL) by Scott Meyers
- cppreference.com - nullptr
- “C++ Primer” (5th Edition) by Stanley Lippman
One-line Summary: nullptr is a type-safe C++11 null pointer literal that solves the problems of NULL and 0.
Related Articles
- C++ Function Overloading
- 10 C++ Compile and Link Errors Beginners Hit First, and What Each Message Means
- Pointer Basics
- Smart Pointers and RAII
- How C++ auto Deduces Types: References, const, Braced Init and decltype(auto)