Skip to main content

Hidden Friends

I describe what argument dependent lookup is. I show how it ensures the compilation of even most simple C++ programs. Then, I go over its disadvantages and how it can be used more efficiently through the hidden friends pattern.

Argument Dependent Lookup

Before understanding the concept of hidden friends, we need to understand what argument dependent lookup (ADL) is. It is also called Koenig-Lookup and its purpose is simplification of code.

Consider the following. Including the whole namespace std is considered bad practice. By using namespace std, you make all the types visible in your module. It might be not what you want, as std::swap() or std::vector might introduce name conflicts with other implementations.

Instead, you might decide to pick specific types from std.

#include <iostream>

auto main() -> int
{
  std::cout << "Poor man's string.\n";
}

This is a trivial example, but it demonstrates ADL already. Why does it compile? Sure, std::cout is visible, but we also use operator << on it. std::cout is the only type we explicitly take from std. How is the operator found without using std::operator <<?

The solution is ADL. It is specified in §6.5.3 of the current C++ specification as part of the name lookup rules.

C++ Specification §6.5.3/1

When the postfix-expression in a function call (7.6.1.3) is an unqualified-id, other namespaces not considered during the usual unqualified lookup (6.5.2) may be searched, and in those namespaces, namespace-scope friend function or function template declarations (11.9.4) not otherwise visible may be found. These modifications to the search depend on the types of the arguments (and for template template arguments, the namespace of the template argument).

The following sections clarify details. ADL extends searchable namespaces for the function's name to those of the function's parameters. In our example, the function is operator <<. The second parameter is a built-it type. Therefore, it does not introduce any new namespaces. The first does, however. Because of std::cout the namespace std is also searched for operator << candidates. The fitting one taking a character literal is found among those. This is the reason why the above example compiles.

Precondition. For ADL to be allowed, the function's name must be unqualified. Hence, only a simple name triggers it. An example would be swap(), while std::swap() would prevent ADL due to the specified namespace. Even parentheses would do that.

In the example above, we could have written our call using function syntax.

operator << (std::cout, "Poor man's string.\n");

This compiles fine. The operator name in parenthesis would disable ADL, however. In such case we would need to pull it explicitly from std.

/* Option 1: Explicit specification of namespace. */
(std::operator <<)(std::cout, "Poor man's string.\n");

/* Option 2: Explicit usage of operator. */
using std::operator <<;
(operator <<)(std::cout, "Poor man's string.\n");

In conclusion, ADL makes your life easier. It allows the compiler to implicitly look for functions in places that usually make sense.

ADL

ADL extends visible namespaces to those of the function's parameters. The function's name needs to be unqualified.

Disadvantages of ADL

ADL works in most situations. However, it also yields too many candidates most of the time. Consider the following prominent example.

#include <iostream>
#include <vector>

auto main() -> int {
  std::vector v{0};
  std::cout << v;
}

If you try to compile that, you instantly get an email. It is your compiler asking for a long meeting. It wants to go over all the myriads of candidates for operator << and explain very precisely why each of them does not fit.

You and your compiler both know the reason. There is no operator << which takes std::vector as parameter. It does not save you from compiler's monologue, however. The compiler needs to regard all candidates equally. There is no way for it to know which candidates are important and which are not. The only way would be to limit the number of candidates in the first place.

Worse scenarios than overly long error messages exist. Among candidates provided by ADL some might unintentionally fit. If your type can be implicitly converted to match such a candidate, the way is open for some debugging sessions.

Hidden Friends

According to legends, the term “hidden friends” has been coined by Scott Mayers. It describes a pattern to limit the number of function candidates. Instead of declaring free-standing operators, they are declared as friends in type's definition. The following example uses the spaceship operator for demonstration.

struct S1 final {
    int data {0};

    friend auto operator <=> (S1& s, int data) noexcept
      -> auto { return s.data <=> data; }
};

struct S2 final {
  /* Same, except missing `operator <=>`. */
};

We declare two structs S1 and S2. The former features operator <=>, while the latter does not. We then attempt to use the missing one.

Note

It does not matter whether the friend declaration is public, protected or private.

auto main() -> int {
  S1 s1{};
  S2 s2{};

  s1 <=> 10; /* Correct. */
  10 <=> s1; /* Correct, as swapped arguments are valid. */

  s2 <=> 20; /* Error: Missing `operator <=>`. */
}

Now, we get a surprisingly short message from gcc v10.2.0. It just says what we had wished in case of std::vector.

main.cpp: In function ‘int main()’:
main.cpp:22:6: error: no match for ‘operator<=>’
                      (operand types are ‘S2’ and ‘int’)
   22 |   s2 <=> 20;
      |   ~~ ^~~ ~~
      |   |      |
      |   S2     int

If S2 had more unfitting overloads of operator <=>, those would be listed as well. Declarations in S1 are excluded, however.

If operator <=> is searched for S1, it is found in own type's scope. If the operator is searched for S2, none can be found directly in it. The surrounding scope does not include any free-standing implementations, so the name resolution fails. The scope of S1 is not considered. This shortens the list of possible candidates.

Excluded candidates avoid aforementioned disadvantages of ADL. The error lists contain only most relevant candidates. Some exclusions might even contain unintentionally matching functions. In theory all functions can benefit, but it especially makes sense for operators. As they are limited, they tend to participate in more overloads.

Conclusion

ADL extends the search for function candidates to its parameters' scopes. Especially for the standard library this leads to overly long error messages. Moreover, even unintended candidates might be pulled in.

You cannot change STL's specification to apply the hidden friends pattern. However, you can apply the pattern to your own code. This avoids surprises and provides less verbose compiler messages.

Have fun with it and write solid code.