0

Let's say I have a class template

template <class Ta>
struct Base {};

And I've written some overloaded function templates around it:

template <class T1, class T2>
void f(Base<T1>&, T2){
    std::cout << "1" << std::endl;
}
template <class T1, class T2>
void f(Base<T1>&, Base<T2>&){
    std::cout << "2" << std::endl;
}

When I invoke the functions:

    Base<float> base1;
    Base<float> base2;
    f(base1,1);
    f(base1,base2);

I get exactly what I'd expect:

1
2

All is well, but now I want to create a derived class:

template <class Ta, class Tb> 
struct Derived : Base<Ta> {};

All of a sudden, when I run the same code on instances of the derived class:

    Derived<float,int> derived1;
    Derived<float,int> derived2;
    f(derived1,1);
    f(derived1,derived2);

I get a different result:

1
1

Apparently, the compiler interprets T2=Derived<Ta,Tb>, and does not interpret the alternate overload as having greater specificity.

What is the best way to clarify to the compiler that calls to f(derived1,derived2) should be interpreted as calls to f(Base<T1>, Base<T2>)?


Full minimal example

#include <iostream>

template <class Ta>
struct Base {};

template <class Ta, class Tb> 
struct Derived : Base<Ta> {};


template <class T1, class T2>
void f(Base<T1>&, T2){
    std::cout << "1" << std::endl;
}
template <class T1, class T2>
void f(Base<T1>&, Base<T2>&){
    std::cout << "2" << std::endl;
}

int main()
{
    Base<float> base1;
    Base<float> base2;
    f(base1,1);
    f(base1,base2);
    Derived<float,int> derived1;
    Derived<float,int> derived2;
    f(derived1,1);
    f(derived1,derived2);
}
16807
  • 1,418
  • 2
  • 18
  • 32
  • FYI: [SO: C++ template function for derived class with std::is_base_of](https://stackoverflow.com/q/42085326/7478597) – Scheff's Cat Aug 19 '20 at 09:09
  • FYI: [SO: How to allow derived classes in a C++ template argument](https://stackoverflow.com/q/30418542/7478597) – Scheff's Cat Aug 19 '20 at 09:12
  • 1
    Notice that, as you take by value, you will have object slicing. – Jarod42 Aug 19 '20 at 09:29
  • In my real world example, I'm passing by reference, but didn't notice I was doing that when I wrote the demo. I'll fix that. – 16807 Aug 19 '20 at 09:32

2 Answers2

4

You might use SFINAE to restrict one overload:

As your Base is template, require custom traits (else std::is_base_of might be used):

template <template <typename> class C, typename T>
std::true_type is_template_base_of_impl(const C<T>*);

template <template <typename> class C>
std::false_type is_template_base_of_impl(...);

template <template <typename> class C, typename T>
using is_template_base_of = decltype(is_template_base_of_impl<C>(std::declval<const T*>()));
template <class T1, class T2, std::enable_if_t<!is_template_base_of<BaseClass, T2>::value, int> = 0>
void f(Base<T1>, T2){
    std::cout << "1" << std::endl;
}
template <class T1, class T2>
void f(Base<T1>, Base<T2>){
    std::cout << "2" << std::endl;
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • This seems promising. There's some parts I need to read up on, but I assume it's checking to see if T2 was implemented from the `Base` template? – 16807 Aug 19 '20 at 09:56
  • BTW, the earlier edit to this post suggested maybe using `std::is_base_of_v`. I found out [here](http://coliru.stacked-crooked.com/a/49f346fd8efb6a3c) I could just as well write a nontemplate parent class to `Base`, though this solution doesn't work if you lack control of `Base`. I'm going to document this in a separate post but will mark this one accepted. – 16807 Aug 19 '20 at 10:12
  • @StPiere: `typename=std::enable_if_t<..>` can be hackjacked, and doesn't allow overloads with different conditions, `std::enable_if_t<.., int>=0` doesn't suffer from those defaults. – Jarod42 Aug 19 '20 at 12:22
0

Jarod42's answer got me looking into conditional compilation solutions and I found a valid solution using std::is_base_of. However, since Base is a template in this example, the solution requires creating a non-template parent class of Base:

struct Base2 {};

template <class Ta>
struct Base : Base2 {};

If this is done the rest of the solution is made a little easier:

template <class T1, class T2, std::enable_if_t<!std::is_base_of<Base2, T2>::value, int> = 0>
void f(Base<T1>, T2){
    std::cout << "1" << std::endl;
}
template <class T1, class T2>
void f(Base<T1>, Base<T2>){
    std::cout << "2" << std::endl;
}

Demo

However this only works if you have control over Base, whereas I think Jarod42's answer works in the general case.

16807
  • 1,418
  • 2
  • 18
  • 32