为什么clang要求在模板中调用函数之前先声明一个函数?

时间:2020-02-15 19:34:15

标签: c++ g++ clang++

我有以下代码示例(可从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>)来说不是可以的。是否因为后者仅包括标准类型和基本类型?

对我来说,(某种程度上)逻辑上的事情似乎是仅在标准/基本类型上定义函数是UB,但是g ++默默地忽略了它,而clang ++给出了一个看起来很奇怪的错误消息。但是,这对我来说并没有太大意义,我找不到相关的标准条款。

注意:我了解向上声明是clang所要求的,但我不明白为什么。我想在单独的标题中提供print_arg函数,并允许包含该标题的人在使用operator<<时专门使用print_arg

3 个答案:

答案 0 :(得分:4)

查看Language Compatibility : Unqualified lookup in templates部分。它确切地解释了这种情况。

摘要是GCC编译错误代码,而clang遵循标准。

答案 1 :(得分:3)

C ++标准规定,可以通过两种方式查找不合格的名称。根据{{​​3}}:

首先,编译器会在 名字被写了。对于模板,这意味着查找是在 点定义模板的位置,而不是实例化的位置。

第二,如果名称像函数一样被调用,那么编译器也会 执行依赖于参数的查找(ADL)。有时不合格的查询可以 抑制ADL;在ADL中,编译器会查看所有 调用的参数。找到类类型时,它会查找 该类名称空间中的名称;结果就是所有的声明 在这些命名空间中查找,再加上来自不合格的声明 抬头。但是,编译器只有知道了所有 参数类型。

有两种方法可以解决此问题:

  1. 确保在要调用的函数之前声明了要调用的函数。如果其所有参数类型都不包含类,则这是唯一的选择。您可以通过移动模板定义或移动函数定义,或在模板之前添加函数的前向声明来完成此操作。
  2. 将函数与其参数之一移动到相同的名称空间中 ADL适用。

↳参见Clang's documentation on language compatibilitybasic.lookup.argdep

答案 2 :(得分:1)

该标准中的相关文本为C ++ 17 [temp.dep.res] / 1:

在解析从属名称时,将考虑以下来源的名称:

  • 在模板定义时可见的声明。
  • 来自实例化上下文和定义上下文中与函数参数类型关联的名称空间的声明。

(这在temp.dep.candidate / 1上进行了详细说明)。

此代码中的问题情况是在std::cout << arg内部的调用print_arg。正在查找的名称是operator<<。这是一个依赖名称,因为它是带有参数的函数调用,其类型取决于模板参数。

定义上下文是该表达式出现的上下文,即在print_arg内部。此时将考虑任何可见的声明。

实例上下文由[temp.point]定义;在此代码中,print_arg是从main()调用的,因此实例化上下文位于main()结束之后的命名空间范围内。但是,如上面第二个要点所述,从实例化上下文考虑的唯一名称是通过参数依赖查找找到的名称。

std::pair<Bar, int>std::pair<Bar, Bar>的自变量具有pair(因此命名空间std)和Bar(因此全局命名空间)的ADL类。模板类型的ADL确实包括任何模板参数类型。

但是,在std::pair<int, int>情况下,唯一的ADL名称空间是std,因此找不到::operator<<


重要的理解要点:之所以找到operator<<(ostream, pair)函数是因为ADL正在搜索全局名称空间,这是因为Bar的使用将全局名称空间添加到了搜索列表中,即使这样做函数没有特别提及Bar。如果Bar在其他命名空间中,则所有三个调用都将无法编译。

由于这里提到的原因,通常建议不要添加运算符重载,除非用户定义的名称空间中至少有一个参数。然后,它将始终由ADL查找相应的参数。最好以一种不会对(ostream, pair)重载的方式定义您的库。