Besides @Yakk's answer there are a number of ways you can do this. Here are 5.
Method 1: Function traits
This is more of a classic technique used before more advanced template-metaprogramming techniques became available. It's still quite handy. We specialize some structure depending on T
to give us the types and constants we want to use.
template<class T>
struct FooTraits;
template<class T>
struct FooTraits<std::plus<T>>
{
using Compare = std::greater<T>;
static constexpr std::tuple<int, int> barVals{0, 10};
};
template<class T>
struct FooTraits<std::minus<T>>
{
using Compare = std::less<T>;
static constexpr std::tuple<int, int> barVals{0, -10};
};
template <class T>
void foo(const int arg1, const int arg2, int& bar)
{
using traits = FooTraits<T>;
typename traits::Compare cmp{};
cmp(arg1, arg2);
cmp(bar, std::get<0>(traits::barVals));
cmp(bar, std::get<1>(traits::barVals));
}
Method 2: Full specialization
Another "classic" technique that remains useful. You are probably familiar with this technique, but I'm showing it for completeness. So long as you never need to partially specialize a function, you can write different version of it for the types you need:
template <class T>
void foo(const int arg1, const int arg2, int& bar);
template <>
void foo<std::plus<int>>(const int arg1, const int arg2, int& bar)
{
arg1 > arg2;
bar > 0;
bar > 10;
}
template <>
void foo<std::minus<int>>(const int arg1, const int arg2, int& bar)
{
arg1 < arg2;
bar < 0;
bar < -10;
}
Method 3: Tagged dispatch
A third classic technique that turns a type check into an overloading problem. The gist is that we define some lightweight tag
struct that we can instantiate, and then use that as a differentiator between overloads. Often this is nice to use when you have a templated class function, and you don't want to specialize the entire class just to specialize said function.
namespace detail
{
template<class...> struct tag{};
void foo(const int arg1, const int arg2, int& bar, tag<std::plus<int>>)
{
arg1 > arg2;
bar > 0;
bar > 10;
}
void foo(const int arg1, const int arg2, int& bar, tag<std::minus<int>>)
{
arg1 < arg2;
bar < 0;
bar < -10;
}
}
template <class T>
void foo(const int arg1, const int arg2, int& bar)
{
return detail::foo(arg1, arg2, bar, detail::tag<T>{});
}
Method 4: Straightforward constexpr if
Since C++17 we can use if constexpr
blocks to make a compile-time check on a type. These are useful because if the check fails the compiler doesn't compile that block at all. This oftentimes leads to much easier code than before, where we had to use complicated indirection to classes or functions with advanced metaprogramming:
template <class T>
void foo(const int arg1, const int arg2, int& bar)
{
if constexpr (std::is_same_v<T, std::plus<int>>)
{
arg1 > arg2;
bar > 0;
bar > 10;
}
if constexpr(std::is_same_v<T, std::minus<int>>)
{
arg1 < arg2;
bar < 0;
bar < -10;
}
}
Method 5: constexpr
+ trampolining
trampolining is a metaprogramming technique where you use a "trampoline" function as an intermediary between the caller and the actual function you wish to dispatch to. Here we will use it to map to the appropriate comparison type (std::greater
or std::less
) as well as the integral constants we wish to compare bar
to. It's a little more flexible than Method 4. It also separates concerns a bit, too. At the cost of readability:
namespace detail
{
template<class Cmp, int first, int second>
void foo(const int arg1, const int arg2, int& bar)
{
Cmp cmp{};
cmp(arg1, arg2);
cmp(bar, first);
cmp(bar, second);
}
}
template <class T>
void foo(const int arg1, const int arg2, int& bar)
{
if constexpr (std::is_same_v<T, std::plus<int>>)
return detail::foo<std::greater<int>, 0, 10>(arg1, arg2, bar);
if constexpr(std::is_same_v<T, std::minus<int>>)
return detail::foo<std::less<int>, 0, -10>(arg1, arg2, bar);
}