在C ++ 14中继承模板化的operator =:g ++和clang ++的不同行为

Jak*_*ský 5 c++ language-lawyer overload-resolution name-hiding c++14

我有这段代码可以在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;
}
Run Code Online (Sandbox Code Playgroud)

(在实践中,我们会花一些心思B::operator=,甚至为使用不同的类型特征enable_if,但这是最简单的可重现示例。)

问题是Clang 8.0.1给出了一个错误operator=,尽管孩子有using A<T>::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.
Run Code Online (Sandbox Code Playgroud)

哪个编译器符合标准?(我正在使用进行编译-std=c++14。)如何更改代码以使其正确?

n. *_* m. 5

考虑以下简化代码:

#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();
}
Run Code Online (Sandbox Code Playgroud)

这将在两个编译器上均显示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) {}
Run Code Online (Sandbox Code Playgroud)

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

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

现在是时候仔细研究一下标准了。

当using-declarator将基类中的声明带入派生类时,派生类中的成员函数和成员函数模板将覆盖和/或隐藏具有相同名称,参数类型列表(11.3)的成员函数和成员函数模板。 5),cv限定符和ref限定符(如果有)在基类中(而不是冲突)。此类隐藏或覆盖的声明不包含在using-declarator引入的声明集中

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

如果名称空间范围或块范围中的函数声明与using-声明引入的函数具有相同的名称和相同的parameter-type-list(11.3.5),且声明未声明相同的函数,则该程序为格式错误。如果名称空间范围内的功能模板声明与using-声明引入的功能模板具有相同的名称,parameter-type-list,返回类型和模板参数列表,则程序格式错误。

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

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

所以我的结论是这样的:

  1. 当模板隐藏使用声明带来的其他模板时,标准尚不清楚。如果要从比较中排除模板参数,则应这样说,并阐明可能的含义。例如,函数可以隐藏函数模板,反之亦然吗?无论如何,using在命名空间范围内,标准语言之间存在无法解释的不一致,using这将基类名称引入派生类。
  2. GCC似乎using在命名空间范围内采用了规则,并将其应用于基类/派生类的上下文中。
  3. 其他编译器做其他事情。目前尚不清楚确切的内容。可能会比较参数类型列表而无需考虑模板参数(或返回类型),如标准文字所言,但我不确定这是否有意义。