The joys of C++17

September 10, 2019 // c++, c++17

This is gonna be a short one.

Some time ago I’ve written a tiny helper Curry for, well, currying functions and function-like objects: given some callable foo accepting arguments of types T_1, ..., T_n, Curry(foo) returns an object such that Curry(foo)(t_1)...(t_n) (where t_i is of type T_i) would, as you might expect, call foo passing all those t_is to it.

This was so long ago that C++11 compatibility was a thing for me back then, so Curry is written with that version of standard in mind. And then a couple of days ago I stumbled upon that code again, and couldn’t help but realize how terribly verbose it is. Let’s see how modern C++ allows reducing the verbosity.

So the original implementation looked roughly like this:

template<typename F, typename... PrevArgs>
class CurryImpl
{
     const F m_f;

     const std::tuple<PrevArgs...> m_prevArgs;
public:
     CurryImpl (F f, const std::tuple<PrevArgs...>& prev)
     : m_f { f }
     , m_prevArgs { prev }
     {
     }
private:
     template<typename T>
     std::result_of_t<F (PrevArgs..., T)> invoke (const T& arg, int) const
     {
          return invokeIndexed (arg, std::index_sequence_for<PrevArgs...> {});
     }

     template<typename IF>
     struct Invoke
     {
          template<typename... IArgs>
          auto operator() (IF fr, IArgs... args)
          {
               return fr (args...);
          }
     };

     template<typename R, typename C, typename... Args>
     struct Invoke<R (C::*) (Args...)>
     {
          auto operator() (R (C::*ptr) (Args...), C c, Args... rest)
          {
               return (c.*ptr) (rest...);
          }

          auto operator() (R (C::*ptr) (Args...), C *c, Args... rest)
          {
               return (c->*ptr) (rest...);
          }
     };

     template<typename T, std::size_t... Is>
     auto invokeIndexed (const T& arg, std::index_sequence<Is...>) const ->
               decltype (Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg))
     {
          return Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg);
     }

     template<typename T>
     auto invoke (const T& arg, ...) const -> CurryImpl<F, PrevArgs..., T>
     {
          return { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) };
     }
public:
     template<typename T>
     auto operator() (const T& arg) const -> decltype (invoke (arg, 0))
     {
          return invoke (arg, 0);
     }
};

template<typename F>
CurryImpl<F> Curry (F f)
{
     return { f, {} };
}

Indeed, the operator() needs to decide whether it’s time to call the wrapped callable or whether it should return another wrapper, so it does some SFINAE via the invoke helper function, and then it needs to unpack the collected arguments and decide how to invoke the callable using some more SFINAE via the nested Invoke struct (which, I’m sure, does not cover all possible callables, but it was good enough for my stuff), and then… well, you get the idea.

Also note how terribly does this thing handle move semantics, perfect forwarding, all of that: it basically doesn’t even try. There’s already just too much cognitive load.

And C++11 didn’t even have std::index_sequence and friends, or the std::result_of_t alias, as well as some other things, so it was actually even more verbose than that. It’s just that I had a tiny library of kludges over C++11 to emulate some of the C++14 library features, which is omitted here.

So, let’s see how C++17 might help us.

Firstly, we don’t need to have trailing return type on the operator(), so we can just do

template<typename T>
auto operator() (const T& arg) const
{
    return invoke (arg, 0);
}

Then, we don’t really need to SFINAE on whether m_f is callable via the Invoke struct, and two gems of C++17 will help us here: std::is_invocable and constexpr if. Let’s write a skeleton of our new operator():

template<typename T>
auto operator() (const T& arg) const
{
     if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
          // Call the function
     else
          // Return the wrapper with `arg` saved
}

The second branch is easy, and we basically reuse what we had earlier:

template<typename T>
auto operator() (const T& arg) const
{
     if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
          // Call the function
     else
          return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) };
}

The first one is more interesting. We need to call m_f, passing all arguments saved in m_prevArgs plus the arg just passed to the operator. Luckily, C++17 has yet another gem that allows invoking a callable passing a tuple of arguments to it: std::apply. Then we could either tuple_cat the existing tuple with a 1-tuple consisting of arg, or go through a wrapper lambda that appends arg to whatever gets passed to it. In my experience instantiating tuples is slow, so I’m going with the second approach. I also need to correctly call the m_f inside the wrapper lambda: this is something the Invoke helper struct has been doing previously, but now I can just use std::invoke, resulting in

template<typename T>
auto operator() (const T& arg) const
{
     if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
     {
          auto wrapper = [this, &arg] (auto&&... args)
          {
               return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg);
          };
          return std::apply (std::move (wrapper), m_prevArgs);
     }
     else
          return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) };
}

Note how the deduced return type allows returning values of different types from the different branches of if constexpr.

Anyway, that’s basically it for the class! Or, to sum it up:

template<typename F, typename... PrevArgs>
class CurryImpl
{
     const F m_f;

     const std::tuple<PrevArgs...> m_prevArgs;
public:
     CurryImpl (F f, const std::tuple<PrevArgs...>& prev)
     : m_f { f }
     , m_prevArgs { prev }
     {
     }

     template<typename T>
     auto operator() (const T& arg) const
     {
          if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
          {
               auto wrapper = [this, &arg] (auto&&... args)
               {
                    return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg);
               };
               return std::apply (std::move (wrapper), m_prevArgs);
          }
          else
               return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) };
     }
};

template<typename F, typename... Args>
CurryImpl<F, Args...> Curry (F f, Args&&... args)
{
     return { f, std::forward_as_tuple (std::forward<Args> (args)...) };
}

I think it’s a huge improvement over the original version.

We could also get rid of the Curry helper by relying on the deduction guides for CurryImpl, but let’s do this after we figure out the handling of value categories first. And, speaking of those…

Now it’s really obvious how terrible is the implementation with respect to copying arguments, doing perfect forwarding and so on. And, more importantly, it’s now way easier to fix this, since there’s so much less code, and the signal-to-noise ratio is so much better. But this is going to be a subject for a next post.

The sad part is that C++20’s std::bind_front will cover most of the cases I needed this function for.