class 内部和外部定义的友元函数查找规则的区别

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

以下代码:

struct X {
    X() {}
};

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

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

编译失败,出现以下错误:

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

虽然如果我将 operator== 的定义移到 class 之外,它工作得很好:

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();
}

有人可以解释为什么吗? (理想情况下,引用标准和人类可读的 explanation/motivation。)在此处的答案中: @rightfold 提到

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

不过我不太明白是什么意思

需要注意的是class内外友元函数的查找规则不同,见[namespace.memdef](重点是我的)

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 ]

这意味着在您的第一个示例中,编译器看到了与操作数 intX 的比较,但是没有从 Xint 的可行转换(或从 intXX 也没有比较运算符)。不会尝试将两个操作数转换为 Y,因为根据上面引用的子句,匹配的比较运算符不可见。

同时你可以看到在第二个例子中使用非显式构造函数是多么危险,因为两个操作数都被隐式转换为可能不相关的类型Y。这可能会产生非常意外的行为,因为编译器认为由于语义不正确而不应编译的代码是有效的。另请参阅 C++ 核心指南 C.46: By default, declare single-argument constructors explicit.