C++ has a lot of features that seem harmful until you consider why they exist, and ADL is one of them. ADL stands for argument-dependent lookup, which means that the lookup of a name depends on the arguments used when calling it. To illustrate this, let’s look at a simple example:
#include <iostream>
#include <iterator>
#include <vector>
int main(void) {
std::vector<int> vec{1, 2};
auto start = vec.begin();
auto end = vec.end();
// distance() is not defined, how does this compile!?
std::cout << distance(start, end);
// similarly, endl() is not defined here and we didn't use std::!
std::cout);
endl( }
This code compiles and runs and prints 2
just like you’d expect! How is this possible? We didn’t use std::distance
or std::endl
, and yet the compiler knew which functions we meant to call. This is what ADL does.
Given a call to an unqualified function a
in the form a(...)
, the compiler can deduce which function named a
to call based on the arguments given to it. Here, distance(start, end)
is a call to some function distance
with types std::vector<int>::iterator
. Since these are in the std
namespace, the compiler can deduce that std::distance
is the function you intended to call, as it’s allowed to look in the std
namespace to find the distance
function. The same goes for endl
. This can have a negative impact on your code. Consider the following: if you had a function called distance
in a template function, you might expect it to work with any type passed into it, including std::vector<int>::iterator
. However, std::distance
may be chosen first. As a result, you would have to fully qualify your call to distance
, e.g., with the namespace it’s in, or ::distance
if it’s a global function. Since this can mess with your code, why is this allowed? You might immediately think this makes no sense.
Let’s look at a different example. Given the following code:
int operator+(const MyType&, int);
This declaration tells us immediately that there’s an operator overload for +
which works on MyType
and int
. When the compiler encounters some code that says my_thing + 5
, where my_thing
is an instance of MyType
, how does it know what to do with that? This is where ADL comes in.
When you write my_thing + 5
, the compiler really interprets that as a call to a function operator+(my_thing, 5)
. However, you didn’t give it a namespace to find that operator+
in, so how does it know what to call? This is an example of why ADL exists. Based on the types of the arguments and their namespaces, the compiler is able to find our overloaded operator+
and successfully call it.
While this is one reason ADL exists, it is not the only one. ADL also allows for specialization of functions for user-defined types. For example, in many cases, the STL actually calls swap(a, b)
instead of std::swap(a, b)
. Since it uses an unqualified name for swap
, this will use ADL to lookup the swap function to use. As a result, you can overload swap
for your own types. Many libraries do this, too! For example, fmt
uses ADL to find a function called format_to
for your type to allow it to format your types. nlohmann::json
uses ADL to find your user-defined to_json
and from_json
functions. This means you don’t have to explicitly tell these libraries which functions to call. Neat, huh?
Let’s look at a complete example of using ADL in your own code. This code may look like it won’t compile, but it actually does!
namespace nature {
template <class Animal>
Animal Breed(Animal&& mother, Animal&& father) {// Notice that Reproduce for any particular type is not defined here!
// (I will forego forwarding here for brevity.)
return Reproduce(mother, father);
}
}
namespace pets {
struct Cat {};
const Cat& mother, const Cat& father) {
Cat Reproduce(std::cout << "making a new cat!\n";
return Cat{};
}
}
int main(void) {
pets::Cat mother;
pets::Cat father;// This compiles!
auto child = nature::Breed(mother, father);
}
Here, despite pets::Reproduce(const Cat&, const Cat&)
not being declared nor defined within the scope of Breed()
, it is still valid to call this function on two cats. This is because of ADL. When the compiler sees that Animal
is pets::Cat
, it can look in pets
to resolve the name Reproduce
, since its arguments are from the pets
namespace.
Hopefully, you’ve learned something about ADL here. If you are a C++ veteran, I should imagine you were bored to death, and I’d be surprised if you got to this point. However, if you’re a newer C++ user, I hope you found this helpful!