With raw pointers, smart pointers like std::unique_ptr and std::shared_ptr, references, and utilities like std::reference_wrapper, it is fair to wonder why C++26 introduces yet another indirection mechanism: std::indirect<T>.

The short answer: std::indirect is not a pointer. It is a value type implemented via heap allocation.

To understand why this is a game-changer for modern C++ development, we need to explore vocabulary types, value semantics, and recursive data structures.

The Core Difference: Pointer Semantics vs. Value Semantics

Existing pointer utilities model reference semantics. Even a std::unique_ptr<T>, which possesses exclusive ownership over its dynamically allocated resource, behaves like a pointer:

  • Equality: Comparing two smart pointers compares the memory addresses they hold, not the values of the objects they point to.
  • Copying: std::unique_ptr is move-only. std::shared_ptr performs a shallow copy by incrementing the reference count; modifying the underlying object through one copy affects the other.
  • Const Propagation: A const std::unique_ptr<T> only prevents changing which address the pointer holds; it does not prevent modifying the T object itself (shallow constness).

In contrast, std::indirect<T> enforces deep value semantics while leveraging indirection under the hood.

Key Features of std::indirect

1. Deep Copying

Unlike std::unique_ptr, std::indirect<T> is copyable (assuming T is copy-constructible). Copying an indirect<T> dynamically allocates a distinct copy of the underlying T object.

#include <indirect> // Expected C++26 header
#include <string>
#include <cassert>

std::indirect<std::string> a{"Hello"};
std::indirect<std::string> b = a; // Deep copy occurs here!

*b = "World";

assert(*a == "Hello"); // 'a' remains unaffected
assert(*b == "World");

2. Deep Equality and Ordering

When you evaluate a == b or a <=> b on two std::indirect wrappers, the operation forwards directly to the wrapped values, not their internal addresses.

std::indirect<int> x{42};
std::indirect<int> y{42};

// True because *x == *y, even though their memory addresses differ
assert(x == y);

3. Deep Const-Correctness

With std::indirect, a const wrapper guarantees const access to the underlying object. Accessing *val through a const std::indirect<T> yields a const T&, avoiding accidental mutation.

The Real-World Problem: Recursive Data Structures

Prior to C++26, defining recursive data types such as trees, graphs, or AST (Abstract Syntax Tree) nodes with true value semantics required extensive boilerplate. Consider a binary tree node:

struct Node {
    int value;
    // std::unique_ptr makes Node move-only and breaks regular copy semantics
    std::unique_ptr<Node> left;
    std::unique_ptr<Node> right;
};

Because std::unique_ptr cannot be copied, Node automatically disables its copy constructor and copy assignment operator. To make Node copyable, you had to manually implement deep copy logic for both children, handle self-assignment, and implement custom comparison operators.

With std::indirect, the compiler-generated special member functions do the right thing out of the box:

struct Node {
    int value;
    // std::indirect gives regular value semantics automatically
    std::indirect<Node> left;
    std::indirect<Node> right;

    // Defaulted copy, move, and comparison operators work seamlessly
    auto operator<=>(const Node&) const = default;
};

Summary: Why std::indirect Matters

Think of std::indirect<T> as being conceptually equivalent to T itself, but with a stable heap address and an incomplete-type-friendly memory footprint. It fills an essential gap in modern C++ by bridging dynamic allocation with the intuitive guarantees of regular value types.