33

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?

David Doria
  • 9,873
  • 17
  • 85
  • 147
  • Related Q&A: ["What happened to my SFINAE" (redux)](http://stackoverflow.com/questions/11531989/what-happened-to-my-sfinae-redux-conditional-template-class-members) – HostileFork says dont trust SE Nov 15 '12 at 16:32
  • Update: C++20 will allow `template struct Point { void MyFunction() requires (std::is_same_v); void MyFunction() requires (!std::is_same_v); };`. Here it's okay that the constraint expression is non-dependent and false - that just makes the entire function less preferred during overload resolution. – aschepler May 13 '19 at 17:49

6 Answers6

39

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;
  }
};
Praetorian
  • 106,671
  • 19
  • 240
  • 328
  • 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. – Nawaz Nov 15 '12 at 16:36
  • @Nawaz Works just fine on gcc4.7.2, can't post a demo link since LWS is down. [Here's](https://ideone.com/J0j3IK) an ideone demo. – Praetorian Nov 15 '12 at 16:38
  • @Nawaz, this should be valid though, right? template struct Point { template void MyFunction(typename std::enable_if::value, U >::type* = 0) { std::cout << "T is int." << std::endl; } template void MyFunction(typename std::enable_if<!std::is_same::value, float >::type* = 0) { std::cout << "T is not int." << std::endl; } }; – David Doria Nov 15 '12 at 16:40
  • ...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()` 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. :-/ – HostileFork says dont trust SE Nov 15 '12 at 16:41
  • @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? – David Doria Nov 15 '12 at 17:04
  • @HostileFork Good point, added another example that uses `static_assert` to prevent explicit specializations of the member templates. – Praetorian Nov 15 '12 at 17:06
  • @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. – Praetorian Nov 15 '12 at 17:08
  • @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? – David Doria Nov 15 '12 at 17:12
  • @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::type`, and the resulting type will be `SomeType`. The `...` in the answer is variadic templates, another C++11 feature. – Praetorian Nov 15 '12 at 17:16
  • @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). – David Doria Nov 15 '12 at 17:25
  • 6
    @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 auto f(T& v) -> decltype(v.foo());` SFINAE construct, which checks for a member, wouldn't work. – Xeo Nov 15 '12 at 17:28
  • @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 : http://stackoverflow.com/questions/12015938/overload-resolution-behaviour-difference-between-gcc-and-clang – Nawaz Nov 15 '12 at 18:12
  • 5
    @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`, you get the error, which never happens with `enable_if`. – Xeo Nov 15 '12 at 20:05
  • 2
    @Praetorian perhaps you could just add `static_assert(std::is_same::value, "Do not specify template arguments!");` to break compilation. Variadic args just serve to a bit more informative error message (correct types in it). – sdd Sep 27 '19 at 21:44
6

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.

Pharap
  • 3,826
  • 5
  • 37
  • 51
Nawaz
  • 353,942
  • 115
  • 666
  • 851
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;
    }
};
Ido Kessler
  • 279
  • 3
  • 5
1

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).

Pharap
  • 3,826
  • 5
  • 37
  • 51
Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • I've seen this solution, but it seems to really corrupt the readability of the code. – David Doria Nov 15 '12 at 16:42
  • @DavidDoria: The original code is too contrived to make a more adapted suggestion. – Kerrek SB Nov 15 '12 at 16:42
  • @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. – HostileFork says dont trust SE Nov 15 '12 at 17:19
1

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();
}
David Doria
  • 9,873
  • 17
  • 85
  • 147
  • Technically Praetorian's suggestion doesn't change the return type, it changes the argument type (i.e. by removing the default argument). – Pharap Sep 10 '19 at 10:44
0

Point template bellow is possible to instantiate only with int or float as template argument T.

To answer the question: here worker() gets called exactly depending on the template parameter of the method() call, but still you are in control of the types.

    template<typename T>
    struct Point
    {
        static_assert (
              std::is_same<T, int>()  ||
              std::is_same<T, float>()
            );

        template<typename U>
        void method(U x_, U y_)
        {
            if constexpr (std::is_same<T, U>()) {
                worker(x_, y_);
                return;
            }
            // else 
            worker(
                static_cast<T>(x_),
                static_cast<T>(y_)
            );
            return ;
        }


    private:

        mutable T x{}, y{};

        void worker(T x_, T y_)
        {
            // nothing but T x, T y
        }

    };

Above worker() will of course work even if it is declared as static. For some valid reason. Few other extensions to the above are possible (and simple) but let us stick to the answer.

  • 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. – Pharap Sep 10 '19 at 10:46