12

我最近发现友元声明作用域遵循极其特殊的规则——如果你有一个friend尚未声明的函数或类的声明(定义),它会在紧接的命名空间中自动声明(定义),它是不可见的不合格和合格的查找;但是,友元函数声明通过依赖于参数的查找仍然可见。

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
}

如果您查看标准(请参阅链接的答案),他们会竭尽全力启用这种古怪的行为,在具有复杂规则的合格/不合格查找中添加特定例外。最终结果在我看来非常令人困惑1,还有另一个角落案例要添加到实现中。作为任一

  • 要求friend声明提及现有名称、句号;或者
  • 允许他们像现在一样声明东西,但不改变普通名称查找(因此,这些名称变得可见,就好像在封闭的命名空间中“正常”声明一样)

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


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

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

    你收到可笑的精神分裂错误信息

    <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,但是当您尝试提供冲突的声明时立即停止装傻。

4

1 回答 1

13

好吧,要回答这个问题,你必须看看 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);
}

退出代码是0or1吗?

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

有一段时间是这样,但被禁止为“太神奇”和“太不明确”。

名称注入还有其他问题,因为它的定义不如预期的那么好。有关更多信息,请参阅N0777:从模板注入名称的替代方法

于 2018-08-31T21:18:42.797 回答