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 typeXis not easily nameable. -
The
MOVE(x)macro is just a shorter spelling ofstd::move(x), and I’m using it here only to capturefooby move without having to include any standard library headers. I want to capturefooby 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)});
};
}
