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

Inheriting templated operator= in C++14: different behaviour with g++ and clang++

我有这段代码可以在 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 给出了一个错误,尽管 child 有 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,  = <>]
    B& operator=(U v)
       ^
1 error generated.

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

Note: I feel that this answer is wrong and is the correct one. I will keep this answer because I am not sure, but please go and check that answer.


根据 [namespace.udecl]/15:

When a using-declaration brings names from a base class into a derived class scope, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list ([dcl.fct]), cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting).

派生class中声明的operator= B具有完全相同的名称、参数类型列表、cv限定(none)和ref-qualifier (none) 作为在 A 中声明的那个。因此,B 中声明的隐藏了 A 中的代码,并且代码格式错误,因为重载解析没有找到合适的函数来调用。但是,模板参数列表未在此处解决。

那么应该考虑他们吗?这是标准变得不清楚的地方。 AB 被 Clang 认为具有相同的(模板)签名,但 GCC 不认为。 指出真正的问题实际上在于 return 类型。 (确定签名时从不考虑默认模板参数。)

请注意,这是由名称查找决定的。模板参数推导还没有进行,替换也没有。 所以 SFINAE 在这里没有区别。

考虑这个简化的代码:

#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。

如果派生的 class 已经有一个具有相同签名的,那么它将隐藏或覆盖由 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 的重新定义错误。

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

是时候仔细看看标准了。

When a using-declarator brings declarations from a base class into a derived class, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list (11.3.5), cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting). Such hidden or overridden declarations are excluded from the set of declarations introduced by the using-declarator

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

If a function declaration in namespace scope or block scope has the same name and the same parameter-type-list (11.3.5) as a function introduced by a using-declaration, and the declarations do not declare the same function, the program is ill-formed. If a function template declaration in namespace scope has the same name, parameter-type-list, return type, and template parameter list as a function template introduced by a using-declaration, the program is ill-formed.

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

那么,如果我们将 B 中的赋值运算符的 return 类型更改为与 A 中的相同,会发生什么? GCC 停止接受代码

所以我的结论是:

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