c++name-lookup

Difference between lookup rules for friend function defined inside vs outside of the class


The following code:

struct X {
    X() {}
};

struct Y {
    Y() {}
    Y(X) {}
    Y(int) {}
    friend bool operator==(const Y&, const Y&) { return false; }
};

bool f()
{
    return 1 == X();
}

fails to compile with the following error:

error: no match for 'operator==' (operand types are 'int' and 'X')
     return 1 == X();

While if I move definition of operator== outside of the class it works just fine:

struct X {
    X() {}
};

struct Y {
    Y() {}
    Y(X) {}
    Y(int) {}
    friend bool operator==(const Y&, const Y&);
};

inline bool operator==(const Y&, const Y&) { return false; }

bool f()
{
    return 1 == X();
}

Could someone explain why? (Ideally, with some quote from the standard and human-readable explanation/motivation.) In the answer here: https://stackoverflow.com/a/20114792/1350936 @rightfold mentioned that

Functions defined outside of the class can be found even without ADL

But I don't quite understand what it means.


Solution

  • One thing to note is that the lookup rules for friend functions inside and outside the class are different, see [namespace.memdef] (emphasis mine)

    If a friend declaration in a non-local class first declares a class, function, class template or function template the friend is a member of the innermost enclosing namespace. The friend declaration does not by itself make the name visible to unqualified lookup or qualified lookup. [ Note: The name of the friend will be visible in its namespace if a matching declaration is provided at namespace scope (either before or after the class definition granting friendship). — end note] If a friend function or function template is called, its name may be found by the name lookup that considers functions from namespaces and classes associated with the types of the function arguments ([basic.lookup.argdep]). If the name in a friend declaration is neither qualified nor a template-id and the declaration is a function or an elaborated-type-specifier, the lookup to determine whether the entity has been previously declared shall not consider any scopes outside the innermost enclosing namespace. [  Note: The other forms of friend declarations cannot declare a new member of the innermost enclosing namespace and thus follow the usual lookup rules. — end note ]

    That means that in your first example the compiler sees a comparison with operands int and X but there is no viable conversion from X to int (or from int to X but X does not have a comparison operator either). A conversion of both operands to Y is not attempted, because the matching comparison operator is not visible as per the clause quoted above.

    At the same time you can see how dangerous it is to have non-explicit constructors in the second example, because both operands are implicitly converted to a possibly unrelated type Y. This might yield very unexpected behaviour because code which should have not compiled due to semantic incorrectness is considered valid by the compiler. See also the C++ Core Guideline C.46: By default, declare single-argument constructors explicit.