Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is this friend method not found as expected?

Tags:

c++

When I have a small piece of code like this:

#include <iostream>

class A {
public:
    friend inline std::ostream& operator<<(std::ostream& os, const A&);
};

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

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

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

the operator<< is called twice for both a and b, because b has an implicit user conversion operator for type A. But I was surprised when I modified the code to this:

#include <iostream>

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

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

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

As you see, I have just transferred the definition inside the class rather than using a free function and marking it as a friend of the class. But this code gives an error which says no suitable function for operator<< is found for b.

Why does this problem happen?

like image 228
Afshin Avatar asked Oct 04 '20 17:10

Afshin


1 Answers

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.

like image 104
Nathan Pierson Avatar answered Nov 09 '22 01:11

Nathan Pierson