Left-folding and right-folding an arbitrary callable
Nerdsnipe: Given an arbitrary binary callable f(x,y)
, write a function left_fold
such that left_fold(f)(a,b,c,...)
evaluates to f(f(f(a,b),c),...)
. Write a function right_fold
such that right_fold(f)(a,b,c,...)
evaluates to
f(a, f(b, f(c, ...)))
.
That is, we’re looking to define left_fold
and right_fold
such that
auto leftsub = left_fold(std::minus<>);
auto rightsub = right_fold(std::minus<>);
static_assert(leftsub(1, 2, 3) == (1 - 2) - 3);
static_assert(rightsub(1, 2, 3) == 1 - (2 - 3));
This is easy with “recursive templates,” but in modern C++ we know that “Iteration is better than recursion” (2018-07-23), so let’s try to do it with C++17 fold-expressions. Godbolt:
#define FWD(x) static_cast<decltype(x)>(x)
#define MOVE(x) static_cast<decltype(x)&&>(x)
template<class F, class TRR>
struct Folder {
const F& foo_;
TRR value_;
template<class U>
constexpr auto operator+(Folder<F, U>&& rhs) && -> decltype(auto) {
using R = decltype(foo_(FWD(value_), FWD(rhs.value_)));
return Folder<F, R>{foo_, foo_(FWD(value_), FWD(rhs.value_))};
}
};
template<class F>
constexpr auto left_fold(F foo) {
return [foo = MOVE(foo)](auto&&... args) -> decltype(auto) {
return (... + Folder<F, decltype(args)>{foo, FWD(args)}).value_;
};
}
template<class F>
constexpr auto right_fold(F foo) {
return [foo = MOVE(foo)](auto&&... args) -> decltype(auto) {
return (Folder<F, decltype(args)>{foo, FWD(args)} + ...).value_;
};
}
There are a couple of tricks hiding in here:
-
The
FWD(x)
macro is just a shorter spelling ofstd::forward<X>(x)
. I normally usestatic_cast<X&&>(x)
, but in this case I have several places where the typeX
is not easily nameable. -
The
MOVE(x)
macro is just a shorter spelling ofstd::move(x)
, and I’m using it here only to capturefoo
by move without having to include any standard library headers. I want to capturefoo
by move, just in case it’s a move-only lambda type. -
All of the uses of
-> decltype(auto)
are necessary. Try removing them and seeing what goes wrong in folding<<
over(std::cout, 1, 2, 3)
. -
We use aggregate initialization for
Folder<F, R>{...}
, so that the value offoo_(FWD(value_), FWD(rhs.value_))
will be constructed in-place rather than constructing a temporary and then having to move it into place. This allows us to work with non-move-constructible types.
However, if the overall result of the fold is a prvalue of a
non-move-constructible type, we fail (Godbolt).
I can partially fix left_fold
’s issue with non-move-constructible types,
but (1) I cannot fix right_fold
; (2) I cannot totally fix left_fold
;
and (3) my change to left_fold
causes additional failures in even more
pathological cases. (Godbolt.)
template<class F, class TRR>
struct LeftFolder {
const F& foo_;
TRR value_;
template<class U>
constexpr friend auto operator+(U&& lhs, LeftFolder&& rhs) -> decltype(auto) {
return rhs.foo_(FWD(lhs), FWD(rhs.value_));
}
};
template<class F>
constexpr auto left_fold(F foo) {
return [foo = MOVE(foo)](auto&& init, auto&&... args) -> decltype(auto) {
return (FWD(init) + ... + LeftFolder<F, decltype(args)>{foo, FWD(args)});
};
}