C++ programming language logo, white text 'C++' in shaded blue hexagon
blog

C++: Avoiding Argument Dependent Lookup

A little trick using an extra namespace and cross-import.
4 min read /

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.