友元声明的复杂范围规则有什么意义?
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,还有一个要添加到实现中的极端情况。作为
- 要求
friend
声明引用现有名称、句点;或者
- 允许他们像现在这样声明东西,但不改变普通的名称查找(因此,这些名称变得可见,就像在封闭的命名空间中声明 "normally" 一样)
似乎更容易实施、指定,最重要的是,更容易理解,我想知道:他们为什么要为这个烂摊子烦恼?他们试图涵盖哪些用例?这些更简单的规则(特别是第二条,与现有行为最相似)违反了什么?
例如,在这种特殊情况下
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。
我最近发现 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,还有一个要添加到实现中的极端情况。作为
- 要求
friend
声明引用现有名称、句点;或者 - 允许他们像现在这样声明东西,但不改变普通的名称查找(因此,这些名称变得可见,就像在封闭的命名空间中声明 "normally" 一样)
似乎更容易实施、指定,最重要的是,更容易理解,我想知道:他们为什么要为这个烂摊子烦恼?他们试图涵盖哪些用例?这些更简单的规则(特别是第二条,与现有行为最相似)违反了什么?
例如,在这种特殊情况下
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。