12

I would like to use a c++ template to aggregate (fold) multiple arguments using a binary operation.

Such a template could be used as follows:

fold<add>(100,10,5) expands to add(add(100, 10), 5)

The particular expansion shown above is the "left fold". The expansion add(100, add(10, 5)) is the "right fold". Assuming that the add function performs simple integer addition, both right and left folds produce the same result, 115.

But consider a function div that performs integer division (div(a,b)=a/b). In this case, associativity matters and the left and right folds produce different results:

fold_left<div>(100,10,5)  --> div(div(100, 10), 5) --> div(10, 5) -->  2
fold_right<div>(100,10,5) --> div(100, div(10, 5)) --> div(100, 2) --> 50

It is straightforward to use a variadic template to produce the right-associative version (fold_right), but I have not been able to figure out how to produce the left-associative version (fold_left). The attempted implementation of fold_left below results in a compiler error:

#include <iostream>

template <typename T> using binary_op = T(*)(const T& a, const T& b);

// The terminal (single-argument) cases of the variadic functions defined later. 
template<typename T, binary_op<T> Operation> inline T fold_right(const T& t) { return t; }
template<typename T, binary_op<T> Operation> inline T fold_left(const T& t) { return t; }

// Combines arguments using right-associative operation
// i.e. fold_right<T,op>(A,B,C) --> op(A, op(B,C))
template<typename T, binary_op<T> Operation, typename ... Rest> 
inline T fold_right(const T& t, Rest... rest) {
    return Operation(t, fold_right<T, Operation>(rest...));
}

// Combines arguments using left-associative operation
// i.e. fold_left<T,op>(A,B,C) --> op(op(A,B), C)
template<typename T, binary_op<T> Operation, typename ... Rest> 
inline T fold_left(Rest... rest, const T& t) {
    return Operation(fold_left<T, Operation>(rest...), t);
}

inline int add(const int& a, const int& b) { return a+b; }
inline int div(const int& a, const int& b) { return a/b; }

int main() {
    std::cout << fold_right<int,div>(100,10,5) //  (100 / (10 / 5))  = 50
              << "\n"
              << fold_left<int,div>(100,10,5)  //  Compiler error!
              << std::endl;
    return 0;
}

How can variadic templates be used (in c++11) to correcty implement fold_left?

I think it essentially comes down to being able to "pop" the last argument off of a parameter pack, which I attempted to do in the left_fold template above, but as I said, this resulted in a compiler error.

Note: I have used simple arithmetic operations and integers as an example in this question, but the answer(s) should be generic enough to handle aggregation of objects using an arbitrary function (assuming it returns the same type of object as its arguments).

Note 2: For those familiar with c++17, fold expressions can be used to produce both left and right folds with binary operators. But these are not available in c++11.

As a related question: The above templates require the type T to be explicitly specified, as in fold_right<int,div>(...). Is there some way to formulate the template so that only the operation is required, e.g. fold_right<div>(...). I would think the type T could be inferred, but I don't see a way to order the template arguments to put the binary_op<> first.

Thanks!

xskxzr
  • 12,442
  • 12
  • 37
  • 77
drwatsoncode
  • 4,721
  • 1
  • 31
  • 45

3 Answers3

12

Parameter packs on the left are problematic. Better reimplement it as a parameter pack on the right:

template<typename T, binary_op<T> Operation> 
inline T fold_left(const T& t) { return t; }

template<typename T, binary_op<T> Operation, typename ... Rest>
inline T fold_left(const T& a, const T& b, Rest... rest) {
    return fold_left<T, Operation>(Operation(a,b), rest...);
}
Michael Veksler
  • 8,217
  • 1
  • 20
  • 33
2

Michael answered your 1st Q. The 2nd may have different answers. My prefered way is to define your operations as functors with template members:

#include <type_traits>
struct homogene_add{
    template<typename T>
    T operator()(T const& lhs, T const& rhs){/*...*/}
};

struct mixed_add{
    template<typename L, typename R>
    std::common_type<L,R>::type
    operator()(L const& lhs, R const& rhs){/*...*/}
};

template<typename binary_op, typename ... Args> 
std::common_type<Args...>::type
fold_right(const Args&... args);

template<typename binary_op, typename First, typename ... Args>
std::common_type<First, Args...>::type
fold_right(const First& init, const Args&... args) {
    binary_op op;
    return op(init, fold_right<binary_op>(args...));
};

template<typename binary_op, typename First>
const First& fold_right(const First& init) {
    return init;
};

CV qualification and valueness correctness, I leave to the OP.

Michael Veksler
  • 8,217
  • 1
  • 20
  • 33
Red.Wave
  • 2,790
  • 11
  • 17
  • Thanks for the suggestion. Unfortunately `auto` cannot be used as a return type in c++11 without using trailing return type syntax (e.g. `auto f() -> decltype(...)`). Would it be possible to change your definitions of `fold_right` to be compatible with c++11 ? – drwatsoncode Feb 19 '19 at 18:36
  • I'd like to use this approach, but even when compiling with c++14, your forward declaration of `auto fold_right(const Args&... args);` seems to cause a compiler error: `error: use of ‘auto fold_right(const Args& ...) [with binary_op= homogene_add; Args = {int, int, int, int}]’ before deduction of ‘auto’` – drwatsoncode Feb 19 '19 at 18:52
  • @ricovox I agree. This is not a complete answer. You can work on the correct return type instead of that while I prepare for an edit. – Red.Wave Feb 19 '19 at 18:56
  • @ricovox that one is the template delaration; It can return void, as long as specializations are correctly defined. – Red.Wave Feb 19 '19 at 19:18
  • The problem I see with your updated version is that `std::common_type` might be the wrong return type. For example, suppose we want to multiply a list of scalars and Matrices. `scalar*Matrix` multiplication is well-defined and produces a Matrix. Also `Matrix*Matrix` is well defined and produces another Matrix, so the expression `Matrix M3 = s1*M1*M2;` (where `s1` is a scalar and `M`'s are matrices) would compile just fine. But a scalar cannot be implicitly converted into a Matrix (and vice-versa) so std::common_type would fail (and cause `fold_right` to fail). – drwatsoncode Feb 19 '19 at 20:41
  • But I think I should move this problem to a separate SO question. Thanks for all your helpful suggestions. – drwatsoncode Feb 19 '19 at 20:42
  • @ricovox if you start with c++14 then 'auto' would suffice. But if you need to carry the burden, you may need some meta programing to create sth like 'typename fold_right_result< binary_op,Args...>::type' – Red.Wave Feb 20 '19 at 10:15
0

That is the magic of non-trailing function parameter packs: they are only deduced from the explicitly provided template parameters.

That means rest... is empty in the fold_left<int,div>(100,10,5) call. Therefore, your function has a single argument, not 3.

Acorn
  • 24,970
  • 5
  • 40
  • 69
  • 1
    Thanks, that explains the compiler error ("*template argument deduction/substitution failed: candidate expects 1 argument, 3 provided*"). But how can I rewrite fold_left to do what I want? – drwatsoncode Feb 19 '19 at 07:30