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.
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