问题
So I have a fairly complex function:
template <typename T>
void foo(const int param1, const int param2, int& out_param)
Given int bar
, const int arg1
, and const int arg2
the function will be called with either: foo<plus<int>>(arg1, arg2, bar)
or foo<minus<int>>(arg1, arg2, bar)
Internally the function is rather complex but I am doing different relational operators based on the type of functor that was passed as a template parameter.
In the case of plus
I need to do:
arg1 > arg2
bar > 0
bar > -10
In the case of minus
I need to do:
arg1 < arg2
bar < 0
bar < 10
Note that 10
does not have the same sign in both 3s. I am currently solving all this by passing a second template parameter (less
or greater
.) But I was thinking it might make more sense to write these relations as arithmetic operations. Is that even possible, or do I need to take the second template parameter?
回答1:
T{}(0, arg1) > T{}(0,arg2);
T{}(0, bar) > 0;
T{}(0, bar) > -10;
The basic idea is a > b
if and only if -a < -b
. And plus(0,a)==a
while minus(0,a)==-a
.
The last one is tricky, as we want to change the order of <
and the sign. Luckily they cancel:
Suppose we want a constant that is -10
in the plus case, and 10
in the minus case. Then
plus(0,-10)
is -10
and
minus(0,-10)
is 10
.
So we get:
T{}(0, bar) > T{}(0, T{}(0,-10))
in the plus case, the rhs is 0+0+-10
, aka -10
.
In the minus case, this is 0-(0-(-10))
, aka -10
.
So the short form is:
T{}(0,bar) > -10
and it should work.
回答2:
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));
}
Live Demo 1
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;
}
Live Demo 2
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>{});
}
Live Demo 3
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;
}
}
Live Demo 4
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);
}
Live Demo 5
来源:https://stackoverflow.com/questions/53750460/can-i-write-relational-operators-in-terms-of-arithmetic-operations