友元声明的复杂范围规则有什么意义?

What is the point of the complicated scoping rules for friend declarations?

我最近发现 friend 声明作用域遵循 extremely peculiar rules - 如果您有函数的 friend 声明(定义)或尚未声明的 class,它是在紧邻的命名空间中自动声明(定义),它对于非限定和限定查找是不可见的;但是,friend function 声明通过参数相关的查找仍然可见。

struct M {
    friend void foo();
    friend void bar(M);
};

void baz() {
    foo();    // error, unqualified lookup cannot find it
    ::foo();  // error, qualified lookup cannot find it
    bar(M()); // ok, thanks to ADL magic
}

如果您查看标准(请参阅 linked answer),他们会竭尽全力启用这种古怪的行为,在 qualified/non 符合条件的复杂查找中添加特定的例外。在我看来,最终结果非常令人困惑1,还有一个要添加到实现中的极端情况。作为

似乎更容易实施、指定,最重要的是,更容易理解,我想知道:他们为什么要为这个烂摊子烦恼?他们试图涵盖哪些用例?这些更简单的规则(特别是第二条,与现有行为最相似)违反了什么?


  1. 例如,在这种特殊情况下

    struct M {
       friend class N;
    };
    N *foo;
    typedef int N;
    

    你得到comically schizophrenic error messages

    <source>:4:1: error: 'N' does not name a type
     N *foo;
     ^
    <source>:5:13: error: conflicting declaration 'typedef int N'
     typedef int N;
                 ^
    <source>:2:17: note: previous declaration as 'class N'
        friend class N;
                     ^
    

    编译器首先声称不存在 N 这样的东西,但当您尝试提供冲突的声明时立即停止装傻。

嗯,要回答这个问题,您必须了解 C++ 的另一个主要功能:模板。

考虑这样的模板:

template <class T>
struct magic {
    friend bool do_magic(T*) { return true; }
};

在这样的代码中使用:

bool do_magic(void*) { return false; }

int main() {
    return do_magic((int*)0);
}

退出代码是 0 还是 1

好吧,这取决于 magic 是否曾经在任何可观察到的地方用 int 实例化。
至少它会,如果 friend - 仅声明为内联的函数会被普通查找规则找到。
而且你不能通过注入所有可能的东西来解决这个难题,因为模板可以专门化。

曾经是这种情况,但被取缔为 "too magic","too ill-defined"。

名称注入还有其他问题,因为它几乎没有像希望的那样定义明确。有关更多信息,请参阅 N0777: An Alternative to Name Injection from Templates