奇怪的功能查找
Strange function look up
在尝试掌握 std::ostream_iterator 时,我想出了以下无法编译的代码(在 gcc 5.3 或 clang 3.6 下)。
#include <iostream>
#include <iterator>
namespace temp {
struct Point {
int x;
};
}
//namespace temp {
//namespace std {
std::ostream& operator<<(std::ostream& s, temp::Point p) {
return s << p.x;
}
//}
int main(int argc, char** argv) {
temp::Point p{1};
std::ostream_iterator{std::cout} = p;
//std::cout << p;
std::cout << std::endl;
return 0;
}
当 operator<<
在全局范围内时,编译会抛出大量模板实例化错误。
但是,std::cout << p
工作正常。而且,如果 operator<<
在 namespace temp
或 namespace std
中声明,代码将按预期编译和运行。
我的问题是为什么全局 operator<<
不起作用?
我不知道你想用这条线做什么:
std::ostream_iterator{std::cout} = p;
至于你的实际问题,你可以在全局范围内定义operator<<()
:
#include <iostream>
#include <iterator>
namespace temp {
struct Point {
int x;
};
}
std::ostream& operator<<(std::ostream& s, temp::Point p) {
return s << p.x;
}
int main(int argc, char** argv) {
temp::Point p{1};
//std::ostream_iterator{std::cout} = p;
std::cout << p;
std::cout << std::endl;
}
编译并输出1
。
这条线有两个问题(除了没有意义的事实):
std::ostream_iterator{std::cout} = p;
首先,std::ostream_iterator
是一个 class 模板,而不是 class。所以你的意思可能是:
std::ostream_iterator<Point>{std::cout} = p;
现在,ostream_iterator::operator=
是如何工作的?它确实依赖于 operator<<
,但在 class 模板的成员函数定义的上下文中。因此,它将找到的重载是 ostream_iterator
的 operator=
范围内的重载(你的不是)和可以在参数的关联名称空间中找到的重载(你的又不是).这就是查找失败的原因。
如果您只是将 operator<<
移动到 namespace temp
:
namespace temp {
std::ostream& operator<<(std::ostream& s, Point p) {
return s << p.x;
}
}
或作为非会员朋友:
namespace temp {
struct Point {
int x;
friend std::ostream& operator<<(std::ostream& s, Point p) { ... }
};
}
然后参数依赖查找成功,这有效:
std::ostream_iterator<Point>{std::cout} = p;
就是说,不要编写该代码。使用正常 std::cout << p
。
这是同一现象的另一个例子,可能更容易理解。假设我们有一些函数模板,它只是在其参数上调用另一个函数:
template <class T>
void call_f(T val) {
f(val);
}
f
将通过从 call_f
的 定义点 查找或通过对 val
的参数相关查找找到。所以如果我们以后做类似的事情:
namespace N {
struct X { };
}
void f(N::X ) { }
int main() {
f(N::X{}); // ok
call_f(N::X{}); // error: can't find 'f'
}
该行错误是因为从 call_f
的定义来看,没有函数 f()
(根本)并且 namespace N
中没有函数 f
任何一个。但是如果我们将 f
移动到那个命名空间中,两个版本都可以正常工作:
template <class T>
void call_f(T val) { f(val); }
namespace N {
struct X { };
void f(X ) { }
}
int main() {
f(N::X{}); // ok
call_f(N::X{}); // now ok too, ADL finds N::f
}
您观察到的行为是 两阶段查找 过程的一个特点,它用于解析模板定义中引用的名称,以及它与参数相关查找 (ADL) 的交互).
在您的情况下,您使用 std::ostream_iterator
中的 operator =
。从 std::ostream_iterator::operator =
的定义中引用的名称将通过两阶段查找进行查找:在第一阶段查找非依赖名称(从 operator =
的定义),同时查找依赖名称从实例化的角度(您对 operator =
的调用)。
在内部,std::ostream_iterator::operator =
对给定的 (stream, value)
对使用运算符 <<
。由于 value
的类型依赖于模板参数,因此对运算符 <<
的引用被视为 dependent 。因此,其决议推迟到第二阶段。
的确,查找的第二阶段(从实例化开始执行)通常比第一阶段看到更多的名称。您显然希望全局命名空间中 operator <<
的定义也变得可见。
但是,重要的是要注意有关第二阶段的一个重要细节:在第二阶段只有 关联的命名空间 (ADL 引入的命名空间)是 "enriched" 具有在实例化时可见的其他名称。但是 "regular" 命名空间(与 ADL 无关) 完全不受第二阶段的影响。在后面的命名空间中,编译器仍然只能看到在第一阶段可见的相同名称,除此之外别无其他。
这正是标准中以下段落所说的
14.6.4 Dependent name resolution [temp.dep.res]
1 In resolving dependent names, names from the following sources are considered:
—
Declarations that are visible at the point of definition of the
template.
— Declarations from namespaces associated with the types of
the function arguments both from the instantiation context (14.6.4.1)
and from the definition context.
这解释了您的情况。即使您向 global 命名空间添加了额外的 operator <<
,在这种情况下,全局命名空间也不是 ADL 关联的命名空间之一(仅 std
和 temp
是)。因此,第二阶段无法真正看到您额外的 <<
定义。
但是,如果您将定义添加到 ADL 关联的命名空间之一,第二阶段将立即注意到该添加。这就是为什么如果您在 std
或 temp
命名空间中定义运算符,您的代码可以正常编译的原因。
在尝试掌握 std::ostream_iterator 时,我想出了以下无法编译的代码(在 gcc 5.3 或 clang 3.6 下)。
#include <iostream>
#include <iterator>
namespace temp {
struct Point {
int x;
};
}
//namespace temp {
//namespace std {
std::ostream& operator<<(std::ostream& s, temp::Point p) {
return s << p.x;
}
//}
int main(int argc, char** argv) {
temp::Point p{1};
std::ostream_iterator{std::cout} = p;
//std::cout << p;
std::cout << std::endl;
return 0;
}
当 operator<<
在全局范围内时,编译会抛出大量模板实例化错误。
但是,std::cout << p
工作正常。而且,如果 operator<<
在 namespace temp
或 namespace std
中声明,代码将按预期编译和运行。
我的问题是为什么全局 operator<<
不起作用?
我不知道你想用这条线做什么:
std::ostream_iterator{std::cout} = p;
至于你的实际问题,你可以在全局范围内定义operator<<()
:
#include <iostream>
#include <iterator>
namespace temp {
struct Point {
int x;
};
}
std::ostream& operator<<(std::ostream& s, temp::Point p) {
return s << p.x;
}
int main(int argc, char** argv) {
temp::Point p{1};
//std::ostream_iterator{std::cout} = p;
std::cout << p;
std::cout << std::endl;
}
编译并输出1
。
这条线有两个问题(除了没有意义的事实):
std::ostream_iterator{std::cout} = p;
首先,std::ostream_iterator
是一个 class 模板,而不是 class。所以你的意思可能是:
std::ostream_iterator<Point>{std::cout} = p;
现在,ostream_iterator::operator=
是如何工作的?它确实依赖于 operator<<
,但在 class 模板的成员函数定义的上下文中。因此,它将找到的重载是 ostream_iterator
的 operator=
范围内的重载(你的不是)和可以在参数的关联名称空间中找到的重载(你的又不是).这就是查找失败的原因。
如果您只是将 operator<<
移动到 namespace temp
:
namespace temp {
std::ostream& operator<<(std::ostream& s, Point p) {
return s << p.x;
}
}
或作为非会员朋友:
namespace temp {
struct Point {
int x;
friend std::ostream& operator<<(std::ostream& s, Point p) { ... }
};
}
然后参数依赖查找成功,这有效:
std::ostream_iterator<Point>{std::cout} = p;
就是说,不要编写该代码。使用正常 std::cout << p
。
这是同一现象的另一个例子,可能更容易理解。假设我们有一些函数模板,它只是在其参数上调用另一个函数:
template <class T>
void call_f(T val) {
f(val);
}
f
将通过从 call_f
的 定义点 查找或通过对 val
的参数相关查找找到。所以如果我们以后做类似的事情:
namespace N {
struct X { };
}
void f(N::X ) { }
int main() {
f(N::X{}); // ok
call_f(N::X{}); // error: can't find 'f'
}
该行错误是因为从 call_f
的定义来看,没有函数 f()
(根本)并且 namespace N
中没有函数 f
任何一个。但是如果我们将 f
移动到那个命名空间中,两个版本都可以正常工作:
template <class T>
void call_f(T val) { f(val); }
namespace N {
struct X { };
void f(X ) { }
}
int main() {
f(N::X{}); // ok
call_f(N::X{}); // now ok too, ADL finds N::f
}
您观察到的行为是 两阶段查找 过程的一个特点,它用于解析模板定义中引用的名称,以及它与参数相关查找 (ADL) 的交互).
在您的情况下,您使用 std::ostream_iterator
中的 operator =
。从 std::ostream_iterator::operator =
的定义中引用的名称将通过两阶段查找进行查找:在第一阶段查找非依赖名称(从 operator =
的定义),同时查找依赖名称从实例化的角度(您对 operator =
的调用)。
在内部,std::ostream_iterator::operator =
对给定的 (stream, value)
对使用运算符 <<
。由于 value
的类型依赖于模板参数,因此对运算符 <<
的引用被视为 dependent 。因此,其决议推迟到第二阶段。
的确,查找的第二阶段(从实例化开始执行)通常比第一阶段看到更多的名称。您显然希望全局命名空间中 operator <<
的定义也变得可见。
但是,重要的是要注意有关第二阶段的一个重要细节:在第二阶段只有 关联的命名空间 (ADL 引入的命名空间)是 "enriched" 具有在实例化时可见的其他名称。但是 "regular" 命名空间(与 ADL 无关) 完全不受第二阶段的影响。在后面的命名空间中,编译器仍然只能看到在第一阶段可见的相同名称,除此之外别无其他。
这正是标准中以下段落所说的
14.6.4 Dependent name resolution [temp.dep.res]
1 In resolving dependent names, names from the following sources are considered:
— Declarations that are visible at the point of definition of the template.
— Declarations from namespaces associated with the types of the function arguments both from the instantiation context (14.6.4.1) and from the definition context.
这解释了您的情况。即使您向 global 命名空间添加了额外的 operator <<
,在这种情况下,全局命名空间也不是 ADL 关联的命名空间之一(仅 std
和 temp
是)。因此,第二阶段无法真正看到您额外的 <<
定义。
但是,如果您将定义添加到 ADL 关联的命名空间之一,第二阶段将立即注意到该添加。这就是为什么如果您在 std
或 temp
命名空间中定义运算符,您的代码可以正常编译的原因。