Understanding std::indirect in C++26: Why Do We Need Another Pointer Wrapper?
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_ptris move-only.std::shared_ptrperforms 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 theTobject 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.