Why is this friend method not found as expected?

It comes down to how C++ generates candidate functions when performing overload resolution. It's trying to find candidates for operator<<(std::cout, b). This means it performs unqualified name lookup which includes performing argument-dependent lookup (ADL). Let's take a look at how that works.

For the first code snippet, unqualified name lookup finds the declaration when it looks in the enclosing scope of the calling code, without needing to perform ADL. It sees inline std::ostream& operator<<(std::ostream& os, const A&) as a candidate, and then is able to apply the user-defined conversion to b to see that it's a valid function to use for overload resolution. All well and good.

For the second code snippet, though, we don't have a declaration of operator<< at file scope. The declaration and definition are entirely within the definition of the class A. That still might let us find it as a candidate function for std::cout << b, but it'll have to be through ADL. Let's check to see if it's actually visible through that:

Otherwise, for every argument in a function call expression its type is examined to determine the associated set of namespaces and classes that it will add to the lookup.

...

  1. For arguments of class type (including union), the set consists of

a) The class itself

b) All of its direct and indirect base classes

c) If the class is a member of another class, the class of which it is a member

d) The innermost enclosing namespaces of the classes added to the set

At any stage, would we look inside the definition of A when performing ADL with arguments std::cout and b? None of a), b), and c) apply to A because A isn't B, A isn't a base class of B, and A doesn't contain B as a member. Crucially, "any class to which the class is implicitly convertible" isn't used to generate candidates through ADL.

So ultimately in the second code snippet, the name lookup never sees the declaration of std::ostream& operator<<(std::ostream& os, const A&) and never realizes that it can apply a user-defined conversion to apply it with the appropriate arguments.

If we just make the function declaration (not definition) visible at file scope like so:

#include <iostream>

class A {
public:
    friend std::ostream& operator<<(std::ostream& os, const A&) {
        os << "Called\n";
        return os;
    }
};

std::ostream& operator<<(std::ostream& os, const A&);

class B {
public:
    operator A() { return A(); }
};

int main()
{
    A a;
    std::cout << a;
    B b;
    std::cout << b;
}

This function declaration is once again found through ordinary unqualified name lookup, the user-defined conversion comes in during overload resolution, and the expected output of "Called" being printed twice is recovered.

Tags:

C++