4

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.

anxieux
  • 757
  • 5
  • 14
  • Is it that you don't understand what ADL is or you don't understand how functions declared outside that class are looked up? – David G Aug 28 '18 at 23:52
  • @0x499602D2 I don't see how ADL is applicable here, since ADL is about looking for functions in the different namespace, but I don't have any namespaces at all in this case. – anxieux Aug 28 '18 at 23:58
  • @0x499602D2 And what I'm really asking is: 1. What is the difference in the lookup rules in these two cases? 2. And why is there any difference at all? – anxieux Aug 29 '18 at 00:08
  • I don't see why the first case should work at all. Do you want the compiler to convert the operands in a comparison to any conceivable unrelated type until there is one which has comparison operator that matches? – Henri Menke Aug 29 '18 at 00:42
  • @HenriMenke What is the difference between first and second cases? Yes, I expect compiler to try implicit conversions to find a matching function. This is exactly what it does in the second case, right? – anxieux Aug 29 '18 at 00:44

1 Answers1

3

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.

Henri Menke
  • 10,705
  • 1
  • 24
  • 42
  • 1
    Your Standard quote about the lexical scope of a friend function definition is actually saying that names used in that definition are looked up in the class scope, so for example class member names are visible. The rule about lookup of the friend function name is in [namespace.memdef]/3. – aschepler Aug 29 '18 at 01:06
  • @aschepler, but I don't see in [namespace.memdef]/3 anything about the difference between friend functions defined inside vs outside of the class. Or is it: "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." with conjunction to what Henri wrote? – anxieux Aug 29 '18 at 01:15
  • 1
    @anxieux "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.)]" So it's not directly where the definition is, it's just that defining inside the class allows for the case where there is no declaration at all outside the class, which is what really makes the difference. – aschepler Aug 29 '18 at 01:24
  • @aschepler Ok, so when I define a `friend` function inside the class, there is no any declaration at the namespace level, and thus lookup fails, correct? If so, why does friend function inside the class work at all if one of the arguments is of the exact type `Y`? – anxieux Aug 29 '18 at 01:38
  • Oh, that's because of "its name may be found by the name lookup that considers functions from namespaces and classes associated with the types of the function arguments" – anxieux Aug 29 '18 at 01:42
  • 1
    @anxieux Right, which refers to the special Argument-Dependent Lookup rule saying that it can see friend functions declared in the "associated classes" determined from the function arguments. [basic.lookup.argdep]/4.2. – aschepler Aug 29 '18 at 01:43
  • @aschepler I guess the only remaining question is: why it was made so complicated? Why not just automatically declare at the namespace level friend function defined inside in the class? – anxieux Aug 29 '18 at 01:50