Skip to content
C++
Language
since C++98
Advanced

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++98

Unary 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::addressof unconditionally, since they can be instantiated with a type that overloads operator&.
  • 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

cpp
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**:

cpp
#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:

cpp
#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:

cpp
#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:

cpp
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:

cpp
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() or reset_and_get_address() method.
  • Pointer wrappers: use std::pointer_traits customization.
  • 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:

cpp
// 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:

cpp
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:

  1. reinterpret_cast<const volatile char&>(r) β€” reinterprets r as a char reference. The standard guarantees that char, unsigned char, and std::byte may alias any object (they are the aliasing exception in the strict aliasing rule). This cast does not invoke operator& because it acts on the underlying storage representation.

  2. &const_cast<char&>(...) β€” applies built-in operator& to a char reference. Because the operand's type is char, not T, no overloaded operator& for T is a candidate. The built-in address-of is selected unconditionally, yielding a char* pointing at r's first byte.

  3. reinterpret_cast<T*>(...) β€” casts the char* back to T*. This is well-defined because the char* was derived from an actual T object 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.

cpp
// 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 constexpr

std::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:

cpp
// 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:

cpp
#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:

cpp
// 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:

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

cpp
// 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 expr

Within 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::MatrixXd with 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 &expr yields 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:

cpp
#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 specialisation

How std::to_address resolves

The lookup algorithm used by std::to_address(p) is:

  1. If p is a raw pointer type, return p directly (this branch is constexpr).
  2. Otherwise, if std::pointer_traits<Ptr>::to_address(p) is well-formed, call it and return the result.
  3. Otherwise, return std::to_address(p.operator->()) β€” recursively unwrap via operator->.

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)
Inputlvalue referencepointer-like object
Bypassesoverloaded operator&variety of pointer representations
SinceC++11 (constexpr C++17)C++20
Use in allocatorsconstruct/destroy of held elementconvert 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; constexpr since C++17
  • std::to_address β€” <memory>, C++20; retrieves a raw pointer from any pointer-like type
  • std::pointer_traits β€” <memory>, C++11; customisation point for allocator-aware pointer types
  • std::allocator_traits::construct β€” mandated to use std::addressof internally since C++11