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?
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:
using
in namespace scope and using
that brings base class names to the derived class.using
in namespace scope and apply it in the context of base/derived class.