The joys of 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_i
s 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.