Address-Of Overload
Overloading unary operator& on class types, its hazards in generic code, and how std::addressof bypasses any overload to retrieve the true address.
Address-Of Overloadsince C++98Unary operator& is an overloadable operator that, when defined on a class type, intercepts the address-of expression &obj and returns an arbitrary value in place of the object's actual memory address.
Overview
Almost every piece of C++ code assumes &obj yields a pointer to obj's storage. Overloading operator& violates that assumption: the expression still compiles, but it now invokes user-defined code and may return any type β including a type that is not a pointer at all.
This makes operator& one of the most hazardous overloadable operators. Generic infrastructure β containers, allocators, placement new, std::construct_at β must not assume that &obj yields the real address. The C++11 standard library response was std::addressof, which always retrieves the actual storage address regardless of any overload.
Understanding the topic falls into two practical areas:
- Authoring generic code: templates that touch object addresses must call
std::addressofunconditionally, since they can be instantiated with a type that overloadsoperator&. - Recognising and auditing legacy types: COM-style smart pointers and some proxy types overload
operator&to support out-parameter APIs, creating subtle hazards for callers.
Syntax
struct MyType {
SomePointerType operator&(); // non-const overload
ConstPointerType operator&() const; // const overload
};The overload may be a member function (most common) or a non-member operator&(MyType&). The return type has no constraint from the language β returning an integer or a struct is syntactically valid, though semantically broken in almost all conceivable designs.
Examples
COM-style out-parameter pattern
The historically common motivation for overloading operator& was COM interfaces, where output-parameter APIs require a T**:
#include <iostream>
#include <memory>
template<class T>
class ComPtr {
T* raw_ = nullptr;
public:
explicit ComPtr(T* p = nullptr) : raw_(p) {}
~ComPtr() { delete raw_; }
T* get() const { return raw_; }
T** operator&() { return &raw_; } // yields T** for QueryInterface-style APIs
};
void create_object(int** pp) { *pp = new int(99); }
int main() {
ComPtr<int> p;
create_object(&p); // operator& called; passes raw_'s address
std::cout << *p.get(); // 99
}The overload works in isolation, but breaks the moment a generic utility calls &p expecting a ComPtr<int>*.
std::addressof bypasses the overload
std::addressof<T> (C++11, <memory>) always returns the object's true address. It avoids involving operator& entirely by reinterpreting the object through char pointers at the implementation level. Compiler intrinsics power the real library implementations:
#include <iostream>
#include <memory>
template<class T>
struct Wrapper {
T value;
T** operator&() = delete; // address-of is unusable via &
};
int main() {
Wrapper<double> w{3.14};
// &w; // error: deleted operator&
Wrapper<double>* p = std::addressof(w); // always works
std::cout << p->value << '\n'; // 3.14
}Since C++17, std::addressof is constexpr for object types (it was not constexpr in C++11/14).
Generic containers require std::addressof
Any template that constructs, destroys, or relocates objects in raw storage must use std::addressof. This includes custom allocators, storage wrappers, and type-erased containers:
#include <memory>
#include <type_traits>
#include <utility>
template<class T>
class ManualStorage {
alignas(T) unsigned char buf_[sizeof(T)];
bool live_ = false;
public:
template<class... Args>
void construct(Args&&... args) {
::new (std::addressof(*reinterpret_cast<T*>(buf_))) // not &obj
T(std::forward<Args>(args)...);
live_ = true;
}
T& get() { return *reinterpret_cast<T*>(buf_); }
void destroy() {
if (live_) {
std::addressof(get())->~T(); // not get().~T()
live_ = false;
}
}
};Using &obj instead of std::addressof(obj) here silently misbehaves when T overloads operator&, passing the wrong address to placement new or the destructor.
Self-assignment guards
The canonical self-assignment check in copy/move assignment relies on address comparison. When T overloads operator&, this != &other may compare incompatible types or call the overload with no meaningful result. The correct form uses std::addressof:
MyType& operator=(const MyType& other) {
if (this != std::addressof(other)) {
// perform copy
}
return *this;
}The standard library's own implementations, such as std::optional::operator= in libc++, follow this pattern.
The ADL ill-formed edge case
There is a subtle scenario where the built-in & is ill-formed via argument-dependent lookup β even without any overload:
template<class T>
struct Holder { T t; };
struct Incomplete; // declared but not defined
int main() {
Holder<Holder<Incomplete>*> x{};
// &x; // error: ADL instantiates Holder<Incomplete>, which is incomplete
std::addressof(x); // OK β no ADL involved
}This corner case is rare in practice but confirms that std::addressof is the unconditionally safe choice in template code.
Best Practices
Use std::addressof unconditionally in templates. Any template parameter type could, in principle, overload operator&. Calling std::addressof costs nothing at runtime and eliminates the entire class of bugs.
Do not overload operator& on new types. Modern C++ provides better alternatives for every historical use-case:
- COM out-parameters: use a named
put()orreset_and_get_address()method. - Pointer wrappers: use
std::pointer_traitscustomization. - Checked/instrumented pointers: expose the inner pointer through
get().
Use std::to_address (C++20) for pointer-like types. If you are implementing a fancy pointer (custom iterator, allocator pointer), the correct extension point is std::pointer_traits<Ptr>::to_address or the ADL-friendly std::to_address:
// C++20 β works for raw pointers and fancy pointers alike
template<class Ptr>
auto as_raw(Ptr& p) {
return static_cast<void*>(std::to_address(p));
}std::to_address participates in std::allocator_traits and is the mechanism by which C++20 containers handle fancy pointers correctly.
Common Pitfalls
Returning non-pointer types. The compiler accepts int operator&() const { return 0; }. Code that calls &obj expecting a pointer will fail in bewildering ways: wrong overload resolution, implicit conversions, or outright compile errors deep inside a call chain.
Broken placement new. Passing the result of an overloaded & to placement new produces undefined behaviour if the returned value is not the object's actual address. This is difficult to diagnose because the bug only manifests at runtime, often as a write to unrelated memory.
constexpr limitations before C++17. std::addressof became constexpr for object types in C++17. Code that uses it in constexpr contexts and targets C++11/14 will fail to compile. There is no portable workaround short of compiler builtins (__builtin_addressof in GCC/Clang).
Interaction with [[no_unique_address]] (C++20). When a member is annotated [[no_unique_address]], its address relative to the containing object may be the same as another member. Code that relies on operator& for such members to provide distinguishing identity gets undefined results.
How std::addressof Works
The reinterpret_cast implementation
Before compiler intrinsics became universal, the canonical portable implementation of std::addressof was:
template<class T>
T* addressof(T& r) noexcept {
return reinterpret_cast<T*>(
&const_cast<char&>(
reinterpret_cast<const volatile char&>(r)));
}Each layer of this expression exists for a specific reason:
-
reinterpret_cast<const volatile char&>(r)β reinterpretsras acharreference. The standard guarantees thatchar,unsigned char, andstd::bytemay alias any object (they are the aliasing exception in the strict aliasing rule). This cast does not invokeoperator&because it acts on the underlying storage representation. -
&const_cast<char&>(...)β applies built-inoperator&to acharreference. Because the operand's type ischar, notT, no overloadedoperator&forTis a candidate. The built-in address-of is selected unconditionally, yielding achar*pointing atr's first byte. -
reinterpret_cast<T*>(...)β casts thechar*back toT*. This is well-defined because thechar*was derived from an actualTobject and points at its first byte.
The result is a T* that is numerically equal to the address of r's storage, obtained without ever naming T::operator& as a candidate.
Modern standard libraries (libstdc++, libc++, MSVC STL) replace this sequence with __builtin_addressof(r) when the compiler provides it, which is both simpler and makes constexpr support trivial. The reinterpret-cast chain is not usable in constant expressions, which is why constexpr std::addressof required compiler buy-in.
constexpr std::addressof (C++17)
Prior to C++17, std::addressof had no constexpr qualifier. The reinterpret_cast-based implementation inherently cannot appear in constant expressions, and the standard did not mandate compiler support.
C++17 added constexpr to std::addressof for object types (not function types, which retain the non-constexpr overload). All three major compilers implement this via __builtin_addressof, which the compiler evaluates at compile time as a special case.
Practical consequence: code targeting C++11 or C++14 that calls std::addressof in a constexpr function will fail to compile. The only portable pre-C++17 workaround is __builtin_addressof, which is GCC/Clang-specific.
// C++17 and later β constexpr context is fine
constexpr int x = 42;
constexpr const int* px = std::addressof(x); // OK in C++17+
// C++11/14 β the line above is ill-formed;
// std::addressof is not constexprstd::allocator_traits::construct and Why It Mandates addressof
The standard mandates that std::allocator_traits<A>::construct(a, p, args...) calls std::addressof (not &) when forwarding the pointer p to the allocator's construct member or to the fallback ::new (static_cast<void*>(p)) T(args...).
This requirement exists because any user-defined allocator's value type could overload operator&. Consider a pool allocator managing a type that uses operator& for reference counting or COM integration. If allocator_traits::construct used &val to determine the object's address, the resulting pointer would be wrong, and placement new would write into the wrong memory location β producing silent corruption.
The relevant wording is in [allocator.traits.members]: the default construct implementation is effectively:
// Simplified β actual standard wording references addressof explicitly
template<class T, class... Args>
static void construct(A& a, T* p, Args&&... args) {
::new (static_cast<void*>(p)) T(std::forward<Args>(args)...);
// p comes from allocator::allocate, but if it were derived from
// an overloaded &, only addressof would give the real address.
}When writing a custom allocator's construct override, the same rule applies: always use std::addressof to convert a value reference to its storage address before passing it to placement new.
Concrete impact on a custom allocator
The following allocator tracks construction and destruction counts for testing. Notice that every internal address computation uses std::addressof:
#include <memory>
#include <cstddef>
template<class T>
struct TrackingAllocator {
using value_type = T;
T* allocate(std::size_t n) {
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, std::size_t n) noexcept {
::operator delete(p);
}
template<class U, class... Args>
void construct(U* p, Args&&... args) {
// CORRECT: placement new receives the actual storage address.
// Using &(*p) here would invoke operator& on U if overloaded.
::new (static_cast<void*>(std::addressof(*p)))
U(std::forward<Args>(args)...);
++constructions;
}
template<class U>
void destroy(U* p) noexcept {
// CORRECT: destructor called on the real object.
std::addressof(*p)->~U();
++destructions;
}
inline static int constructions = 0;
inline static int destructions = 0;
};If the allocator held SomeComType objects β where operator& returns SomeComType** β using &(*p) rather than std::addressof(*p) inside construct would silently pass a SomeComType** where a void* is expected. The implicit conversion through void* is accepted by the compiler, resulting in placement new writing the object into whatever memory SomeComType** happens to point at. The bug produces no diagnostic.
Historical Examples: CComPtr and boost::spirit::hold_any
Microsoft ATL CComPtr
CComPtr<T> is the template smart pointer from Microsoft's Active Template Library, used throughout COM programming on Windows. It overloads operator& to return T** β a pointer to the internal raw pointer member:
// Simplified CComPtr<T>::operator&
T** operator&() throw() {
ATLASSERT(p == NULL); // asserts not already holding a pointer
return &p;
}The intent is to allow COM factory functions that take IUnknown** or IFoo** out-parameters to write directly into the CComPtr's internal storage:
CComPtr<IShellItem> item;
SHCreateItemFromParsingName(path, nullptr, IID_PPV_ARGS(&item));
// &item calls operator&, yielding IShellItem**This works for the specific COM-API use case, but is a trap for generic code. Any template instantiated with CComPtr that does &obj expecting CComPtr<T>* gets T** instead. This is precisely the class of bugs that motivated std::addressof's addition to C++11: standard containers and algorithms had no safe way to get the real address of a stored element when the stored type was a COM smart pointer.
The C++23 standard library responded with std::out_ptr and std::inout_ptr β dedicated adapters for the T** out-parameter pattern that do not require overloading operator&.
boost::spirit::hold_any
Boost.Spirit's hold_any (an any-type similar to std::any) historically overloaded operator& to support stream insertion and extraction syntax in Spirit's grammar expressions. In that DSL context, &x meant "the address of x's held value" in a Spirit-grammar sense, not a memory address.
This created a conflict when Boost internals or user code tried to use hold_any with standard containers: the containers' internal machinery would call &element expecting a hold_any*, but received whatever the grammar expression node returned instead. The fix across the Boost ecosystem was to retrofit uses of &element with boost::addressof (Boost's pre-C++11 equivalent of std::addressof, with the same reinterpret-cast implementation).
Both cases β ATL's CComPtr and Spirit's hold_any β are documented in the original proposal paper for std::addressof (N2600 / N2723) as the motivating real-world examples that justified adding the utility to the standard.
Legitimate Modern Use: Expression Template Proxy Types
Despite the general advice to avoid overloading operator&, there is one well-established modern use case where it remains appropriate: expression template libraries.
Libraries like Eigen, Blaze, and SIMD wrappers define proxy types that represent lazy computations. An expression such as A + B does not compute a matrix sum; it creates a lightweight object describing the operation. In this context, taking the address of an expression node should yield a reference node in the expression tree, not a memory address β because the proxy itself has no meaningful persistent storage address to expose.
// Conceptual sketch β how an expression template might use operator&
namespace expr {
template<class Derived>
struct ExprBase {
// operator& yields a "reference" node wrapping this expression
AddressNode<Derived> operator&() const {
return AddressNode<Derived>{static_cast<const Derived&>(*this)};
}
};
template<class L, class R>
struct Add : ExprBase<Add<L, R>> {
L lhs; R rhs;
};
} // namespace exprWithin the DSL, &(A + B) produces a new expression node; it does not attempt to expose a memory address. This use is legitimate because:
- Code that uses
Eigen::MatrixXdwith standard library algorithms should pass matrices by value or through named variables, not by taking raw pointers to expression temporaries. - The library controls all the "generic" code that touches these objects, so there is no ambient template infrastructure that will incorrectly assume
&expryields a pointer. - The semantic contract of the library's DSL makes the overloaded meaning of
&consistent and documented.
The key distinction from the COM case is that expression template proxies are intentionally not stored in standard containers, and callers do not feed them to std::allocator_traits. When a type is designed exclusively for DSL expression composition, overloading operator& is a contained choice. When a type is a general-purpose value type intended for use with containers and algorithms, overloading operator& is a trap.
std::to_address (C++20): Extending Beyond Objects
std::to_address (C++20, <memory>) generalises the concept of "get the underlying raw pointer" to any pointer-like type β raw pointers, smart pointers, allocator fancy pointers, and custom iterator types.
Motivation
std::addressof obtains the address of an object held by an lvalue reference. But allocator-aware containers also work with fancy pointers β objects returned by allocator::allocate that behave like pointers but may not be raw T* (e.g., offset pointers for shared-memory allocators, or checked pointer types for debugging).
For a raw pointer T* p, you need p itself. For a fancy pointer Ptr p, you might need p.operator->() or a specialised std::pointer_traits<Ptr>::to_address(p). std::to_address wraps both cases uniformly:
#include <memory>
// Works for raw pointers
int arr[4] = {1, 2, 3, 4};
int* raw = std::to_address(arr); // same as arr
// Works for unique_ptr (has operator->)
auto up = std::make_unique<int>(42);
int* p = std::to_address(up); // same as up.get()
// Works for custom fancy pointers via pointer_traits specialisation
template<class T>
struct OffsetPtr {
std::ptrdiff_t offset;
T* get() const { return reinterpret_cast<T*>(
reinterpret_cast<char*>(this) + offset); }
};
template<class T>
struct std::pointer_traits<OffsetPtr<T>> {
static T* to_address(const OffsetPtr<T>& p) noexcept {
return p.get();
}
};
OffsetPtr<int> op{/*...*/};
int* resolved = std::to_address(op); // calls pointer_traits specialisationHow std::to_address resolves
The lookup algorithm used by std::to_address(p) is:
- If
pis a raw pointer type, returnpdirectly (this branch isconstexpr). - Otherwise, if
std::pointer_traits<Ptr>::to_address(p)is well-formed, call it and return the result. - Otherwise, return
std::to_address(p.operator->())β recursively unwrap viaoperator->.
Step 2 is the customisation point. A custom pointer type that is not dereferenceable (e.g., a sentinel or a pointer with deferred validity) should specialise std::pointer_traits rather than rely on step 3, which requires a functioning operator->. For any pointer-like type used as an allocator's pointer typedef, providing a pointer_traits specialisation with to_address is the correct integration path.
The recursive step 3 also means std::to_address works transparently on std::unique_ptr and std::shared_ptr without any specialisation, because both define operator-> returning a raw pointer.
Relationship to std::addressof
std::to_address and std::addressof solve different problems:
std::addressof(ref) | std::to_address(ptr) | |
|---|---|---|
| Input | lvalue reference | pointer-like object |
| Bypasses | overloaded operator& | variety of pointer representations |
| Since | C++11 (constexpr C++17) | C++20 |
| Use in allocators | construct/destroy of held element | convert allocator's fancy pointer to raw T* |
In a fully allocator-aware container, both appear: std::to_address converts the allocator-returned fancy pointer into a T* for use as the placement new target; internally, std::construct_at (which wraps placement new) ensures the address is passed as void* without invoking operator& on the pointed-to type.
See Also
std::addressofβ<memory>, C++11;constexprsince C++17std::to_addressβ<memory>, C++20; retrieves a raw pointer from any pointer-like typestd::pointer_traitsβ<memory>, C++11; customisation point for allocator-aware pointer typesstd::allocator_traits::constructβ mandated to usestd::addressofinternally since C++11