C++: Avoiding Argument Dependent Lookup
While refactoring a substantial codebase in C++, I had an issue of ambiguous function calls as a result of argument-dependent lookup (ADL, also known as Koenig lookup). The following minimal example demonstrates:
namespace a {
struct X {};
void f(const X& x) {}
}
namespace b {
void f(const a::X& x) {}
void g() {
a::X x;
f(x);
}
}
When compiling, gcc
gives:
$ g++ -c -o test.o test.cpp
test.cpp: In function ‘void b::g()’:
test.cpp:10:6: error: call of overloaded ‘f(a::X&)’ is ambiguous
10 | f(x);
| ~^~~
test.cpp:7:8: note: candidate: ‘void b::f(const a::X&)’
7 | void f(const a::X& x) {}
| ^
test.cpp:3:8: note: candidate: ‘void a::f(const a::X&)’
3 | void f(const X& x) {}
| ^
We might naively expect the function b::f
to be preferred over the function a::f
for a call within the b
namespace, or even for a::f
to be invisible in such a setting—there is no using namespace a
after all!—but argument-dependent lookup applies here: the namespaces of the types of arguments are also included during function lookup. Because the call is made from namespace b
, that namespace is considered, but because the argument type X
is from namespace a
, that namespace is also considered.
This is an essential feature of C++, and enables code like this outside the namespace std
:
std::cout << "Hello, world!" << std::endl;
Because the arguments std::cout
and std::endl
are inside the namespace std
, that namespace is considered wherever such code appears, so that the operator std::operator<<
can be found.
In my particular case, from within the b
namespace, I wanted the function b::f
to be preferred over a::f
. One way would be to explicitly specify b::f(...)
on all function calls, but this would apply to hundreds of locations in this particular codebase, and make for messy client code. Instead, a solution was to move the culprit type to a new namespace, then cross-import between the new and original namespaces. Using the minimal example, it works like so:
/* move X to its own namespace */
namespace c {
struct X {};
}
/* cross-import these namespaces */
namespace a {
using namespace c;
}
namespace c {
using namespace a;
}
/* the rest of the code as before */
namespace a {
void f(const X& x) {}
}
namespace b {
void f(const a::X& x) {}
void g() {
a::X x;
f(x);
}
}
Generally, culprit types go in the new namespace c
, functions remain in the original namespace a
. The above compiles, but we can pad out the code to a standalone program to confirm that the behavior is as intended:
#include <iostream>
/* move X to its own namespace */
namespace c {
struct X {};
}
/* cross-import these namespaces */
namespace a {
using namespace c;
}
namespace c {
using namespace a;
}
/* the rest of the code as before */
namespace a {
void f(const X& x) {
std::cout << "running a::f" << std::endl;
}
}
namespace b {
void f(const a::X& x) {
std::cout << "running b::f" << std::endl;
}
void g() {
a::X x;
f(x);
}
}
int main() {
b::g();
return 0;
}
Compiling and running gives:
$ g++ -o test test.cpp
$ ./test
running b::f
There is no longer a compiler error for an ambiguous function call, and the preferred function b::f
has been chosen.
The convenience of this approach is that there is no need to update existing usage of a::X
throughout the original codebase: while X
is now declared in the new namespace c
, it is also imported back into the original namespace a
, so can still be referred to as a::X
. I am not aware of any unintended side effects, except that compiler error messages may refer to c::X
after the change, rather than to a::X
as before.