5

我有这个代码可以按预期与 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<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.

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

4

2 回答 2

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();
}

两个编译器都应该打印 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这会导致两个编译器的重新定义错误。

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

是时候仔细研究标准了。

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

Now this sounds dubious as far as templates are concerned. How could one even compare two parameter type lists without comparing template parameter lists? The former depends on the latter. Indeed, a paragraph above says:

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.

This makes much more sense. Two templates are the same if their template parameter lists are the same, along with everything else... but wait, this includes the return type! Two templates are the same if their names and everything in their signatures, including the return types (but not including default parameter values) is the same. Then one can conflict with or hide the other.

So what happens if we change the return type of the assignment operator in B and make it the same as in A? GCC stops accepting the code.

So my conclusion is this:

  1. The standard is unclear when it comes to templates hiding other templates brought by using declarations. If it meant to exclude template parameters from comparison, it should have said so, and clarify the possible implications. For example, can a function hide a function template, or vise versa? In any case there's an unexplained inconsistency in the standard language between using in namespace scope and using that brings base class names to the derived class.
  2. GCC seems to take the rule for using in namespace scope and apply it in the context of base/derived class.
  3. Other compilers do something else. It is not too clear what exactly; possibly compare the parameter type lists without considering the template parameters (or return types), as the letter of the standard says, but I'm not sure this makes any sense.
于 2019-08-02T09:56:21.353 回答
2

注意:我觉得这个答案是错误的,nm 的答案是正确的。我会保留这个答案,因为我不确定,但请去检查那个答案。


根据[namespace.udecl]/15

using-declaration将基类中的名称带入派生类作用域时,派生类中的成员函数和成员函数模板会覆盖和/或隐藏具有相同名称的成员函数和成员函数模板,parameter-type-list ([ dcl.fct])、cv-qualification 和ref-qualifier(如果有)在基类中(而不是冲突)。

operator=派生类中声明的名称B、参数类型列表、cv 限定符(无)和引用限定符(无)与A. 因此, in 中声明的B那个隐藏了 in中的那个A,并且代码格式错误,因为重载决议没有找到合适的函数来调用。然而,模板参数列表在这里没有涉及。

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

请注意,这取决于名称查找。模板参数推导还没有进行,替换也没有。 您不能说“哦,扣除/替换失败,所以让我们向重载集添加更多成员”。 所以 SFINAE 在这里并没有什么不同。

于 2019-08-02T09:39:30.253 回答