Name Lookup in C++

How many times have you used “std::cout” until now? I bet the answer is “a lot of times”.

Did you ever think about how the “cout” ostream object is actually found by the compiler? I mean, of course it is in the “iostream” header file, but how does the compiler find it? Also, what if you have other C++ entity that is called “std” or “cout” in the file with the call to “std::cout”?

Well, I don’t know if you’ve thought about all these questions, but if you did and you searched for an answer, you probably encountered something called “name lookup“.

What is name lookup?

With the help of our friend cppreference, we can find a pretty simple description for what actually is “name lookup”:

Name lookup is the procedure by which a name, when encountered in a program, is associated with the declaration that introduced it.

Even simpler, when we use any C++ entity, the compiler will search for the declaration of that entity.

There are two types of lookup in C++: qualified name lookup and unqualified name lookup. The second one also includes something called argument dependent lookup for function names.

For example, to compile

std::cout << std::endl;

the compiler performs:

  • unqualified name lookup for the name std, which finds the declaration of namespace std in the header
  • qualified name lookup for the name cout, which finds a variable declaration in the namespace std
  • qualified name lookup for the name endl, which finds a function template declaration in the namespace std
  • both argument-dependent lookup for the name operator<< which finds multiple function template declarations in the namespace std and qualified name lookup for the name std::ostream::operator<< which finds multiple member function declarations in class std::ostream.

Qualified and unqualified name lookup

A qualified name is a name that appears on the right hand side of the scope resolution operator ::

A qualified name may refer to a

  • class member (including static and non-static functions, types, templates, etc)
  • namespace member (including another namespace)
  • enumerator

For an unqualified name, that is a name that does not appear to the right of a scope resolution operator ::, name lookup examines the scopes, until it finds at least one declaration of any kind, at which time the lookup stops and no further scopes are examined.

Unqualified name lookup of the name used to the left of the function-call operator (and, equivalently, operator in an expression) is called argument-dependent lookup.

#include <iostream>
 
int main()
{
    struct std {};
 
    std::cout << "fail\n"; // Error: unqualified lookup for 'std' finds the struct
    ::std::cout << "ok\n"; // OK: ::std finds the namespace std
}

Argument-dependent lookup

Argument-dependent lookup, also known as ADL, or Koenig lookup, is the set of rules for looking up the unqualified function names in function-call expressions, including implicit function calls to overloaded operators. These function names are looked up in the namespaces of their arguments in addition to the scopes and namespaces considered by the usual unqualified name lookup.

template <class T>
struct Container
{
    T val_;
};

template <class T>
void swap(Container<T>& lhs, Container<T>& rhs)
    noexcept(std::is_nothrow_swappable_v<T>)
{
    swap(lhs.val_, rhs.val_); // uses ADL
}

Container<std::string> c1, c2;
swap(c1, c2);

Example on godbolt with comments.

In the example above, we can call our “swap” implementation for Container objects with any type T from std namespace. However, if we try to use a Container object with a primitive type, i.e. int, you will see that it will not work.

std::swap should be used for int type, but it cannot find it using ADL because, int is not in the std namespace.

In this case, we can use using declarations to introduce namespace members into other namespaces and block scopes.

// struct Container definition

template <class T>
void swap(Container<T>& lhs, Container<T>& rhs)
    noexcept(std::is_nothrow_swappable_v<T>)
{
    using std::swap; // this also introduces std::swap into the current namespace and block scope
    swap(lhs.val_, rhs.val_);
}

// this will work now because of the "using" above
Container<int> c1, c2;
swap(c1, c2);

Full example on godbolt – I also added some custom type for T in the example.

Good practices

To prevent unexpected lookup problems try to keep your namespaces flat and shallow wherever possible.

namespace firstLayer {
void func(const std::string&);

namespace secondLayer {
void func(int);

namespace thirdLayer {
void test()
{
    std::string s("test");
    func(s); // this doesn't work as expected
}

}  // namespace thirdLayer
}  // namespace secondLayer
}  // namespace firstLayer

In the above example, the compiler finds the declaration of func in the namespace secondLayer usign unqualified name lookup and then stops. It will throw compile error because the arguments do not match.

The same applies to overloads in a class hierarchy.

struct Base {
    void func(const std::string&);
};

struct Derived : Base {
    void func(int);
};

Derived d;
d.func(test); // this doesn't work as expected

Customization Point Object (CPO)

In order to talk a little bit about CPO, we will return to our swap example above.

In the swap implementation for our Container template, we had

using std::swap;
swap(lhs.val_, rhs.val_);

So, why is this needed and frequently seen? This is because of how C++ resolves function calls in templates. These 2 lines, are both used, for the sake of genericity.

First, there is an unqualified call to swap in order to find an overload that might be defined in lhs.val_ and rhs.val_’s associated namespaces and then, on the off-chance that there is no such overload, we can find the default version defined in the std namespace – this is done with the using std::swap line.

Swap functions like this one are called customization points – hooks used by generic code that end-users can specialize to customize the behavior for their types.

What customization point objects do is wrap the customization dispatch in a single object, so that you don’t have to the customization points(like mentioned above) by yourself.

C++20 Ranges added a lot of customization point objects.

std::ranges::swap(a, b);
std::ranges::begin(v);
std::ranges::end(v);

One more thing to note here is that, customization point objects aren’t always customizable. For example:

std::ranges::cbegin(v);

has no customization point cbegin that it tries to invoke. It only ever calls begin.

Leave a Reply

%d bloggers like this: