Can I Write Relational Operators in Terms of Arithmetic Operations?

佐手、 提交于 2019-12-11 01:56:01

问题


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:

  1. arg1 > arg2
  2. bar > 0
  3. bar > -10

In the case of minus I need to do:

  1. arg1 < arg2
  2. bar < 0
  3. 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!