【问题标题】:Inheriting templated operator= in C++14: different behaviour with g++ and clang++在 C++14 中继承模板化 operator=:g++ 和 clang++ 的不同行为
【发布时间】:2019-08-02 08:29:09
【问题描述】:

我的这段代码可以在 GCC 9.1 中正常工作:

#include <type_traits>

template< typename T >
class A
{
protected:
    T value;

public:
    template< typename U,
              typename...,
              typename = std::enable_if_t< std::is_fundamental< U >::value > >
    A& operator=(U v)
    {
        value = v;
        return *this;
    }
};

template< typename T >
class B : public A<T>
{
public:
    using A<T>::operator=;

    template< typename U,
              typename...,
              typename = std::enable_if_t< ! std::is_fundamental< U >::value > >
    B& operator=(U v)
    {
        this->value = v;
        return *this;
    }
};

int main()
{
    B<int> obj;
    obj = 2;
}

(实际上,我们会在B::operator= 中做一些花哨的事情,甚至对enable_if 使用不同的类型特征,但这是最简单的可重现示例。)

问题是 Clang 8.0.1 给出了一个错误,不知何故,来自父类的 operator= 不被考虑,虽然孩子有 using A&lt;T&gt;::operator=;:

test.cpp:39:9: error: no viable overloaded '='
    obj = 2;
    ~~~ ^ ~
test.cpp:4:7: note: candidate function (the implicit copy assignment operator) not viable:
      no known conversion from 'int' to 'const A<int>' for 1st argument
class A
      ^
test.cpp:4:7: note: candidate function (the implicit move assignment operator) not viable:
      no known conversion from 'int' to 'A<int>' for 1st argument
class A
      ^
test.cpp:20:7: note: candidate function (the implicit copy assignment operator) not
      viable: no known conversion from 'int' to 'const B<int>' for 1st argument
class B : public A<T>
      ^
test.cpp:20:7: note: candidate function (the implicit move assignment operator) not
      viable: no known conversion from 'int' to 'B<int>' for 1st argument
class B : public A<T>
      ^
test.cpp:28:8: note: candidate template ignored: requirement
      '!std::is_fundamental<int>::value' was not satisfied [with U = int, $1 = <>]
    B& operator=(U v)
       ^
1 error generated.

根据标准,哪个编译器是正确的? (我正在使用-std=c++14 进行编译。)我应该如何更改代码以使其正确?

【问题讨论】:

  • 请注意,clang 不是唯一拒绝您的代码的人:godbolt.org/z/bLp_i3
  • 这很有趣,但它们是对还是错?
  • 我猜他们是对的。你可以写这个wandbox.org/permlink/PPXl1iWpxO0icf3S
  • 对于您之前的评论,请注意,icc 的问题可以通过添加另一个模板参数以使“签名”不同:godbolt.org/z/REouFh 它仍然不适用于 clang 或 msvc。
  • 使用std::enable_if_t&lt;cond&lt;T&gt;::value, int&gt; = 0&gt;(相对于typename = std::enable_if_t&lt;cond&lt;T&gt;::value&gt;)也可以避免劫持(并且还允许重载)

标签: c++ c++14 language-lawyer overload-resolution name-hiding


【解决方案1】:

考虑这个简化的代码:

#include <iostream>

struct A
{
    template <int n = 1> void foo() { std::cout << n; }
};

struct B : public A
{
    using A::foo;
    template <int n = 2> void foo() { std::cout << n; }
};

int main()
{
    B obj;
    obj.foo();
}

这两个编译器都应该打印 2。

如果派生类已经有一个具有相同签名的类,那么它会隐藏或覆盖using 声明引入的那个。您的赋值运算符的签名表面上是相同的。考虑这个片段:

template <typename U, 
          typename = std::enable_if_t<std::is_fundamental<U>::value>>
void bar(U) {}
template <typename U, 
          typename = std::enable_if_t<!std::is_fundamental<U>::value>>
void bar(U) {}

这会导致两个编译器对 bar 的重新定义错误。

但是,如果更改其中一个模板中的返回类型,错误就会消失!

是时候仔细看看标准了。

当 using-declarator 将声明从基类引入派生类时,成员函数和 派生类中的成员函数模板覆盖和/或隐藏成员函数和成员函数 具有相同名称、参数类型列表 (11.3.5)、cv-qualification 和 ref-qualifier(如果有)的模板 基类(而不是冲突的)。这种隐藏或覆盖的声明被排除在集合之外 using-declarator 引入的声明

现在,就模板而言,这听起来很可疑。如果不比较模板参数列表,怎么可能比较两个参数类型列表?前者取决于后者。确实,上面一段说:

如果命名空间范围或块范围内的函数声明与 using 声明引入的函数具有相同的名称和相同的参数类型列表 (11.3.5),并且声明未声明 同样的功能,程序格式错误。如果命名空间范围内的函数模板声明具有相同的 名称、参数类型列表、返回类型和模板参数列表作为由 a 引入的函数模板 using-declaration,程序格式错误。

这更有意义。如果两个模板的模板参数列表相同,那么两个模板以及其他所有内容都是相同的……但是等等,这包括返回类型!如果两个模板的名称和签名中的所有内容相同,则两个模板相同,包括返回类型(但不包括默认参数值)相同。然后一个可以与另一个发生冲突或隐藏另一个。

那么,如果我们改变 B 中赋值运算符的返回类型并使其与 A 中的相同,会发生什么? GCC 停止接受代码。

所以我的结论是这样的:

  1. 当涉及到隐藏使用声明带来的其他模板的模板时,该标准尚不清楚。如果它意味着从比较中排除模板参数,它应该这样说,并澄清可能的含义。例如,函数可以隐藏函数模板,反之亦然?无论如何,命名空间范围内的using 和将基类名称带入派生类的using 之间的标准语言存在无法解释的不一致。
  2. GCC 似乎在命名空间范围内采用using 的规则并将其应用到基类/派生类的上下文中。
  3. 其他编译器执行其他操作。不太清楚到底是什么;可能在不考虑模板参数(或返回类型)的情况下比较参数类型列表,正如标准的字母所说,但我不确定这是否有意义。

【讨论】:

  • "当 using 声明将名称从基类带入派生类范围时,派生类中的成员函数和成员函数模板会覆盖和/或隐藏具有相同名称的成员函数和成员函数模板基类中的名称、参数类型列表 ([dcl.fct])、cv 限定符和 ref 限定符(如果有)(而不是冲突)。”你能告诉我函数模板的“参数类型列表”是如何确定的吗?是否进行模板参数推导?
  • @L.F.这似乎是一个措辞缺陷,请参阅上面的段落“如果命名空间范围内的函数模板声明与 using-declaration 引入的函数模板具有相同的名称、参数类型列表、返回类型和模板参数列表,该程序格式不正确”。一个模板可以隐藏另一个模板。一个模板实例化不能隐藏另一个模板实例化。
  • 我认为这很好地解释了为什么 GCC 接受代码,所以我将接受答案。既然标准有待提高,请问可以报告问题吗?
【解决方案2】:

注意:我觉得这个答案是错误的,n.m.'s answer 是正确的。一世 会保留这个答案,因为我不确定,但请去检查 那个答案。


每[namespace.udecl]/15:

当 using-declaration 将名称从基类带入 派生类作用域、成员函数和成员函数模板 派生类覆盖和/或隐藏成员函数和成员 同名函数模板,参数类型列表 ([dcl.fct])、cv-qualification 和 ref-qualifier(如果有的话)在基础中 类(而不是冲突)。

在派生类B 中声明的operator= 与一个在A 中声明。因此,B 中声明的那个隐藏了A 中的那个,并且代码格式错误,因为重载解析找不到合适的函数来调用。但是,模板参数列表在这里没有涉及。

那么应该考虑它们吗?这就是标准变得不明确的地方。 A 和 B 被 Clang 认为具有相同的(模板)签名,但不是 GCC。 n.m.'s answer 指出真正的问题实际上在于返回类型。 (在确定签名时从不考虑默认模板参数。)

请注意,这取决于名称查找。模板参数推导还没有进行,替换也没有。 You can't say "oh, deduction / substitution failed, so let's go add more members to the overload set". 所以 SFINAE 在这里并没有什么不同。

【讨论】:

  • 这听起来很合理,但我还不知道它是如何工作的。你说“模板参数推导后”,然后“模板参数推导还没有进行”...
  • @JakubKlinkovský 对不起,我搞砸了。应该删除“模板参数推导后”。
  • 另一个挑剔的是,由于名称查找发生在模板参数推导之前,参数类型列表不应该是int,而是类似于typename U。我认为这就是为什么@n.m。说标准不清楚……
  • IMO,标准很明确,(但忽略模板参数列表是“错误的”(与默认模板参数无关))B::operator=(U v) 隐藏A::operator=(U v)。我们应该忽略返回类型差异(或至少是协变返回类型)。
猜你喜欢
  • 2016-06-19
  • 1970-01-01
  • 1970-01-01
  • 2018-07-21
  • 1970-01-01
  • 2013-12-14
  • 2018-10-16
  • 1970-01-01
  • 2018-01-06
相关资源
最近更新 更多