2

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?

Jonathan Mee
  • 37,899
  • 23
  • 129
  • 288
  • 1
    `foo` and `foo` can be trampoline functions that call into more complicated template machinery where you can encode the `less` `greater` and the appropriate constants. – AndyG Dec 12 '18 at 20:00
  • @AndyG Yeah, I suppose I could do a `constexpr if` based on the type. – Jonathan Mee Dec 12 '18 at 20:02
  • Basically that's the way to go. Will probably be cleaner than without `constexpr if`. I think you've got the gist of it, would you like a code example, too? Is there another way you're hoping for this to work? – AndyG Dec 12 '18 at 20:05
  • @AndyG Yeah I guess if that's as good as we got and you write it up I'll accept. But what I was thinking of was a way to magic less than out of `minus` and greater than out of `plus`. – Jonathan Mee Dec 12 '18 at 20:10
  • There's no inherent binding of `less` to `minus` or `greater` to `plus`, so the traditional way would be through traits, but that is a lot of extra machinery that you just don't need when `if constexpr` is around – AndyG Dec 12 '18 at 20:16
  • How are you passing `std::plus{}` to `auto T`? `std::plus` is not an integral type – AndyG Dec 12 '18 at 20:50
  • @AndyG Yeah `T` can be anything. I learned about `auto` template parameters here: https://stackoverflow.com/q/38457112/2642059 And I've found them extremely useful for handing around functors/function pointers/member pointers. I'm effectively passing the type *and* the object in one parameter there. – Jonathan Mee Dec 12 '18 at 20:56
  • 1
    Can you show a compiling example where you pass `std::plus{}` as an explicit template argument for a function? – AndyG Dec 12 '18 at 20:59
  • @AndyG And of course you're totally right T.T I can't pass a functor as a template argument :( I thought this was possible. I'll be asking a follow up question and post it here. – Jonathan Mee Dec 13 '18 at 12:18
  • @AndyG I've asked a question for clarification on what is and isn't allowed as an argument to an `auto` template parameter here: https://stackoverflow.com/q/53761899/2642059 Just thought I'd link in case you would like to weigh in. – Jonathan Mee Dec 13 '18 at 12:32
  • @JonathanMee [tag:C++20] with `operator<=>` permits non-type template parameters of (some) class types. I'm uncertain if lambdas or `std::plus` will be one of them, but as both are monostate types there is no reason why they won't. – Yakk - Adam Nevraumont Dec 13 '18 at 16:55
  • @Yakk-AdamNevraumont Sadly, the standard functors do not define relational operators, so I think even in C++20, I won't be able to do this. Thank you for the tip though. I also found this rather exciting tidbit in my linked question, and I've been excited about it ever since. – Jonathan Mee Dec 14 '18 at 04:51
  • @jona adding `<=> = default` to std types is probably worth proposing. – Yakk - Adam Nevraumont Dec 14 '18 at 07:44
  • @Yakk-AdamNevraumont It is at that. I'd love to... though I've never written a proposal. I don't suppose there's a beginner's guide somewhere? – Jonathan Mee Dec 14 '18 at 13:15

2 Answers2

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

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

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

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • Just wanted to say that I appreciated all the effort that went into this. I'm restricted to Visual Studio 2012 for this project so, I think you're only suggestion that I could benefit from was the specialization. Ultimately I found that passing 2 template arguments was just the best approach for this problem. But I find these very educational in a more general sense as well. So thank you. – Jonathan Mee Dec 14 '18 at 04:58
  • 1
    @JonathanMee: I'm sorry about your luck. That's coming from a former VS2012 user. I hope you get to upgrade soon, the product has really improved through VS 2017. That said, I think with some fiddling we could make the function traits and the tagged dispatch approaches work there, but full specialization is probably good enough. – AndyG Dec 14 '18 at 13:36