5

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?

L. F.
  • 19,445
  • 8
  • 48
  • 82
Jakub Klinkovský
  • 1,248
  • 1
  • 12
  • 33
  • Please note that clang is not the only one rejecting your code: https://godbolt.org/z/bLp_i3 – Bob__ Aug 02 '19 at 09:01
  • That's interesting, but are they right or wrong? – Jakub Klinkovský Aug 02 '19 at 09:12
  • I *guess* they are right. You could write this https://wandbox.org/permlink/PPXl1iWpxO0icf3S – Bob__ Aug 02 '19 at 09:16
  • For your previous comment, note that the problem with icc can be fixed by adding another template parameter to make the "signatures" different: https://godbolt.org/z/REouFh It still does not work with clang or msvc though. – Jakub Klinkovský Aug 02 '19 at 09:32
  • I *think* this is related too: https://stackoverflow.com/questions/15427667/sfinae-working-in-return-type-but-not-as-template-parameter – Bob__ Aug 02 '19 at 09:44
  • Related: https://stackoverflow.com/q/34158902. The problem is more subtle here because `operator=` cannot have a default argument. – L. F. Aug 02 '19 at 10:44
  • Also, what is the stray template parameter pack `typename...` supposed to do here? They are always deduced as empty, so why? – L. F. Aug 02 '19 at 10:48
  • `typename...` prevents users to bypass SFINAE with something like `obj.template operator=(2)`. I admit it's more useful for normal functions rather than operators and not relevant here. – Jakub Klinkovský Aug 02 '19 at 11:09
  • @JakubKlinkovský Wow that's so clever! If that is the case, you can use `std::enable_if_t<..., int> = 0` instead. – L. F. Aug 02 '19 at 11:42
  • 1
    using `std::enable_if_t::value, int> = 0>` (versus `typename = std::enable_if_t::value>`) would avoid hijacking too (and in addition allows overloads) – Jarod42 Aug 02 '19 at 11:43
  • Related to [are-type-aliases-used-as-type-of-function-parameter-part-of-the-function-signature](https://stackoverflow.com/questions/46805276/are-type-aliases-used-as-type-of-function-parameter-part-of-the-function-signatu/) – Jarod42 Aug 02 '19 at 11:45
  • @Jarod42 What do you mean by "in addition allows overloads"? – Jakub Klinkovský Aug 02 '19 at 12:15
  • I strongly suggest you get into the habit of using SINAE like `template = true> return_type function_name(params)`. That lets you write overload sets like `template , bool> = true> return_type function_name(params)` `template , bool> = true> return_type function_name(params)` since default template values are part of the template signature. – NathanOliver Aug 02 '19 at 12:43
  • @NathanOliver Can you avoid writing the `std::enable_if_t` part in two different places when you want to write the definition of a member function outside the class declaration? – Jakub Klinkovský Aug 02 '19 at 13:42
  • @JakubKlinkovský I don't believe so. – NathanOliver Aug 02 '19 at 13:44
  • @NathanOliver So I don't think it's a good universal habit. Thanks for the tip though, I'll be considering it for plain functions. – Jakub Klinkovský Aug 02 '19 at 13:51

2 Answers2

5

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.
n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • "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)." Can you tell me how the "parameter-type-list" is determined for function templates? Does template argument deduction take place? – L. F. Aug 02 '19 at 10:00
  • 1
    @L.F. This seems to be a wording defect, see the paragraph abov "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". A template can hide another template. A template *instantiation* cannot hide another template instantiation. – n. m. could be an AI Aug 02 '19 at 10:06
  • I think this nicely explains why GCC accepts the code so I'm going to accept the answer. Since the standard should be improved, can you report the problem? – Jakub Klinkovský Aug 02 '19 at 12:32
2

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.

L. F.
  • 19,445
  • 8
  • 48
  • 82
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/197381/discussion-between-l-f-and-kostasrim). – L. F. Aug 02 '19 at 10:59
  • This sounds reasonable, but I'm not getting how it works yet. You say "After template argument deduction" and then "Template argument deduction is not carried out yet"... – Jakub Klinkovský Aug 02 '19 at 11:12
  • @JakubKlinkovský Sorry, I messed up with that. That "After template argument deduction" should be deleted. – L. F. Aug 02 '19 at 11:13
  • Another nitpick is that since name lookup happens before template argument deduction, the parameter-type-list should not be `int`, but something like `typename U`. I think this is why @n.m. says that the standard is unclear... – Jakub Klinkovský Aug 02 '19 at 11:39
  • IMO, standard is clear, (but "wrong" to ignore template argument list (unrelated to default template argument BTW)) `B::operator=(U v)` hides `A::operator=(U v)`. We should ignore return type differences (or at least covariant return type). – Jarod42 Aug 02 '19 at 11:52
  • @Jarod42 The problem, I guess, is that "parameter-type-list" is about functions, and a function template by itself is not a function, so it doesn't make much sense to talk about the parameter type list of a function template when instantiation is not relevant. – L. F. Aug 02 '19 at 11:54
  • `using` brings names, They might have formulated differently for the "special" case of function template. a simple *"and for member function templates, also the template-argument-list"* would do the job IMO. but doing it now is more complicated to avoid breaking changes. – Jarod42 Aug 02 '19 at 12:01