Search code examples
c++templatesenable-if

Make template class method parameter type same as class template parameter (including void)


I would like to make a class method have a signature that matches the class template parameter, e.g., for a Class<T>, there's Class<T>::Method(T t) if T is not void, and Class<T>::Method() if T is void.

Compiling the attempted code below, if T is void, then both methods exist. I'm not understanding why the same use of enable_if works for foo but not that version of print.

#include <iostream>
using namespace std;

template <typename T>
struct S
{
    template <typename U = T>
    std::enable_if_t<std::is_void_v<U>, void>
    print(void) { cout << "print void"  << endl; }

    template <typename U = T>
    std::enable_if_t<not(std::is_void_v<U>), void>
    print(U x) { cout << "print not void " << x << endl; }

    template <typename U = T>
    std::enable_if_t<not(std::is_void_v<U>), void>
    foo() {};

    // ... rest of class
};

int main()
{
    S<int> i;
    i.print(1);
    // i.print();  // is disabled

    S<void> v;
    v.print();
    v.print(2); // should be disabled but is not
    v.foo(); // same enable_if use disabled this method
}

Solution

  • v.foo<int>()
    

    you are incorrect in thinking that the version of foo on the void version of S doesn't exist. The above code will run perfectly fine.

    What you have shown is that the default template parameters pick a version that doesn't exist.

    In the case of print, you provide default template parameters - but you also allow them to be both passed (ie, .print<double>(1)) explicitly, or be deduced (as in .print(2)).

    In both of those cases, this result in a U type that is not T, hence not void.

    There are at least 2 approaches. You can block deduction (getting the foo style not-exist to work for print) and you can even block passing explicit parameters (preventing foo<int>() from being called). Second, you can also use requires, which is cleaner but requires a newer version of C++.

    To block deduction, you can use std::identity_t or you own hand-rolled equivalent:

    template <typename U = T>
    std::enable_if_t<std::is_void_v<U>, void>
    print() { std::cout << "print void"  << std::endl; }
    
    template <typename U = T>
    std::enable_if_t<not(std::is_void_v<U>), void>
    print(std::type_identity_t<U> x) { std::cout << "print not void " << x << std::endl; }
    

    to block passing parameters explicitly, do this:

    template <class..., class U = T>
    std::enable_if_t<std::is_void_v<U>, void>
    print() { std::cout << "print void"  << std::endl; }
    

    now any passed parameters get "swallowed up" by the class... and thrown away.

    Doing both gives you a older-standard implementation that does most of what you want.

    We can do better with requires however:

    template <typename T>
    struct S
    {
      void print() requires (std::is_same_v<T,void>)
      { std::cout << "print void"  << std::endl; }
    
      template<class..., class U=T>
      void print(std::type_identity_t<U> x) requires (!std::is_same_v<T,void>)
      { std::cout << "print not void " << x << std::endl; }
    
      void foo() requires (!std::is_same_v<T, void>)
      {};
    };
    

    which sadly still requires some similar machinery for print.

    As an alternative:

      void print(std::convertible_to<T> auto x) requires (!std::is_same_v<T,void>)
      { std::cout << "print not void " << static_cast<T>(x) << std::endl; }
    

    using concepts to constrain the argument to be T compatible.

    Live example.