I wanted to write a fold
function for std::tuple
that can compute e.g. the sum (or product) of all the elements in a given tuple. For example, given
std::tuple<int,double> t = std::make_tuple(1,2);
I'd like to compute
auto s = sumT(t); //giving 3
I tried but couldn't get my template programming (c++11/1z) code below to compile. I also tried to adapt the accepted answer for my other question (How to perform tuple arithmetic in C++ (c++11/c++17)?), but can't figure out how to use std::index_sequence
in this case.
The issues I have are:
1) I can't figure out the types, e.g. how to use the type of the first element as the return type. Currently, I use a _res
type in the template, but I don't know if that will block c++'s auto type-inferencing.
2) I'd like to program this without using an explicit initial element 0
, so that this can be used for other kinds of fold
operations.
Currently, the recursion ends at the last element. I'd like to end the recursion at _size - 1
so that I can directly perform the operation on the last element without resorting to 0
.
The code I have below tries to do this by recursion. But I don't know template programming well, and how the loops work for tuples.
Can someone help fix the code or come up with a better solution?
My code so far is:
#include <tuple>
#include <iostream>
#include <functional>
// helper class for fold operations
template<typename Op,typename _res, typename _Tp, size_t _i, size_t _size>
struct _tuple_fold {
static constexpr _res _op(Op const & op, const _Tp& _t) {
return _res(op(std::get<_i>(_t),
_tuple_fold<Op, _res, _Tp, _i + 1, _size>::_op(op,_t) ));
}
};
template<typename Op,typename _res,typename _Tp, size_t _size>
struct _tuple_fold<Op, _res,_Tp, _size, _size> {
static constexpr _res _op(Op const &, const _Tp&) { return 0; }
};
template <typename ... Ts>
auto sumT (std::tuple<Ts...> const & t1) {
return _tuple_fold::_op(std::plus<>{}, t1);
}
int main () {
std::tuple<int,double> t = std::make_tuple(1,2);
auto s = sumT(t);
std::cout << s << std::endl;
}
The error messages for compiling with g++ -std=c++17 tuple_sum.cpp
:
tuple_sum.cpp: In function ‘auto sumT(const std::tuple<_Elements ...>&)’:
tuple_sum.cpp:21:10: error: ‘template<class Op, class _res, class _Tp, long unsigned int _i, long unsigned int _size> struct _tuple_fold’ used without template parameters
return _tuple_fold::_op(std::plus<>{}, t1);
^
tuple_sum.cpp: In function ‘int main()’:
tuple_sum.cpp:27:19: error: ‘void s’ has incomplete type
auto s = sumT(t);
^
I'm not sure how to supply the type parameters for _tuple_fold
on the call site, especially the type for std::plus
.
note that in c++17 we can apply() and fold:
auto t = std::make_tuple( 1, 2. );
auto sum = std::apply([]( auto... v ){ return ( v + ... ); }, t );
this will work for any tuple-like type and follows usual promotion/conversion rules for '+' out of the box ( this may or may not be desirable ). In the above I pass by value because we're dealing with arithmetic types, but you can apply your preferred forwarding strategy of course...
Thankfully, if constexpr
in C++17 allows us to avoid complicating things by introducing partially specialized helper structs, and makes it easy to terminate the recursion on whatever condition we want:
#include <functional>
#include <iostream>
#include <tuple>
#include <type_traits>
template <size_t index, class Op, class... Ts>
constexpr auto tuple_fold(Op op, const std::tuple<Ts...>& t) {
if constexpr(index == sizeof...(Ts) - 1) {
return std::get<index>(t);
} else {
return op(std::get<index>(t), tuple_fold<1 + index>(op, t));
}
}
template <typename ... Ts>
constexpr auto sumT (std::tuple<Ts...> const & t1) {
return tuple_fold<0>(std::plus<>{}, t1);
}
int main () {
std::tuple<int,double> t = {1, 2.0};
auto s = sumT(t);
static_assert(std::is_same_v<decltype(s), double>);
std::cout << s << std::endl;
}
Coliru link: http://coliru.stacked-crooked.com/a/1e7051b8652fb942
This will perform a right fold, a + (b + (c + ...))
but it's easy to rewrite it to perform a left fold instead, if you want.
We can leverage c++17's left and right fold builtins to fold over any binary operation.
template<class F, class Lhs=void>
struct invoke_by_times_t {
F& f;
Lhs lhs;
template<class Rhs>
auto operator*( Rhs&& rhs )&&
->invoke_by_times_t<F, std::invoke_result_t< F&, Lhs, Rhs >>
{
return {
f,
f(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))
};
}
};
template<class F>
struct invoke_by_times_t<F, void> {
F& f;
template<class Rhs>
invoke_by_times_t<F, Rhs> operator*( Rhs&& rhs )&&{
return {f, std::forward<Rhs>(rhs)};
}
};
template<class F>
auto fold_over( F&& f ) {
return [f=std::forward<F>(f)](auto&&...args)mutable{
return ( invoke_by_times_t<F>{f}*...*decltype(args)(args) ).lhs;
};
}
now given any binary function we can create a function object that folds over it without doing any recursion.
Together with std::apply
we are done.
template <typename ... Ts>
auto sumT (std::tuple<Ts...> const & t1) {
return std::apply( fold_over(std::plus<>{}), t1);
}
Live example
This is a left fold. A right fold simply involves changing the fold_over
function. If you try to pass an empty pack to this, it will fail to compile. If you pass it one element, it will return that element.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With