为什么 clang 需要在模板中调用函数之前声明它?
Why does clang require a function to be declared before it's called in a template?
我有以下代码示例(可在 coliru 在线获取):
#include <iostream>
#include <utility>
struct Bar {
int a;
};
template <class T>
void print_arg(const T& arg) {
std::cout << arg << std::endl;
}
std::ostream& operator<<(std::ostream& os, const Bar& b) {
os << b.a;
return os;
}
template <class T1, class T2>
std::ostream& operator<<(std::ostream& os, const std::pair<T1, T2>& pair) {
os << "Pair(" << pair.first << ',' << pair.second << ")";
return os;
}
int main()
{
auto bar = Bar{1};
print_arg(bar);
print_arg(std::make_pair(bar, bar));
print_arg(std::make_pair(bar, 1));
print_arg(std::make_pair(0, 1));
}
main 函数的最后一行给我带来了麻烦。使用 g++ 编译工作正常(使用与下面完全相同的选项),我启动可执行文件并按预期打印所有内容。但是,Clang++ 给我以下错误:
$ clang++ -std=c++17 -O2 -Wall -Werror -Wpedantic main.cpp && ./a.out
main.cpp:10:15: error: call to function 'operator<<' that is neither visible in the template definition nor found by argument-dependent lookup
std::cout << arg << std::endl;
^
main.cpp:29:5: note: in instantiation of function template specialization 'print_arg<std::pair<int, int> >' requested here
print_arg(std::make_pair(0, 1));
^
main.cpp:19:15: note: 'operator<<' should be declared prior to the call site
std::ostream& operator<<(std::ostream& os, const std::pair<T, T>& pair) {
^
1 error generated.
此外,删除最后一行(将其注释掉)会导致 Clang++ 正确编译所有内容。据我所知,这意味着 std::pair<int, int>
在质量上不同于其他参数类型。
我的问题是,为什么 g++ 无论如何都要编译它?还有更重要的是,为什么clang认为后面声明operator<<(ostream, pair<Bar, Bar>)
是可以的,而operator<<(ostream, pair<int, int>)
就不可以了。是因为后者只包括标准类型和基本类型吗?
对我来说(有点)合乎逻辑的事情似乎是仅在 standard/basic 类型上定义函数是 UB,但 g++ 默默地忽略它并且 clang++ 给出了 weird-looking 错误消息。但是,这对我来说意义不大,我找不到相关的标准条款。
注意:我明白把声明向上移动是clang要求的,但我不明白为什么。我想在单独的 header 中提供 print_arg
功能,并允许包含 header 的人在使用 print_arg
.
时专门化 operator<<
查看 Language Compatibility : Unqualified lookup in templates 部分。它恰好解释了这种情况。
总结是GCC
编译有bug的代码,而clang遵循标准。
C++ 标准规定可以通过两种方式查找非限定名称。根据Clang's documentation on language compatibility:
First, the compiler does unqualified lookup in the scope where the
name was written. For a template, this means the lookup is done at the
point where the template is defined, not where it's instantiated.
Second, if the name is called like a function, then the compiler also
does argument-dependent lookup (ADL). Sometimes unqualified lookup can
suppress ADL; In ADL, the compiler looks at the types of all the
arguments to the call. When it finds a class type, it looks up the
name in that class's namespace; the result is all the declarations it
finds in those namespaces, plus the declarations from unqualified
lookup. However, the compiler doesn't do ADL until it knows all the
argument types.
有两种方法可以解决这个问题:
- 确保要调用的函数在可能调用它的模板之前声明。如果 none 的参数类型包含 类,则这是唯一的选项。您可以通过移动模板定义、移动函数定义或在模板前添加函数的前向声明来实现。
- 将函数移动到与其参数之一相同的名称空间中
ADL 适用。
↳ 参见 basic.lookup.argdep and temp.dep.candidate
标准中的相关文本是 C++17 [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 and from the definition context.
(temp.dep.candidate/1对此进行了详细说明)。
这段代码中的问题情况是print_arg
里面的调用std::cout << arg
。正在查找的名称是 operator<<
。这是一个从属名称,因为它是一个函数调用,其参数的类型取决于模板参数。
定义上下文是这个表达式出现的上下文,即在print_arg
里面。考虑此时可见的任何声明。
实例化上下文由[temp.point]定义;在此代码中,print_arg
是从 main()
调用的,因此实例化上下文在 main()
结束后的命名空间范围内。然而,正如上面第二个要点所涵盖的,从实例化上下文中考虑的唯一名称是那些通过参数相关查找找到的名称。
std::pair<Bar, int>
或 std::pair<Bar, Bar>
的参数具有 pair
的 ADL 类(因此命名空间 std
),以及 Bar
(因此全局命名空间)。模板类型的 ADL 不包括任何模板参数类型。
但是在 std::pair<int, int>
的情况下,唯一的 ADL 命名空间是 std
,因此找不到 ::operator<<
。
要理解的要点:operator<<(ostream, pair)
函数是由于 ADL 正在搜索全局命名空间而找到的,这是因为 Bar
的使用将全局命名空间添加到搜索列表中,甚至虽然这个函数没有具体提到 Bar
。如果 Bar
在其他命名空间中,那么所有三个调用都应该无法编译。
由于此处所述的原因,通常建议不要添加运算符重载,除非至少有一个参数位于用户定义的命名空间中。然后它总是会被 ADL 找到相应的参数。最好以不会为 (ostream, pair)
超载的方式定义您的库。
我有以下代码示例(可在 coliru 在线获取):
#include <iostream>
#include <utility>
struct Bar {
int a;
};
template <class T>
void print_arg(const T& arg) {
std::cout << arg << std::endl;
}
std::ostream& operator<<(std::ostream& os, const Bar& b) {
os << b.a;
return os;
}
template <class T1, class T2>
std::ostream& operator<<(std::ostream& os, const std::pair<T1, T2>& pair) {
os << "Pair(" << pair.first << ',' << pair.second << ")";
return os;
}
int main()
{
auto bar = Bar{1};
print_arg(bar);
print_arg(std::make_pair(bar, bar));
print_arg(std::make_pair(bar, 1));
print_arg(std::make_pair(0, 1));
}
main 函数的最后一行给我带来了麻烦。使用 g++ 编译工作正常(使用与下面完全相同的选项),我启动可执行文件并按预期打印所有内容。但是,Clang++ 给我以下错误:
$ clang++ -std=c++17 -O2 -Wall -Werror -Wpedantic main.cpp && ./a.out
main.cpp:10:15: error: call to function 'operator<<' that is neither visible in the template definition nor found by argument-dependent lookup
std::cout << arg << std::endl;
^
main.cpp:29:5: note: in instantiation of function template specialization 'print_arg<std::pair<int, int> >' requested here
print_arg(std::make_pair(0, 1));
^
main.cpp:19:15: note: 'operator<<' should be declared prior to the call site
std::ostream& operator<<(std::ostream& os, const std::pair<T, T>& pair) {
^
1 error generated.
此外,删除最后一行(将其注释掉)会导致 Clang++ 正确编译所有内容。据我所知,这意味着 std::pair<int, int>
在质量上不同于其他参数类型。
我的问题是,为什么 g++ 无论如何都要编译它?还有更重要的是,为什么clang认为后面声明operator<<(ostream, pair<Bar, Bar>)
是可以的,而operator<<(ostream, pair<int, int>)
就不可以了。是因为后者只包括标准类型和基本类型吗?
对我来说(有点)合乎逻辑的事情似乎是仅在 standard/basic 类型上定义函数是 UB,但 g++ 默默地忽略它并且 clang++ 给出了 weird-looking 错误消息。但是,这对我来说意义不大,我找不到相关的标准条款。
注意:我明白把声明向上移动是clang要求的,但我不明白为什么。我想在单独的 header 中提供 print_arg
功能,并允许包含 header 的人在使用 print_arg
.
operator<<
查看 Language Compatibility : Unqualified lookup in templates 部分。它恰好解释了这种情况。
总结是GCC
编译有bug的代码,而clang遵循标准。
C++ 标准规定可以通过两种方式查找非限定名称。根据Clang's documentation on language compatibility:
First, the compiler does unqualified lookup in the scope where the name was written. For a template, this means the lookup is done at the point where the template is defined, not where it's instantiated.
Second, if the name is called like a function, then the compiler also does argument-dependent lookup (ADL). Sometimes unqualified lookup can suppress ADL; In ADL, the compiler looks at the types of all the arguments to the call. When it finds a class type, it looks up the name in that class's namespace; the result is all the declarations it finds in those namespaces, plus the declarations from unqualified lookup. However, the compiler doesn't do ADL until it knows all the argument types.
有两种方法可以解决这个问题:
- 确保要调用的函数在可能调用它的模板之前声明。如果 none 的参数类型包含 类,则这是唯一的选项。您可以通过移动模板定义、移动函数定义或在模板前添加函数的前向声明来实现。
- 将函数移动到与其参数之一相同的名称空间中 ADL 适用。
↳ 参见 basic.lookup.argdep and temp.dep.candidate
标准中的相关文本是 C++17 [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 and from the definition context.
(temp.dep.candidate/1对此进行了详细说明)。
这段代码中的问题情况是print_arg
里面的调用std::cout << arg
。正在查找的名称是 operator<<
。这是一个从属名称,因为它是一个函数调用,其参数的类型取决于模板参数。
定义上下文是这个表达式出现的上下文,即在print_arg
里面。考虑此时可见的任何声明。
实例化上下文由[temp.point]定义;在此代码中,print_arg
是从 main()
调用的,因此实例化上下文在 main()
结束后的命名空间范围内。然而,正如上面第二个要点所涵盖的,从实例化上下文中考虑的唯一名称是那些通过参数相关查找找到的名称。
std::pair<Bar, int>
或 std::pair<Bar, Bar>
的参数具有 pair
的 ADL 类(因此命名空间 std
),以及 Bar
(因此全局命名空间)。模板类型的 ADL 不包括任何模板参数类型。
但是在 std::pair<int, int>
的情况下,唯一的 ADL 命名空间是 std
,因此找不到 ::operator<<
。
要理解的要点:operator<<(ostream, pair)
函数是由于 ADL 正在搜索全局命名空间而找到的,这是因为 Bar
的使用将全局命名空间添加到搜索列表中,甚至虽然这个函数没有具体提到 Bar
。如果 Bar
在其他命名空间中,那么所有三个调用都应该无法编译。
由于此处所述的原因,通常建议不要添加运算符重载,除非至少有一个参数位于用户定义的命名空间中。然后它总是会被 ADL 找到相应的参数。最好以不会为 (ostream, pair)
超载的方式定义您的库。