C++ Template Specialization and Subclassing

2020-07-13 07:29发布

I was wondering if it is possible to have a template specialization accept a class and its subclasses. Like so:

class A {};

class B : public A {};

template <typename T>
void foo(const T& t) {
  printf("T");
}

template <>
void foo(const A& t) {
  printf("A");
}

int main(int argc, char** argv) {
  B b;
  foo(b);

  return 0;
}

Currently it output 'T' because b doesn't have its own template specialization, so it defaults to printing 'T'. I was wondering if it was possible for B to use the template specialization of A since B is a subclass of A. Or is that just not a thing?

Note: Because of some requirement, I can't use copy/move.

Note: I would also prefer if I didn't need to change A or B, but lets see what is possible first.

4条回答
我欲成王,谁敢阻挡
2楼-- · 2020-07-13 08:16

There may be a cleaner way to do this. But if you change the actual implementation of foo to a SFINAE functionoid object like std::hash, you can keep your default overload without polluting it with all the potential overload conditions. (Credit Arthur O'Dwyer's blog).

class A {};

class B : public A {};

template <typename T, typename Enable = void>
struct FooImpl {
    static void foo(const T& a) {
        printf("B");
    }
};

template <typename T>
struct FooImpl<T, std::enable_if_t<std::is_base_of_v<A, T>>> {
static void foo(const T& a) {
    printf("A");
}
};

template <typename T>
void foo(const T& t) {
    FooImpl<T>::foo(t);
}
查看更多
时光不老,我们不散
3楼-- · 2020-07-13 08:26

How about this:

foo(static_cast<A&>(b));
查看更多
祖国的老花朵
4楼-- · 2020-07-13 08:28

It is definitely possible and there is no need for
-copy/move,
-change in the classes,
-change in the body of the functions (including but not limited to calling another class template static function),
-change in the return type/s of the function/s,
-or even using a constant expression as the return type/s of the function/s!

Solution

It is just needed to have the general function as a function template which employes SFINAE technique in form of default template type/s argument for extra template type parameter/s in order to avoid the special cases and to have the specialization/s as normal function/s:

template <typename T, typename = std::enable_if_t<!std::is_base_of_v<A, T> > >
void foo(const T& t) {
    printf("T");
}
void foo(const A& t) {
    printf("A");
}


Explanation:

For general cases the default template type argument std::enable_if_t<!std::is_base_of_v<A, T> > > can be deduced from the first template type argument T. Because It exists and it is well defined, function template will be called.

When the function is called with an object of a type based on class A because std::enable_if_t<!std::is_base_of_v<A, T> > > is not defined the default template type argument does not exists hence template type parameter can not be deduced. So compiler would look for another functions with the same name and similar parameter type so the normal function void foo(const A& t) { printf("A");} would be called and there will be no ambiguity.

Notes on Usage

For a new specializations simply add one more similar pseudo class to the (one) function template and write a function(non-template) for the new specialization.

If the template default type argument looks big and confusing one can simply create a policy template and use that instead. Like:

template<typename T, typename P>
using exclude =  std::enable_if_t<!std::is_base_of_v<P, T> >;
template <typename T, typename = exclude<T,A> >
void foo(const T& t) {
    printf("T");
}

Also since before C++17 certain features are not enabled, for lower C++ versions, one can write the template like:

template <typename T, typename  = typename std::enable_if<!std::is_base_of<A, T>::value>::type>

I addition if one chooses to use solution of @songyuanyao which uses a constant expression as the return type of the functions, If the return type of the functions is not void, for example return_type, The solution becomes like:

template <typename T>
std::enable_if_t<!std::is_base_of_v<A, T>, return_type> foo(const T& t) {
  printf("T");
  return_type return_value;
  return return_value;
}

template <typename T>
std::enable_if_t<std::is_base_of_v<A, T>, return_type> foo(const T& t) {
  printf("A");
  return_type return_value;
  return return_value;
}

Further Example

Finally for a better understanding of SFINAE One can consider a not-generally-correct/not-all-encompassing alternative solution which does not require any library:

template<bool>
struct ifnot;
template<>
struct ifnot<false> {
    enum {v};
};
template<typename T, typename P>
struct test {
    static T value_of_T();
    static char check(...);
    static int check(P);
    enum {v = sizeof(check(value_of_T())) - 1};
};

template <typename T, bool = ifnot<test<T, A>::v>::v>
void foo(const T& t) {
    printf("T");
}
void foo(const A& t) {
    printf("A");
}

Although this solution also works for this specific example, please note that this solution is not always correct. Because it only tests conversion of T to A (which even in itself is not complete and problematic) not the inheritance. Specially for these kind of functions which are supposed to be called with objects of similar types, there is a great chance that many of these types would be convertible to each other!

I think the proper way for an inheritance test includes non-mutually conversion tests and determining if none of the types is void*. All considered It is much better to use std::is_base_of or std::is_base_of_v. However The struct ifnot is OK and one can even exchange std::enable_if for it with the proper changes in their usage.

Good luck!

查看更多
狗以群分
5楼-- · 2020-07-13 08:30

The probelm is, the primary template is an exact match when T being deduced as B; it's a better match than the specialization.

You can use template overloading instead; with SFINAE.

template <typename T>
std::enable_if_t<!std::is_base_of_v<A, T>> foo(const T& t) {
  printf("T");
}

template <typename T>
std::enable_if_t<std::is_base_of_v<A, T>> foo(const T& t) {
  printf("A");
}

LIVE

查看更多
登录 后发表回答