Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I have this code which works as expected with 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;
}

(In practice we would do something fancy in the B::operator= and even use different type traits for enable_if, but this is the simplest reproducible example.)

The problem is thtat Clang 8.0.1 gives an error, somehow the operator= from the parent class is not considered, although the child has 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.

Which compiler is right according to the standard? (I'm compiling with -std=c++14.) How should I change the code to make it correct?

like image 247
Jakub Klinkovský Avatar asked Aug 02 '19 08:08

Jakub Klinkovský


2 Answers

Consider this simplified code:

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

This prints 2 as it should with both compilers.

If the derived class already has one with the same signature, then it hides or overrides the one brought in by the using declaration. The signatures of your assignment operators are ostensibly the same. Consider this fragment:

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) {}

This causes a redefinition error for bar with both compilers.

HOWEVER if one changes the return type in one of the templates, the error goes away!

It's time to look at the standard closely.

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.
like image 138
n. 1.8e9-where's-my-share m. Avatar answered Nov 14 '22 23:11

n. 1.8e9-where's-my-share m.


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


Per [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).

The operator= declared in the derived class B has exactly the same name, parameter-type-list, cv-qualification (none), and ref-qualifier (none) as the one declared in A. Therefore, the one declared in B hides the one in A, and the code is ill-formed because overload resolution does not find an appropriate function to call. However, template parameter lists are not addressed here.

So should they be considered? This is where the standard becomes unclear. A and B are considered to have the same (template) signature by Clang, but not by GCC. n.m.'s answer points out that the real issue actually lies on the return type. (Default template arguments are never considered when determining a signature.)

Note that this is decided on name lookup. Template argument deduction is not carried out yet, and neither is substitution. You can't say "oh, deduction / substitution failed, so let's go add more members to the overload set". So SFINAE doesn't make a difference here.

like image 35
L. F. Avatar answered Nov 14 '22 21:11

L. F.