ADL Customization
Use argument-dependent lookup to let generic algorithms discover type-specific overloads defined in a type's own namespace, enabling non-intrusive extensibility.
ADL Customizationsince C++98A technique where generic algorithms call unqualified free functions so that argument-dependent lookup finds type-specific overloads defined in the type's own namespace, enabling extension without modifying the algorithm or sharing a base class.
Overview
Argument-dependent lookup (ADL) extends unqualified function name resolution to include the namespaces of all argument types. The ADL customization idiom deliberately exploits this: a library algorithm calls a function without namespace qualification, allowing user code to inject custom behaviour purely by placing an overload in the right namespace.
This mechanism underpins much of the Standard Library's extensibility. std::swap, std::begin, std::end, and every algorithm that accepts a callable object have historically relied on ADL-based customization. The pattern scales from a single extension point to full protocol suites.
The defining advantage over virtual dispatch is that ADL customization is resolved entirely at compile time, has zero runtime cost, and requires no common base class or vtable.
Mechanism
When a call like swap(a, b) is written without a namespace qualifier, the compiler builds a candidate set from:
- Names visible through ordinary lookup at the call site, including any
usingdeclarations. - The namespaces of the types of every argument β including enclosing namespaces.
For ADL customization to work reliably in generic code, the conventional two-step pattern must be used:
template <typename T>
void generic_swap(T& x, T& y) {
using std::swap; // fallback: std::swap handles all types
swap(x, y); // unqualified call: ADL finds namespace-local swap for T if present
}Without using std::swap, the call fails for fundamental types whose namespace (::) has no swap. Without the unqualified call, ADL is bypassed entirely and the generic std::swap runs unconditionally, ignoring any user-provided optimisation.
Defining a Customization Point
Namespace-scope free function
Place the customization function in the same namespace as the type. No modification to the class internals is required:
namespace geometry {
struct Rect {
double x, y, w, h;
};
// Found by ADL whenever a Rect appears as an argument to an unqualified swap
void swap(Rect& a, Rect& b) noexcept { // C++98
using std::swap;
swap(a.x, b.x); swap(a.y, b.y);
swap(a.w, b.w); swap(a.h, b.h);
}
} // namespace geometryAny template that follows the two-step pattern and receives geometry::Rect arguments will find this overload via ADL.
Hidden friend
C++11 codebases increasingly prefer the hidden friend pattern: a friend function defined inside the class body. The function is injected into the enclosing namespace but is visible only through ADL β not through ordinary qualified or unqualified lookup. This reduces overload-set noise and lets the compiler reject ill-formed calls faster:
namespace io {
class Buffer {
public:
explicit Buffer(std::size_t cap);
// Hidden friends: injected into namespace io, but found only via ADL
friend void swap(Buffer& a, Buffer& b) noexcept { // C++11 friend-in-class
using std::swap;
swap(a.data_, b.data_);
swap(a.capacity_, b.capacity_);
swap(a.size_, b.size_);
}
friend std::ostream& operator<<(std::ostream& os, const Buffer& buf) {
return os.write(buf.data_, static_cast<std::streamsize>(buf.size_));
}
private:
char* data_ = nullptr;
std::size_t capacity_ = 0;
std::size_t size_ = 0;
};
} // namespace ioswap and operator<< live in namespace io but cannot be found by a qualified call like io::swap(a, b) β only by unqualified calls when an io::Buffer is one of the arguments.
Range Customization: begin and end
Making a user type work with range-based for and standard algorithms requires ADL-findable begin and end in the type's namespace. std::begin (C++11) uses ADL internally for non-array types:
namespace grid {
struct Grid {
double* data;
std::size_t rows, cols;
};
// ADL customization points for range protocol
double* begin(Grid& g) noexcept { return g.data; } // C++11
double* end(Grid& g) noexcept { return g.data + g.rows * g.cols; }
const double* begin(const Grid& g) noexcept { return g.data; }
const double* end(const Grid& g) noexcept { return g.data + g.rows * g.cols; }
} // namespace grid
void process(const grid::Grid& g) {
for (double v : g) // range-based for calls std::begin(g) β grid::begin(g) via ADL
use(v);
}C++20: Customization Point Objects
C++20's std::ranges replaces raw ADL calls with customization point objects (CPOs): inline constexpr function objects such as std::ranges::swap, std::ranges::begin, and std::ranges::end. CPOs still find ADL overloads but provide three additional guarantees:
- They are niebloids β not themselves subject to ADL, preventing infinite-recursion footguns where your
swapaccidentally calls itself. - They enforce concept constraints on the discovered overload at the point of call.
- They define well-specified fallback behaviour when no user overload exists.
#include <concepts> // C++20
#include <ranges> // C++20
namespace mylib {
struct Matrix {
std::vector<double> elems;
std::size_t rows, cols;
// ADL-found hidden friend; std::ranges::swap and std::ranges::iter_swap both discover it
friend void swap(Matrix& a, Matrix& b) noexcept { // C++11 hidden friend, C++20 CPO-compatible
using std::swap;
swap(a.elems, b.elems);
swap(a.rows, b.rows);
swap(a.cols, b.cols);
}
};
} // namespace mylib
void example(mylib::Matrix& x, mylib::Matrix& y) {
std::ranges::swap(x, y); // C++20: finds mylib::swap via ADL; concept-checked
}Note that C++20 algorithms operating on ranges use CPOs exclusively internally. If your code targets C++20 or later, prefer CPOs at call sites over raw two-step patterns, while still providing ADL-findable overloads in your type's namespace.
Best Practices
Place the customization function in the same namespace as the type. ADL searches the namespace of argument types and their enclosing namespaces. A function in the global namespace or an unrelated namespace will not be found.
Prefer hidden friends for swap and operators. Ordinary namespace-scope free functions are visible to all unqualified lookups, inflating overload sets globally. Hidden friends are discoverable only when the defining type appears as an argument β exactly the desired semantics.
Mark swap noexcept. Move constructors and move assignment operators are typically noexcept; swap is the primitive they rely on. A throwing swap breaks exception-safety guarantees in containers and algorithms. Add noexcept unconditionally unless your swap can genuinely throw.
Follow the two-step in generic code. Inside any template that calls an ADL customization point, always write the using std::X; X(a, b); pattern. Never qualify the call (std::swap(a, b)) β this silently bypasses every user-defined overload and is the most common source of unexpected performance regressions.
Adopt C++20 CPOs in new projects. std::ranges::swap, std::ranges::begin, and their siblings provide concept-checked, niebloid-safe alternatives. They still discover ADL overloads; adopting them at call sites costs nothing in terms of the overloads you define.
Common Pitfalls
Qualified call suppresses ADL. std::swap(a, b) inside a generic function always calls the standard version, silently ignoring every user-provided overload. This is a latent bug that only surfaces when a type's custom swap matters for performance or correctness.
Wrong namespace. Defining swap in the global namespace (::) for a type in mylib means ADL will not find it in template contexts because the type's associated namespace is mylib, not ::.
Parenthesised name disables ADL. (swap)(a, b) is not an unqualified function call; it is a call through a parenthesised expression. ADL does not apply. This is occasionally used deliberately to call only the in-scope or std version, but it is a common accidental mistake.
Overload-set contamination from non-hidden free functions. A namespace-scope overload of swap or operator== accepting common types like std::string can be found by ADL in completely unrelated contexts, causing hard-to-diagnose ambiguity. Restrict overloads to user-defined parameter types, or use hidden friends.
Template specialisation vs. overloading. You cannot partially specialise a function template; the idiomatic extension mechanism for std::swap and similar points is overloading in the type's namespace, not specialising std::swap. Specialising std::swap in namespace std is technically permitted (C++98) but unnecessary given ADL, and specialising anything else in namespace std is undefined behaviour.
See Also
- Hidden Friend Idiom β defining
friendfunctions inside the class body to restrict visibility to ADL contexts - Swap Idiom β the full two-step swap pattern and its interaction with move semantics
- CRTP β compile-time static polymorphism as an alternative to ADL-based dispatch
- Tag Dispatch β using type tags to steer overload resolution at compile time