How to specialize a template class method for a specific type?
Asked Answered
K

3

6

I have codes like this:

class Bar {
 public:
  void print() {
    std::cout << "bar\n";
  }
};

template<typename T>
class Foo {
 public:
  template <typename std::enable_if<std::is_base_of<T,Bar>::value,T>::type>
  void print() {
    t.print();
  }

 template <typename>
  void print() {
    std::cout << t << std::endl;
  }
 private:
  T t;
};

int main() {
//  Foo<int> foo1;
  Foo<Bar> foo2;
  foo2.print();
}

The purpose of this code is that: If the T t is a Bar or a subclass of Bar, then foo.print() is deduced to void print() {t.print();}, otherwise deduced to void print() {std::cout << t << std::endl;}, but things didn't work as I expect. The compiler errors:

"a non-type template parameter cannot have type 'typename std::enable_if::value, Bar>::type' (aka 'Bar')",

What's wrong with this code?

Kevinkevina answered 19/7, 2019 at 3:17 Comment(3)
After template <typename , an identifier is expected. As in template <typename T> .Your definition of print is not valid C++.Protist
@IgorTandetnik, I changed ...::type> to ...::type* = nullprt> and it works, but I don't know why write like this.Kevinkevina
You can't name a default for a typename template parameter without an =.Twoseater
C
6
  1. You should make both the overloading of print() to function template (to make SFINAE working), otherwise the non-template function is always preferred.

  2. You should let print() taking its own template type parameter; type cheking shouldn't be performed on the class template parameter T directly, function templates overload resolution and SFINAE are performed on the function templates themselves, the class template doesn't involve in.

  3. You can move the part of std::enable_if to the return type.

  4. You should change the order specified to std::is_base_of (i.e. std::is_base_of<Bar, X>, not std::is_base_of<X, Bar>) if you want the type to be Bar or the derived class of Bar.

e.g.

template <typename X = T>
typename std::enable_if<std::is_base_of<Bar, X>::value>::type print() {
  t.print();
}

template <typename X = T>
typename std::enable_if<!std::is_base_of<Bar, X>::value>::type print() {
  std::cout << t << std::endl;
}

LIVE

Cowfish answered 19/7, 2019 at 3:24 Comment(10)
what if I want keep my function returning void? Or in other cases the return type is fixed?Kevinkevina
@reavenisadesk If you don't specify, the return type is void (as my sample code). If you want other types, you can specify explicitly to std::enable_if, anyway, the type is fixed.Cowfish
Ahh, you came to the same conclusion I did. I see now. I was initially confused because you made it look as if they were standalone functions.Twoseater
Hi, another question, in your example, if I change X to T in std::is_base_of<X,Bar>, the code will not compile, what's the difference? Since typename X = T?Kevinkevina
@reavenisadesk std::enable_if should depend on the template parameter of the function template print()'s own; not the one of the class template Foo.Cowfish
Does that mean template don't "share" template parameters?Kevinkevina
@reavenisadesk if enable_if doesn't depend on a template parameter of the print function, the compiler expands it immediately and that will result in an error in at least one case. Using the template parameter of the print function forces the compiler to wait to expand the return type until it tries to expand the whole print template. And since substitution failure is not an error, and there is a version that expanded correctly with no errors, the compiler doesn't flag it as an error.Twoseater
@reavenisadesk Function templates overload resolution and SFINAE are performed on the function templates themselves; the class template doesn't involve in.Cowfish
@Twoseater when you mention "expands it immiediately", does that mean the SNINAE is not gonna happen and there's no deduce at all?Kevinkevina
@reavenisadesk - There's no deduction at the time the function is called.Twoseater
Z
1

Since you are actually interested in wether a type has the member function print or has defined the operator<<, you should also constrain it this way.

With the upcoming C++20 standard, we are getting concepts & constraints. With that in mind, we can do the following:

namespace traits
{
template<typename T>
concept has_print_v = requires(T&& t) { t.print(); };

template<typename T>
concept has_ostream_op_v = requires(T&& t, std::ostream& os) { os << t; };
} // end of namespace traits

And use the concepts like this:

template<typename T>
class Foo
{
public:
    void print()
    {
        if constexpr (traits::has_print_v<T>) { t.print(); }
        else if constexpr (traits::has_ostream_op_v<T>) { std::cout << t << "\n"; }
    }
private:
    T t;  
};

LIVE DEMO

Ziegfeld answered 19/7, 2019 at 20:57 Comment(1)
Does any compiler support C++20 yet?Kevinkevina
W
0

You should be adding template parameter while calling print() as it's a template method itself. In any case, your design is overly complicated!

It becomes very simple with C++17, where you need only one Foo<T>::print() method.

void print() {
  if constexpr(std::is_base_of_v<T,Bar>) // ignores `Foo<int>`
    t.print();
}

Demo.

Wheen answered 19/7, 2019 at 3:36 Comment(2)
does if constexpr(std::is_base_of_v<T,Bar>) really generate a compare command?(asked in efficiency aspect)Kevinkevina
@reavenisadesk, it's very efficient, because if condition is evaluated during the compile time. This is as good as what you do using enable_if kind of type_traits, but very concise. In nutshell, if constexpr is a notebook example for your kind of query. :-)Wheen

© 2022 - 2024 — McMap. All rights reserved.