How std::to_address Works in Modern C++
In modern C++, std::to_address provides a standard,
robust mechanism for extracting a raw pointer from any pointer-like
type, such as smart pointers, iterators, or custom fancy pointers,
without triggering undefined behavior. Introduced in C++20 in the
<memory> header, it generalizes address extraction
where simply dereferencing an object via &*p might lead
to invalid memory access. This article explains the underlying mechanism
std::to_address uses to safely resolve fancy pointers to
raw addresses, the specific fallback rules it applies, and why it is
indispensable when writing generic code and custom allocators.
The Problem with Dereferencing
Prior to C++20, obtaining a raw pointer from an iterator or a
pointer-like object typically involved expression idioms like
&*p or std::addressof(*p). While this
works for active elements, it causes undefined behavior when
p is an end iterator (such as vec.end()) or a
singular pointer that cannot legally be dereferenced.
std::to_address solves this by obtaining the underlying raw
memory address without evaluating *p.
The Resolution Mechanism
The behavior of std::to_address is defined through a
specific sequence of compile-time checks:
- Raw Pointers: If the passed argument is already a
raw object pointer of type
T*,std::to_addresssimply returns it. It is conditionally ill-formed if the pointer is a function pointer. std::pointer_traitsSpecialization: If the type is not a raw pointer,std::to_addresschecks ifstd::pointer_traits<Ptr>::to_address(p)is well-formed. If a custom specialization ofstd::pointer_traitsprovides a staticto_addressmember function, that implementation is invoked.- Fallback to
operator->(): Ifpointer_traitsdoes not define ato_addressfunction, the utility falls back to callingp.operator->(). If the result ofoperator->()is itself another pointer-like type rather than a raw pointer,std::to_addressrecursively unwraps it until a raw pointer is attained.
Code Implementation Example
The following example demonstrates how std::to_address
handles raw pointers, smart pointers, and empty container iterators
safely:
#include <iostream>
#include <memory>
#include <vector>
int main() {
int val = 42;
int* raw_ptr = &val;
auto smart_ptr = std::make_unique<int>(100);
std::vector<int> vec{1, 2, 3};
// 1. Raw pointer extraction (returns directly)
int* p1 = std::to_address(raw_ptr);
// 2. Smart pointer extraction (uses operator->)
int* p2 = std::to_address(smart_ptr);
// 3. Past-the-end iterator (safe, avoids dereferencing vec.end())
int* p3 = std::to_address(vec.end());
std::cout << "Raw pointer value: " << *p1 << '\n';
std::cout << "Smart pointer value: " << *p2 << '\n';
std::cout << "Iterator points to address: " << static_cast<void*>(p3) << '\n';
return 0;
}Custom Pointer Types
When implementing fancy pointers—such as offset pointers for shared
memory or custom allocator storage—you can enable direct support for
std::to_address by specializing
std::pointer_traits:
template <typename T>
struct CustomPtr {
T* raw;
T& operator*() const { return *raw; }
T* operator->() const { return raw; }
};
template <typename T>
struct std::pointer_traits<CustomPtr<T>> {
using pointer = CustomPtr<T>;
using element_type = T;
using difference_type = std::ptrdiff_t;
static element_type* to_address(pointer p) noexcept {
return p.raw;
}
};By prioritizing pointer_traits<Ptr>::to_address,
the standard allows complex structures to map their internal
representation directly to a real hardware memory address without
relying on an overloaded arrow operator.
Usage in Generic Code
The primary consumer of std::to_address is the C++
standard library itself, particularly within the implementation of
contiguous ranges (std::ranges::contiguous_range) and
algorithms that can be optimized using low-level routines such as
std::memcpy or SIMD vectorization. Whenever a generic
algorithm receives a contiguous iterator, it utilizes
std::to_address to access the contiguous memory block
directly, bridging modern high-level abstractions with zero-cost
bare-metal operations.