How gsl::not_null Enforces Non-Null Pointers in C++

This article explains how the C++ Core Guidelines Support Library utility gsl::not_null eliminates null pointer dereferencing bugs. It covers the dual mechanism of static compile-time prevention—such as deleted default and nullptr constructors—and runtime contract verification during dynamic initialization, providing zero-overhead, self-documenting pointer safety.

What is gsl::not_null?

gsl::not_null<T> is a zero-overhead template wrapper designed for pointer types, including raw pointers (T*), standard smart pointers (std::unique_ptr, std::shared_ptr), and user-defined pointer-like objects. Its sole purpose is to extend the C++ type system by establishing an invariant: the encapsulated pointer can never hold a null value (nullptr).

Compile-Time Enforcement

gsl::not_null leverages C++ type constraints and constructor overloads to eliminate entire categories of null pointer operations during compilation:

  1. Deleted nullptr_t Constructor: The wrapper explicitly deletes the constructor taking std::nullptr_t:

    not_null(std::nullptr_t) = delete;
    not_null& operator=(std::nullptr_t) = delete;

    Attempting to initialize or assign a gsl::not_null object directly with nullptr causes an immediate compilation failure.

  2. No Default Constructor: Standard pointers default-initialize to an indeterminate state or nullptr. gsl::not_null deletes or omits the default constructor, requiring an explicit valid pointer object at the point of declaration.

  3. Explicit Interface Contracts: Functions defined with gsl::not_null<T*> parameters cannot accidentally accept raw pointers without an explicit conversion. This forces the caller to either guarantee the pointer is non-null or handle the validation at the boundary.

Dynamic Construction and Runtime Enforcement

When a pointer's value is determined dynamically at runtime—such as the result of a system call, memory allocation, or external function—compile-time checks alone cannot guarantee safety. gsl::not_null enforces its invariant through runtime contract checks during construction.

Upon receiving a raw or smart pointer:

template <typename U>
constexpr explicit not_null(U&& u) : ptr_(std::forward<U>(u)) {
    Expects(ptr_ != nullptr);
}

The constructor validates the incoming pointer using the GSL contract macro Expects(). If the provided argument evaluates to nullptr at runtime:

  • The program triggers a contract violation.
  • By default, this immediately terminates execution (via std::terminate), avoiding undefined behavior further down the call stack.
  • In customized environments, it can be configured to throw an exception or invoke a designated error handler.

Once construction succeeds, the invariant is locked. Any subsequent assignment to the wrapper also passes through these validation checks.

Access and Runtime Efficiency

Because gsl::not_null guarantees its invariant at construction, member access via operator* and operator-> executes without repetitive runtime null checks:

void process(gsl::not_null<int*> data) {
    // No "if (data == nullptr)" check needed; guaranteed valid
    *data = 42;
}

Modern compilers inline these operators completely. In optimized builds (-O2 or -O3), gsl::not_null<T*> generates the exact same assembly as a bare raw pointer, providing type-level safety with zero runtime performance cost after initialization.