Skip to content
C++
Language
since C++11
Basic

nullptr

The null pointer literal introduced in C++11, typed as std::nullptr_t, that replaces NULL and 0 in pointer contexts with full type safety.

nullptrsince C++11

nullptr is a keyword and prvalue of type std::nullptr_t that represents the universally null pointer constant, converting implicitly to any pointer or pointer-to-member type but never to an integer type.

Overview

Before C++11, null pointers were expressed as the integer literal 0 or the macro NULL, which typically expands to 0 or (void*)0 depending on the implementation. Both are fundamentally integer values that the compiler permits in pointer contexts through implicit conversion. This creates two distinct problems: overload resolution becomes ambiguous between pointer and integer overloads, and generic code loses the null-pointer meaning when the integer constant is deduced as int.

C++11 introduced nullptr as a dedicated null pointer literal with its own type: std::nullptr_t, declared in <cstddef>. This type is distinct from all pointer types, yet it converts implicitly to any pointer type and any pointer-to-member type. Critically, it does not convert to int or any other arithmetic type. That single distinction eliminates the class of overload ambiguity and template deduction failures that plagued pre-C++11 null pointer handling.

nullptr is a keyword, not a macro. It is a prvalue (pure rvalue) in the sense of C++11 value categories β€” it has no identity, no address, and cannot appear on the left-hand side of an assignment. Every expression of type std::nullptr_t is a null pointer constant, meaning it converts to null of any pointer type.

std::nullptr_t is a complete, regular type. Variables may be declared of type std::nullptr_t, it may appear as a template argument, it participates in overload resolution as its own distinct type, and since C++11 it may appear in constant expressions evaluated at compile time.

Syntax

cpp
nullptr

The keyword itself requires no header. To name the type std::nullptr_t explicitly in code, include <cstddef>.

cpp
#include <cstddef>

std::nullptr_t np = nullptr;    // C++11: variable of type std::nullptr_t
int*          ip = nullptr;    // C++11: null int*
void        (*fn)() = nullptr; // C++11: null function pointer
int     Foo::* mp = nullptr;   // C++11: null pointer-to-data-member

Examples

Overload Resolution β€” The Core Motivation

cpp
#include <cstddef>
#include <iostream>

void process(int n)  { std::cout << "integer: " << n << '\n'; }
void process(int* p) { std::cout << "pointer: " << (p ? "non-null" : "null") << '\n'; }

int main()
{
    process(0);        // calls process(int)  β€” may surprise callers
    process(NULL);     // calls process(int)  β€” NULL is an integer constant
    process(nullptr);  // calls process(int*) β€” unambiguous, C++11
}

With 0 or NULL, the compiler resolves to process(int) because those are integer constants. nullptr has type std::nullptr_t, which does not convert to int, leaving only the pointer overload viable.

Templates and Type Deduction

nullptr retains its identity through template instantiation; 0 and NULL do not:

cpp
#include <cstddef>
#include <iostream>

template<class T>
constexpr T forwarded(const T& v) { return v; } // C++11

void sink(int*) { std::cout << "pointer\n"; }

int main()
{
    sink(forwarded(nullptr));  // OK: T = std::nullptr_t, converts to int*
 // sink(forwarded(0));        // error: T = int, not convertible to int*
 // sink(forwarded(NULL));     // error: same β€” NULL deduces as int
}

This distinction is critical in middleware and generic wrappers that forward null sentinels through multiple levels of deduction. nullptr is the only null constant that survives deduction intact.

Overloading on std::nullptr_t

When a type should accept only nullptr and reject arbitrary integers, overload its constructor or assignment operator on std::nullptr_t directly:

cpp
#include <cstddef>

class Handle {
public:
    explicit Handle(void* raw) noexcept : ptr_(raw) {}
    Handle(std::nullptr_t)    noexcept : ptr_(nullptr) {} // C++11: reset overload

    Handle& operator=(std::nullptr_t) noexcept {          // C++11
        ptr_ = nullptr;
        return *this;
    }

    explicit operator bool() const noexcept { return ptr_ != nullptr; }

private:
    void* ptr_;
};

Handle h{nullptr};  // uses Handle(std::nullptr_t)
h = nullptr;        // uses operator=(std::nullptr_t)
// Handle bad{0};   // error: no conversion from int to Handle

This pattern is used throughout the standard library in std::unique_ptr, std::shared_ptr, and std::function to provide clean reset semantics without accidentally accepting 0.

auto and nullptr

auto deduces std::nullptr_t, not a pointer type:

cpp
auto p = nullptr;   // p is std::nullptr_t β€” NOT void*, NOT int*
// *p;              // error: cannot dereference std::nullptr_t

int* ip = nullptr;  // explicit type: fine
auto q = static_cast<int*>(nullptr); // C++11: force pointer type via cast

When you need a typed null pointer via auto, either declare the type explicitly or cast. This deduction behaviour can surface as an error far from the declaration site, so explicit pointer types are preferable in these situations.

Equality and Comparison

nullptr compares equal to any null pointer value of any pointer type:

cpp
#include <cstddef>

int* p  = nullptr;
void* q = nullptr;

bool a = (p == nullptr);   // true
bool b = (q == nullptr);   // true
// bool c = (p == q);      // error: cannot compare int* and void* directly

std::nullptr_t n1 = nullptr;
std::nullptr_t n2 = nullptr;
bool d = (n1 == n2);       // true: all std::nullptr_t values compare equal
bool e = (n1 < n2);        // false: relational operators are also defined (C++11)

Best Practices

Always use nullptr instead of NULL or 0 in pointer contexts. The type distinction is enforced by the compiler and prevents silent resolution to integer overloads.

Overload on std::nullptr_t when a class has a distinct "reset to null" operation. This makes the API explicit, prevents accidental construction from the integer 0, and documents intent at the call site.

Do not use auto to capture nullptr when you need a pointer. Declare the concrete pointer type or assign to a typed pointer first. The std::nullptr_t type has no dereference, arithmetic, or indexing operations.

In generic code, always pass nullptr rather than 0 through deduction contexts. A 0 that enters template<class T>(T v) becomes int and loses all pointer-convertibility. nullptr never does.

Prefer std::nullptr_t parameters over pointer parameters for factory/reset functions when the semantic meaning is strictly "set to null," not "accept any pointer." It prevents callers from accidentally passing live pointers.

Common Pitfalls

sizeof(std::nullptr_t) Is Not Guaranteed to Match sizeof(void*)

std::nullptr_t is an independent type. Its size is implementation-defined and need not match any pointer size:

cpp
// May or may not hold on a given platform:
static_assert(sizeof(std::nullptr_t) == sizeof(void*));  // NOT guaranteed

Do not use std::nullptr_t as a proxy for pointer size in low-level code.

Boolean Conversion Is Always false

A value of type std::nullptr_t is always null by definition. Boolean conversion always yields false, and compilers typically warn on if (nullptr):

cpp
std::nullptr_t n = nullptr;
if (n) { /* unreachable */ }  // compiler warning; condition is always false

The warning is correct. There is no way to construct a non-null std::nullptr_t.

nullptr converts to pointer-to-member types, which is syntactically valid but undefined behaviour if dereferenced:

cpp
struct Foo { int x; };
int Foo::* mp = nullptr;  // legal: null pointer-to-data-member

Foo obj;
// obj.*mp;               // undefined behaviour: dereferencing null pointer-to-member

The type system does not prevent this. The hazard is identical to dereferencing a null object pointer but occurs in a context developers may not expect to be dangerous.

nullptr Cannot Be Passed to a Variadic Function Correctly

Because variadic functions (printf, va_list-based APIs) do not have typed parameters, nullptr is passed as std::nullptr_t, not as void*. On platforms where the two have different representations or sizes this causes undefined behaviour:

cpp
printf("%p\n", nullptr);              // undefined behaviour
printf("%p\n", (void*)nullptr);       // correct: explicit cast to void*

Always cast nullptr to the expected pointer type before passing it to C-style variadic interfaces.

std::nullptr_t as a First-Class Type

std::nullptr_t is the type of the nullptr expression. It is a distinct, complete type β€” not a pointer type, not an integer type, and not an alias for void*. The standard defines it in <cstddef> as:

cpp
namespace std {
    using nullptr_t = decltype(nullptr);
}

Because std::nullptr_t is its own type, it participates in overload sets independently of any pointer type. This is the feature that makes nullptr genuinely useful rather than merely cosmetic. Consider a handle class that must distinguish "construct from a raw pointer" from "reset to null":

cpp
#include <cstddef>

class Socket {
public:
    explicit Socket(int fd) noexcept : fd_(fd) {}

    // Dedicated null-reset overload β€” accepts only nullptr, not 0 or any int
    Socket(std::nullptr_t) noexcept : fd_(-1) {}

    Socket& operator=(std::nullptr_t) noexcept {
        close_if_open();
        fd_ = -1;
        return *this;
    }

    explicit operator bool() const noexcept { return fd_ >= 0; }

private:
    int fd_;
    void close_if_open();
};

Socket s{nullptr};  // uses Socket(std::nullptr_t)
s = nullptr;        // uses operator=(std::nullptr_t)
// Socket bad{0};   // error: no viable constructor β€” prevents silent misuse

Without std::nullptr_t, the pre-C++11 pattern was to add Socket(int) and document "pass 0 to reset," which allowed any integer through and made the API semantics unclear. The std::nullptr_t overload closes that hole exactly.

The Old NULL Ambiguity

The historical problem is that NULL was typically defined as the integer constant 0:

cpp
// Common pre-C++11 implementations:
#define NULL 0           // integer β€” compiles as int
#define NULL 0L          // long integer
#define NULL (void*)0    // C99 style β€” illegal in C++ (no implicit void* β†’ T* conversion)

C++ could not adopt the C99 form ((void*)0) because C++ requires an explicit cast from void* to any typed pointer, making int* p = (void*)0; ill-formed. So C++ compilers defined NULL as the integer 0, which then created overload resolution problems:

cpp
void f(int);
void f(char*);

f(NULL);    // calls f(int) in most compilers β€” almost never what the developer intended
f(nullptr); // calls f(char*) β€” correct and unambiguous

The only legal resolution for NULL was f(int) because NULL is an integer constant expression with value zero, which converts to int but also to char*. Compilers picked int because it was an exact match. With nullptr, f(int) is removed from consideration (no conversion from std::nullptr_t to int exists), leaving f(char*) as the sole viable overload.

Overload Resolution Rules for nullptr

Why nullptr Does Not Convert to int

The C++ standard explicitly excludes arithmetic types from the implicit conversion sequence of std::nullptr_t. The conversion rules are:

  • std::nullptr_t converts implicitly to any pointer type (T* for any T)
  • std::nullptr_t converts implicitly to any pointer-to-member type (T C::*)
  • std::nullptr_t converts implicitly to bool (always false)
  • std::nullptr_t does not convert to any integer type β€” not int, not long, not std::uintptr_t

This means the following is a hard compile error, not a surprise at runtime:

cpp
int n = nullptr;              // error: cannot convert std::nullptr_t to int
long l = nullptr;             // error
std::uintptr_t u = nullptr;   // error β€” even though pointers fit in uintptr_t

The f(int*) / f(char*) Ambiguity

A common misconception is that nullptr always resolves pointer ambiguities. It does not when two pointer overloads are both viable:

cpp
void g(int*);
void g(char*);

g(nullptr);  // error: ambiguous β€” nullptr converts equally well to int* and char*

Both g(int*) and g(char*) accept std::nullptr_t through the same kind of implicit conversion (null pointer conversion). Neither is a better match than the other. The fix is to add a std::nullptr_t overload:

cpp
void g(int*)          { /* ... */ }
void g(char*)         { /* ... */ }
void g(std::nullptr_t){ /* ... */ }  // catches nullptr explicitly

g(nullptr);  // unambiguous: calls g(std::nullptr_t) β€” exact match beats conversion

The std::nullptr_t overload is selected because it is an exact match (no conversion needed), while both pointer overloads would require a null pointer conversion, which ranks lower in the overload resolution hierarchy than an exact match.

nullptr_t Conversion to bool

std::nullptr_t does implicitly convert to bool, always yielding false. This is why if (ptr) works for any pointer type β€” the pointer is compared to the null pointer, and nullptr is the canonical null. However, this conversion is contextual (used in boolean contexts like if, while, !) and ranks as a standard conversion in overload resolution. A bool overload is therefore reachable from nullptr:

cpp
void h(bool);
void h(int*);

h(nullptr);   // calls h(int*) β€” pointer conversion is preferred over bool conversion
              // when both are standard conversions and pointer is more specific

In the absence of a pointer overload, h(bool) would be called with nullptr yielding false.

nullptr in Templates and Type Traits

Deduction and std::is_null_pointer

In a deduced context, nullptr deduces as std::nullptr_t, not as any pointer type. This is the fundamental distinction from 0 and NULL:

cpp
#include <type_traits>

template<class T>
void inspect(T) {
    static_assert(std::is_null_pointer_v<T>,   "not nullptr_t");  // C++14
    static_assert(std::is_same_v<T, std::nullptr_t>, "same check");
}

inspect(nullptr);  // T = std::nullptr_t β€” both assertions pass
inspect(0);        // T = int β€” first assertion fails at compile time

std::is_null_pointer&lt;T&gt; (C++14, &lt;type_traits&gt;) is true if and only if T is std::nullptr_t or const std::nullptr_t or volatile std::nullptr_t (or cv-qualified combinations). It is false for every pointer type, every integer type, and void*.

SFINAE and Concepts with nullptr_t

You can gate a function template exclusively on callers passing nullptr using either SFINAE or a C++20 concept:

cpp
#include <cstddef>
#include <type_traits>

// C++11/14/17 SFINAE approach
template<class T,
         std::enable_if_t<std::is_null_pointer_v<T>, int> = 0>
void reset(T) noexcept { /* only callable with nullptr */ }

// C++20 concept approach β€” cleaner and produces better diagnostics
template<class T>
concept NullLiteral = std::is_null_pointer_v<T>;

void reset(NullLiteral auto) noexcept { /* same constraint */ }

reset(nullptr);  // OK
// reset(0);     // error: T=int does not satisfy NullLiteral
// reset(ptr);   // error: T=int* does not satisfy NullLiteral

if constexpr Dispatch on nullptr_t

When writing generic wrappers that must handle nullable arguments differently from typed arguments, if constexpr on std::is_same_v&lt;T, std::nullptr_t&gt; provides compile-time branching without runtime cost:

cpp
#include <cstddef>
#include <type_traits>
#include <iostream>

template<class T>
void store(T value) {
    if constexpr (std::is_same_v<T, std::nullptr_t>) {
        // Caller explicitly passed nullptr β€” clear the slot
        std::cout << "clearing\n";
    } else if constexpr (std::is_pointer_v<T>) {
        // Non-null pointer β€” store it
        std::cout << "storing pointer " << (void*)value << '\n';
    } else {
        static_assert(sizeof(T) == 0, "unsupported argument type");
    }
}

int x = 42;
store(&x);      // "storing pointer 0x..."
store(nullptr); // "clearing"
store(0);       // static_assert fires: int is not a pointer or nullptr_t

This pattern is common in type-erased containers, optional-like wrappers, and signal/slot systems that need to distinguish "reset" from "assign."

nullptr in constexpr and consteval Contexts

nullptr is a constant expression. Pointers and pointer-to-member values may appear in constexpr expressions as long as they are null β€” the standard explicitly permits comparing a pointer to nullptr at compile time, even when the pointer's target is not a constant expression.

cpp
#include <cstddef>

struct Node {
    int value;
    Node* next;
};

constexpr bool is_empty(const Node* head) noexcept {
    return head == nullptr;   // comparison to nullptr is constexpr-safe
}

static_assert(is_empty(nullptr));  // evaluated at compile time β€” passes

This property enables compile-time linked-list and tree traversal in constexpr functions (C++14 and later, where constexpr functions may contain loops and local variables):

cpp
#include <cstddef>

struct Node {
    int   value;
    const Node* next;
};

constexpr int sum(const Node* head) noexcept {
    int total = 0;
    while (head != nullptr) {   // nullptr comparison β€” constexpr-legal
        total += head->value;
        head = head->next;
    }
    return total;
}

inline constexpr Node c{3, nullptr};
inline constexpr Node b{2, &c};
inline constexpr Node a{1, &b};

static_assert(sum(&a) == 6);  // evaluated entirely at compile time

consteval functions (C++20) impose the same rules: the comparison ptr == nullptr is legal at constant evaluation time provided ptr itself was obtained through constant evaluation (e.g., is itself nullptr or points to a constexpr object).

Historical Deep Dive: Why NULL Was Always a Hack

The C Legacy

In C, NULL is defined in <stddef.h> and several other headers. The C standard permits any of these definitions:

c
/* All of these are valid C implementations of NULL */
#define NULL 0
#define NULL 0L
#define NULL ((void*)0)   /* most common in C99/C11 */

The (void*)0 form is idiomatic in C because C allows implicit conversion from void* to any pointer type. It made NULL carry pointer semantics in assignments and comparisons.

Why C++ Rejected (void*)0

C++ removed the C implicit void* β†’ T* conversion to strengthen the type system. Code like int* p = malloc(sizeof(int)); is valid C but ill-formed C++ β€” it requires an explicit cast. As a direct consequence, if NULL were defined as (void*)0 in a C++ translation unit, every use of NULL in a pointer initializer would require a cast, making it useless as a convenience macro. C++ implementations therefore defined NULL as the integer 0 (or 0L), reverting to the pre-C99 situation.

The Variadic Trap

The integer definition of NULL has a particularly nasty interaction with C-style variadic functions. Consider printf with the %p conversion specifier, which expects a void*:

cpp
// Undefined behaviour in C++ (and in C if sizeof(int) != sizeof(void*)):
printf("addr: %p\n", NULL);      // NULL is int 0, not void* β€” UB on LP64 systems

// Correct β€” explicit cast to the expected type:
printf("addr: %p\n", (void*)nullptr);
printf("addr: %p\n", static_cast<void*>(some_ptr));

On LP64 platforms (Linux x86-64, most 64-bit POSIX systems), int is 32 bits and void* is 64 bits. The variadic ... parameter passes NULL as a 32-bit integer pushed onto the argument area, but printf reads 64 bits expecting a pointer. The upper 32 bits are whatever happened to be in memory β€” typically zero on most call conventions, which masks the bug in practice, but remains undefined behaviour.

nullptr does not fix this on its own β€” printf("%p\n", nullptr) is still undefined behaviour because nullptr is passed as std::nullptr_t, not as void*. The rule is: always cast to the concrete pointer type before passing any null to a variadic C function.

The NULL in Variadic Templates Pitfall

The same issue surfaces in C++ variadic templates that eventually forward to C APIs:

cpp
#include <cstdio>
#include <cstddef>

template<class... Args>
void log(const char* fmt, Args&&... args) {
    std::printf(fmt, std::forward<Args>(args)...);
}

log("ptr=%p\n", NULL);     // Args = {int} β€” UB: printf gets int, expects void*
log("ptr=%p\n", nullptr);  // Args = {std::nullptr_t} β€” still UB: not void*
log("ptr=%p\n", (void*)nullptr); // correct

Neither NULL nor nullptr is safe here without the explicit void* cast. The only correct approach is to cast at the call site.

See Also

  • std::nullptr_t β€” the type of nullptr, declared in <cstddef> (C++11); usable as a template parameter and function parameter type
  • NULL β€” the legacy null pointer macro from <cstddef> and related headers; prefer nullptr in all new C++11 and later code
  • auto β€” type deduction; auto x = nullptr deduces std::nullptr_t, not any pointer type