错误:使用 clang 没有可行的重载,使用 gcc 编译

error: no viable overloading with clang, compiles with gcc

提问人:francesco 提问时间:6/23/2020 最后编辑:francesco 更新时间:6/24/2020 访问量:1915

问:

以下程序使用 g++(版本 10.1.0)可以正常编译,但不能使用 clang++ (10.0.0)

#include <iostream>

template <typename U>
struct A { U x; };

namespace tools {
  template <typename U>
  void operator+=(A<U>& lhs, const A<U>& rhs) { lhs.x += rhs.x; }
}

namespace impl {
  template <typename U = int>
  void f() {
    A<U> a{3};
    A<U> b{2};
    a += b;
    std::cout << a.x << std::endl;
  }
}

namespace impl {
  using namespace tools;
}

int main()
{
  impl::f();
}

错误是:

name.cpp:16:7: error: no viable overloaded '+='
    a += b;
    ~ ^  ~
name.cpp:27:9: note: in instantiation of function template specialization 'impl::f<int>' requested here
  impl::f();

显然,在模板函数之前移动零件可以消除 clang 错误。using namespace toolsimpl::f()

补充说明这里重要的一点是,这是一个模板函数。如果没有模板参数,代码既不会使用 gcc 编译,也不会使用 clang 编译。f

这里什么编译器是正确的?海湾合作委员会还是叮当声?

C++ 模板 命名空间 language-lawyer

评论

0赞 Eric Towers 6/24/2020
@francesco : 对我有用。

答:

7赞 bartop 6/23/2020 #1

根据这一点,似乎 clang 就在这里。简而言之 - 您正在扩展您的命名空间,但应该仅向前“传播”到此扩展。using namespace

using 指令指定指定命名空间中的名称可以在 using 指令出现在 using 指令之后的作用域中使用。 在非限定名称查找 ([basic.lookup.unqual]) 期间,名称看起来就像在最近包含 using 指令和指定命名空间的封闭命名空间中声明一样。 [注:在此上下文中,“包含”是指“直接或间接包含”。 — 尾注 ]

评论

0赞 francesco 6/23/2020
但这也适用于模板函数?因为,如果你删除了代码中的所有模板参数(比如放在任何地方),那么代码也不会在 gcc 上编译。但是有模板呢?U=int
0赞 bartop 6/23/2020
@francesco 我认为模板是这里差异的根本原因,因为编译器必须以半魔法的方式处理它们(即使在大多数情况下,标准要求将它们视为常规函数)。在这种情况下,我认为它们不应该与功能有任何区别
1赞 dfrib 6/23/2020 #2

Clang 是对的:非限定的依赖名称查找仅考虑在模板定义点可见的声明

在您的示例中,是函数模板中的依赖名称,在这种情况下,调用的非限定名称查找仅考虑在函数模板的定义点可见的声明。由于命名空间仅作为指定命名空间添加到定义点 之后,因此 unqual.名称查找将看不到从 引入的声明,并且无法看到 。因此,Clang 在这里是正确的,而 GCC 和 MSVC 在不拒绝代码方面是错误的。operator+=fa += b;ftoolsimplftoolstools::operator+=

GCC 的这种行为似乎只在依赖名称引用运算符函数时才存在,而如果我们用命名函数替换运算符,GCC 也会拒绝代码。

被 Clang 拒绝,被 GCC 接受:

struct Dummy{};

namespace ns_g {
    template <typename T>
    bool operator!(T) { return true; } 
}  // namespace ns_f

namespace ns_f {
    template <typename T>
    void f() { 
        (void)(!T{});
    }
    
    // Add ns_g as a nominated namespace to ns_f
    // _after_ point of definition of ns_f::f.
    using namespace ns_g;
}  // namespace ns_f

int main() {
    ns_f::f<Dummy>();
    return 0;
}

被 Clang 和 GCC 拒绝:

struct Dummy{};

namespace ns_g {
    template <typename T>
    bool g(T) { return true; } 
}  // namespace ns_f

namespace ns_f {
    template <typename T>
    void f() { 
        (void)(g(T{}));
    }
    
    // Add ns_g as a nominated namespace to ns_f
    // _after_ point of definition of ns_f::f.
    using namespace ns_g;
}  // namespace ns_f

int main() {
    ns_f::f<Dummy>();
    return 0;
}

对于后者,GCC 甚至给了我们一个注释:

注意:''宣布 在这里,稍后在翻译单元中template<class T> bool ns_g::g(T)

这种不一致本身就暗示了 GCC 在前一个示例中是错误的,我们可能不会说 Clang 的语言兼容性页面明确提到某些版本的 GCC 可能接受无效代码:

语言兼容性

[...]

模板中的不合格查找

某些版本的 GCC 接受以下无效代码:[...]

即使 Clang 指出的特定示例也被最近的 GCC 版本拒绝,这个问题的上下文是相同的。


GCC 上的未解决错误报告

请注意,一个类似的 SO 问题的 OP(和回答者)(在所有答案都落在这个问题上很久之后我发现),这个问题可能是重复的:

提交了一份关于 GCC 的错误报告,但尚未被认领/解决:


(以下所有 ISO 标准参考均参考 N4659:2017 年 3 月后 Kona 工作草案/C++17 DIS

标准参考

即使 [temp.res]/9 声明 [extract, emphasis mine]:

[温度]/9查找模板中使用的名称的声明时 definition,通常的查找规则([basic.lookup.unqual], [basic.lookup.argdep]) 用于非依赖名称。查找 依赖于模板参数的名称被推迟到 实际的模板参数是已知的 ([temp.dep])。[ 示例: ... ] [...]

[temp.dep.res]/1 很清楚,只有在模板定义点可见的声明才会被考虑用于非限定(依赖)名称查找 [强调我的]:

[temp.dep.res]/1在解析依赖名称时,来自以下来源的名称包括 考虑:

  • (1.1) 在模板定义点可见的声明。
  • (1.2) 来自与函数参数类型相关的命名空间的声明,两者都来自实例化上下文 ([temp.point]) 并从定义上下文中。

在 [temp.dep.candidate]/1 [强调我的]中重复的事实:

[临时候选人]/1对于后缀表达式是依赖名称的函数调用, 候选函数是使用通常的查找规则找到的 ([basic.lookup.unqual], [basic.lookup.argdep]) 除了:

  • (1.1) 对于使用非限定名称查找的查找部分,只能找到模板定义上下文中的函数声明
  • (1.2) 对于使用关联命名空间 ([basic.lookup.argdep]) 的查找部分,仅在 模板定义上下文或模板实例化上下文 被找到。

其中使用措辞模板定义上下文而不是模板的定义点,但这些是等效的。

根据 [namespace.udir]/2 [强调我的]:

using 指令指定指定命名空间中的名称可以在 using 指令出现在 using 指令之后的作用域中使用。在非限定名称查找期间,名称看起来就像在最近的封闭命名空间中声明一样,该命名空间同时包含 using 指令和指定的命名空间。[注:在此上下文中,“包含”是指“直接或间接包含”。

将 using 指令放在函数模板的定义点之后,相当于简单地在同一定义点之后声明一个名称,并且正如预期的那样,以下修改后的示例被 Clang 拒绝,但被 GCC 接受:f

struct Dummy{};

namespace ns_f {
    template <typename T>
    void f() { 
        (void)(!T{});
    }

    template <typename T>
    bool operator!(T) { return true; } 
}  // namespace ns_f

int main() {
    ns_f::f<Dummy>();
    return 0;
} 

最后,请注意,上面 [temp.dep.candidate]/1 中的 ADL (1.2) 在这里不适用,因为 ADL 不会继续封闭作用域。


非依赖结构可以在没有实例化的情况下被诊断

补充说明这里重要的一点是,这是一个模板函数。如果没有模板参数,代码既不能用 gcc 编译,也不能用 clang 编译。f

如果要制作成一个非模板类,比如说A

struct A { int x; };

然后 [temp.res]/8.3 适用,并且程序格式不正确,无需诊断:

[温度]/8知道哪些名称是类型名称后,可以检查每个模板的语法。如果出现以下情况,则程序格式不正确,无需诊断:

[...]

(8.3) 由于构造不依赖于模板参数,紧随模板定义之后的模板的假设实例化格式不正确,或者

[...]

评论

0赞 bartop 6/23/2020
看来我错了。C++有时很复杂
0赞 dfrib 6/23/2020
@bartop Tbh 我才意识到我实际上不知道 Clang 是否正确。根据 [temp.res]/9,应将依赖名称查找(包括依赖调用的非限定查找)推迟到已知模板参数为止。在 in 的调用站点上,命名空间已使用命名空间进行扩展,因此,不合格的查找应找到依赖运算符重载。operator+=impl::f();main()impltools
0赞 dfrib 6/23/2020
@Oliv 关于模板的定义点模板定义上下文之间可能存在的显着差异,您有什么想法吗?据我所知,此上下文中的非限定查找应推迟到已知模板参数为止,因此,在 中的调用站点已扩展为 ,此时从定义上下文中,依赖名称应找到 中的重载。我想知道 [temp.point] 在这里是否相关。main()impltoolsfoperator+=tools
0赞 Oliv 6/23/2020
@dfri 不,没有显着差异。新的C++20标准更清晰,也许你应该看看它。从模板定义上下文执行非限定名称查找。对于 ADL 部分,执行查找的上下文并不重要:eel.is/c++draft/temp.dep.candidate#1.sentence-3
0赞 dfrib 6/23/2020
@Oliv谢谢。根据我的阅读,模板定义上下文应该只是空间的(“查找空间”),因此 [temp.res]/9 的依赖名称查找延迟规则适用于此空间上下文,在这种情况下,OPs 示例中的程序应该格式正确。
7赞 Oliv 6/23/2020 #3

代码的格式不正确,因为在模板定义上下文中执行了不依赖于参数的非限定名称查找部分。所以 Clang 是对的,并且已经报告了 GCC 错误(错误 #70099)

接下来是冗长的解释。

在示例代码中,必须标记一些地方,以便进行讨论:

namespace impl {
  template <typename U = int>
  void f() {                       // (1) point of definition of the template f
    A<U> a{3};
    A<U> b{2};
    a += b;                        //  call operator += with arguments of dependent type A<U> 
    std::cout << a.x << std::endl;
  }
}

namespace impl {
  using namespace tools;          // using directive     
}

int main()
{
  impl::f();
}                                 // (2) point of instantiation of impl::f<int>

在模板 (1) 的定义中,对运算符 += 的调用是使用 类型的参数执行的。 是依赖类型因此也是依赖名称fA<U>A<U>operator +=

[temp.dep.res]/1 描述如何查找:operator +=

对于后缀表达式是依赖名称的函数调用,可以使用模板定义上下文([basic.lookup.unqual], [basic.lookup.argdep]) 中的常用查找规则找到候选函数。[注意:对于使用关联命名空间([basic.lookup.argdep])的查找部分,此查找将找到在模板实例化上下文中找到的函数声明,如[basic.lookup.argdep]中所述。[...]

执行了两个查找。

与参数无关的非限定名称查找 [basic.lookup.unqual]。

此查找是从模板定义上下文执行的。“从模板定义上下文”是指模板定义点的上下文。术语“上下文”是指查找上下文。如果模板首先在命名空间中声明,然后在全局命名空间范围内定义,则非限定名称查找仍将查找命名空间的成员。这就是为什么规则 [temp.dep.res]/1 使用“模板定义上下文”而不是简单的“模板定义点”的原因。fimplimpl

此查找是从 (1) 执行的,它找不到 在命名空间中定义 。using 指令的出现晚于 (1),并且不起作用。operator +=tools

参数相关名称查找 (ADL) [basic.lookup.argdep]

ADL 在实例化点 (2) 执行。所以它是在 using 指令之后实现的。尽管如此,ADL 仅考虑与参数类型关联的命名空间。参数具有 type ,模板是全局命名空间的成员,因此 ADL 只能找到此命名空间的成员。A<int>A

在 (2) 处,全局命名空间作用域中没有声明。因此,ADL 也无法找到 .operator +=operator +=