Selecting a member function using different enable_if conditions

21,336

Solution 1

enable_if works because the substitution of a template argument resulted in an error, and so that substitution is dropped from the overload resolution set and only other viable overloads are considered by the compiler.

In your example, there is no substitution occurring when instantiating the member functions because the template argument T is already known at that time. The simplest way to achieve what you're attempting is to create a dummy template argument that is defaulted to T and use that to perform SFINAE.

template<typename T>
struct Point
{
  template<typename U = T>
  typename std::enable_if<std::is_same<U, int>::value>::type
    MyFunction()
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U = T>
  typename std::enable_if<std::is_same<U, float>::value>::type
    MyFunction()
  {
    std::cout << "T is not int." << std::endl;
  }
};

Edit:

As HostileFork mentions in the comments, the original example leaves the possibility of the user explicitly specifying template arguments for the member functions and getting an incorrect result. The following should prevent explicit specializations of the member functions from compiling.

template<typename T>
struct Point
{
  template<typename... Dummy, typename U = T>
  typename std::enable_if<std::is_same<U, int>::value>::type
    MyFunction()
  {
    static_assert(sizeof...(Dummy)==0, "Do not specify template arguments!");
    std::cout << "T is int." << std::endl;
  }

  template<typename... Dummy, typename U = T>
  typename std::enable_if<std::is_same<U, float>::value>::type
    MyFunction()
  {
    static_assert(sizeof...(Dummy)==0, "Do not specify template arguments!");
    std::cout << "T is not int." << std::endl;
  }
};

Solution 2

A simple solution is to use delegation to worker private functions:

template<typename T>
struct Point
{

  void MyFunction()
  {
     worker(static_cast<T*>(nullptr)); //pass null argument of type T*
  }

private:

  void worker(int*)
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U>
  void worker(U*)
  {
    std::cout << "T is not int." << std::endl;
  }
};

When T is int, the first worker function will be called, because static_cast<T*>(0) turns out to be of type int*. In all other cases, the template version of worker will be called.

Solution 3

I think this follows @Praetorian's solution, but I find it easier:

template<typename T>
struct Point
{
    template<typename U = T>
    std::enable_if_t<std::is_same<U, T>::value && std::is_same<T, int>::value>
    MyFunction()
    {
        std::cout << "T is int." << std::endl;
    }

    template<typename U = T>
    std::enable_if_t<std::is_same<U, T>::value && std::is_same<T, float>::value>
    MyFunction()
    {
        std::cout << "T is not int." << std::endl;
    }
};

Solution 4

enable_if only works for deduced function template arguments or for specialized class template arguments. What you're doing doesn't work, because obviously with a fixed T = int, the second declaration is just erroneous.

This is how it can be done:

template <typename T>
void MyFreeFunction(Point<T> const & p,
                    typename std::enable_if<std::is_same<T, int>::value>::type * = nullptr)
{
    std::cout << "T is int" << std::endl;
}

// etc.

int main()
{
    Point<int> ip;
    MyFreeFunction(ip);
}

An alternative would be to specialize Point for various types T, or to put the above free function into a nested member template wrapper (which is probably the more "proper" solution).

Solution 5

Based on Praetorian's suggestion (but without changing the return type of the function), this seems to work:

#include <iostream>
#include <type_traits>

template<typename T>
struct Point
{
  template<typename U = T>
  void MyFunction(typename std::enable_if<std::is_same<U, int>::value, U >::type* = 0)
  {
    std::cout << "T is int." << std::endl;
  }

  template<typename U = T>
  void MyFunction(typename std::enable_if<!std::is_same<U, int>::value, float >::type* = 0)
  {
    std::cout << "T is not int." << std::endl;
  }
};

int main()
{
  Point<int> intPoint;
  intPoint.MyFunction();

  Point<float> floatPoint;
  floatPoint.MyFunction();
}
Share:
21,336
David Doria
Author by

David Doria

I live at the intersection of computer vision research and software engineering. With my background in image processing, computer vision, machine learning, and LiDAR data analysis, I am in a position to develop algorithmic solutions to these difficult problems. But these algorithms are not particularly beneficial as only ideas or prototypes - efficient and carefully executed implementations are the key to their real world application. I am passionate about the reusability of the software I develop - I have found that continually building tools for my team rather than simply solving the particular problem at hand is the key to accelerating future work and success.

Updated on September 20, 2020

Comments

  • David Doria
    David Doria over 3 years

    I am trying to determine which version of a member function gets called based on the class template parameter. I have tried this:

    #include <iostream>
    #include <type_traits>
    
    template<typename T>
    struct Point
    {
      void MyFunction(typename std::enable_if<std::is_same<T, int>::value, T >::type* = 0)
      {
        std::cout << "T is int." << std::endl;
      }
    
      void MyFunction(typename std::enable_if<!std::is_same<T, int>::value, float >::type* = 0)
      {
        std::cout << "T is not int." << std::endl;
      }
    };
    
    int main()
    {
      Point<int> intPoint;
      intPoint.MyFunction();
    
      Point<float> floatPoint;
      floatPoint.MyFunction();
    }
    

    which I thought is saying "use the first MyFunction if T is int, and use the second MyFunction if T is not int, but I get compiler errors saying "error: no type named ‘type’ in ‘struct std::enable_if’". Can anyone point out what I am doing wrong here?

    • HostileFork says dont trust SE
      HostileFork says dont trust SE over 11 years
    • aschepler
      aschepler about 5 years
      Update: C++20 will allow template<typename T> struct Point { void MyFunction() requires (std::is_same_v<T, int>); void MyFunction() requires (!std::is_same_v<T, int>); };. Here it's okay that the constraint expression is non-dependent and false - that just makes the entire function less preferred during overload resolution.
  • Nawaz
    Nawaz over 11 years
    In C++11, SFINAE rules has been modified a little bit, due to which SFINAE will not trigger on return type. In short, this answer is wrong.
  • Praetorian
    Praetorian over 11 years
    @Nawaz Works just fine on gcc4.7.2, can't post a demo link since LWS is down. Here's an ideone demo.
  • David Doria
    David Doria over 11 years
    @Nawaz, this should be valid though, right? template<typename T> struct Point { template<typename U = T> void MyFunction(typename std::enable_if<std::is_same<U, int>::value, U >::type* = 0) { std::cout << "T is int." << std::endl; } template<typename U = T> void MyFunction(typename std::enable_if<!std::is_same<U, int>::value, float >::type* = 0) { std::cout << "T is not int." << std::endl; } };
  • HostileFork says dont trust SE
    HostileFork says dont trust SE over 11 years
    ...also there's nothing stopping one from doing a mix and match if someone explicitly specializes and doesn't use the default. So you get situations like intPoint.MyFunction<float>() which are probably incorrect. A static assert in the body that makes sure T matched the same type you tested U against is necessary as well. :-/
  • David Doria
    David Doria over 11 years
    I've seen this solution, but it seems to really corrupt the readability of the code.
  • Kerrek SB
    Kerrek SB over 11 years
    @DavidDoria: The original code is too contrived to make a more adapted suggestion.
  • Kerrek SB
    Kerrek SB over 11 years
    I actually like this. I don't think I'd ever use it, but the OP's example is so contrived that this is indeed a good solution.
  • David Doria
    David Doria over 11 years
    @Praetorian I made a new answer that uses your suggestion but does not move the SFINAE trigger to the return type (to avoid the invalid c++11 that Nawaz mentioned). Thoughts?
  • Praetorian
    Praetorian over 11 years
    @HostileFork Good point, added another example that uses static_assert to prevent explicit specializations of the member templates.
  • Praetorian
    Praetorian over 11 years
    @David I don't know whether to believe Nawaz that C++11 changed the rules and broke a ton of enable_if code out there. The example in your answer should work too; I prefer the dummy template argument as opposed to the dummy function argument, but that's just personal preference. Also, I've posted another example that uses static_assert to prevent the user from explicitly specializing the member template.
  • David Doria
    David Doria over 11 years
    @Praetorian I do not like the dummy function argument, but I also don't want to write code that is going to break with the next gcc :) (as Nawaz suggests this might). Is there a way to use a dummy template argument and have a return type of void? Also, what is the ... called in this context so I can look it up?
  • Praetorian
    Praetorian over 11 years
    @David I assume you meant use dummy template argument and have return type other than void. The second, optional, template argument for enable_if is the type argument, so use typename enable_if<expr, SomeType>::type, and the resulting type will be SomeType. The ... in the answer is variadic templates, another C++11 feature.
  • HostileFork says dont trust SE
    HostileFork says dont trust SE over 11 years
    @DavidDoria If your case is that you are only using SFINAE to check if certain types are is_same (then having a default if those don't match) then template-specializing Point for those fixed types would be exactly what you wanted. You'd have the same instantiation with more readable definitions.
  • David Doria
    David Doria over 11 years
    @Praetorian Ah I see, I didn't realize there was a default of 'void' as the second enable_if parameter. So with the variable number of template parameters, it does not work like functions where the "fixed" parameters have to go first and then the "remaining arguments" are passed to the "..." (i.e. you have Dummy... before T=U).
  • Xeo
    Xeo over 11 years
    @Nawaz: Where exactly does the C++11 standard say you can't do that anymore? That'd be a pretty daring breaking change, IMO, and I can't see why they should do it. Also, if it was true, the (now) idiomatic template<class T> auto f(T& v) -> decltype(v.foo()); SFINAE construct, which checks for a member, wouldn't work.
  • Nawaz
    Nawaz over 11 years
    @Xeo: Compare the wordings of $14.8.2/2 from C++03 with the wordings of $14.8.2/8 from C++11. According to C++11, this doesn't seem to be SFINAE, rather it is an error. See this topic also : stackoverflow.com/questions/12015938/…
  • Xeo
    Xeo over 11 years
    @Nawaz: That doesn't in any way suggest that substitution in a return type is not a soft-error anymore. The problem in that other thread is that inside of meta<int>, you get the error, which never happens with enable_if.
  • Janek Olszak
    Janek Olszak over 9 years
    static_cast<T*>(nullptr)
  • Pharap
    Pharap over 4 years
    Technically Praetorian's suggestion doesn't change the return type, it changes the argument type (i.e. by removing the default argument).
  • Pharap
    Pharap over 4 years
    The question is specifically tagged C++11, if constexpr is only available from C++17 onwards. Also the mutable isn't needed and is probably a bad idea as mutable usually is.
  • sdd
    sdd over 4 years
    @Praetorian perhaps you could just add static_assert(std::is_same<U, T>::value, "Do not specify template arguments!"); to break compilation. Variadic args just serve to a bit more informative error message (correct types in it).