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.

ExpressionMeaningSelects f(int)Pointer IntentAssign to int variable
0Integer 0 or null pointer constantPossibleConverts to pointer depending on contextint x = 0 OK
NULLImplementation-defined (0, 0L, etc.)Often integer sideMay differ from intentPossible depending on implementation (warning)
nullptrstd::nullptr_tGenerally NoMatches pointer/nullptr_t overloadImpossible (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

  1. Pointer Initialization/Comparison: = NULL, == NULL, == 0 → nullptr (when the meaning is “pointer null”).
  2. Remove NULL Macro: nullptr is a keyword and needs no header at all; only the type name std::nullptr_t requires <cstddef>. Replacing NULL can therefore also drop includes that existed only to define the macro.
  3. Check for Broken Overloads: If f(NULL) previously called int, after changing to f(nullptr), check unit tests and call sites.
  4. Boundary with C Code: NULL may remain in C compilation units. Gradually unify only the C++ side to nullptr.
  5. Compiler Warnings: Use -Wzero-as-null-pointer-constant (GCC/Clang) to catch 0 null 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

  • auto deduces std::nullptr_t, not a pointer. auto p = nullptr; gives p the type std::nullptr_t, so p = &x; later fails to compile. Write int* 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 when NULL is a 32-bit 0, because the callee reads a pointer-sized value. A std::nullptr_t argument passed through ... is converted to void* by the language, so execl(..., nullptr) or (char*)nullptr is correct.
  • Integer comparisons do not compile. if (count == nullptr) is an error, which is the point, but it also means generic code comparing an arbitrary T against nullptr only works when T is 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.