RSS | GitHub |
Projected variadic fold is a fold over transformed arguments, and is
functionally equivalent to std::inner_product
but the input is passed as
function arguments rather than iterators.
This article is a continuation of Variadic Fold, and will use the same techniques described therein, such as parameter pack expansion and integer sequences, without further explanation.
While fold is useful for simple cumulative algorithms such as calculating the
sum or the product, other cumulative algorithms are more involved.
An example is the dot product,
which exists in the standard library as
std::inner_product
.
The dot product of two $N$-dimensional vectors
$$\begin{eqnarray*} \mathbf{a} &=& (a_0, a_1, \ldots, a_{N-1}) \\ \mathbf{b} &=& (b_0, b_1, \ldots, b_{N-1}) \end{eqnarray*}$$
is given by
$$\begin{eqnarray*} \mathbf{a} \cdot \mathbf{b} &=& \sum_{k=0}^{N-1} a_k\,b_k \end{eqnarray*}$$
The elements $a_k$ and $b_k$ are multiplied pair-wise and the results are summed up.
Projected variadic fold is a generalization of the above, where the addition and multiplication are replaced by arbitrary numeric functions.
The algorithm first applies an inner function g
repeatedly across the
parameter pack, and then folds the results using an outer function f
.
The first step is called transform or projection, and the second is called
reduction or fold.
Each projection consumes W arguments from the parameter pack, where W is the
arity of the inner function g
.
The parameter pack must contain a multiple of W elements.
Using a binary inner function splits the parameter pack into slices with two
elements each.
becomes the fold of
which will be expanded as
We assume that the arguments $a_k$ and $b_k$ are interleaved by the user when calling this algorithm.
The dot product is a projected fold using multiplication as the projection and addition as the fold.
Before the implementation of projected fold is explained, we will take a detour into extended invocation to pick up some useful building-blocks.
The standard library contains std::invoke
which invokes the given function with the given arguments.
For instance
becomes
Selected invocation invokes the given function with selected arguments determined by an integer sequence.
Selective invocation is declared as
The integer sequence holds the indices of the arguments to be used.
becomes
using C++26 pack indexing, which amounts to
Selective invocation is fairly easy to implement.
We are not going to use pack indexing because of the C++26 requirement, but
rather we are going to forward the arguments as a tuple to a helper function
that uses std::get
to select the arguments.
As a side-note, tuple_apply_sequence
can also be used to implement std::apply
.
The standard provides functionality to create an integer sequence with the numbers from $0$ to $N-1$, but we are going to need a facility to create integer sequences where we can choose the starting value and the step size between values.
We therefore introduce an extension to create integer sequences that takes 3 arguments
where N
is the number of integers, Offset
is the starting value, and
Step
is the step size. Offset
defaults to 0, and Step
defaults to 1.
becomes
A couple of concrete examples
The implementation is outside the scope of this article so we just stipulate that such a facility is available.
Projected invocation is similar to the before-mentioned projected fold, except
the outer function f
is not a fold function, but just a normal function
that must take as many arguments as produced by the projection.
Projected invocation is declared as
If f3
takes three arguments, and g2
takes two then
becomes
Projected invocation has to split the parameter pack into smaller slices, which is done using selective invocation. The intermediate steps can be illustrated as follows.
The projection applies g2
to slices of the parameter pack using generated
integer sequences
which expands to
The generic implementation may use a projection function of arbitrary arity,
so the injection of invoke_sequence
becomes a matter of juggling with integer
sequences to select the proper slices.
While it is possible to determine the arity automatically, we pass it as an
argument below for simplicity.
This injects an integer sequence with the starting offsets of the slices and
calls a helper function invoke_over_sequence
below, which generates the calls
to invoke_sequence
for each slice.
We start with an integer sequence containing the start of each slice, and the individual slices will be generated from that.
The helper function is given two parameter packs: Args...
and Offset...
which are expanded at different levels, so Args...
is expanded for each
Offset
.
Projected fold is a projection invocation where the outer function is a fold.
This creates a function that uses f
to fold over the results.
The fold uses partial application
in the form of C++20 std::bind_front
where
becomes
In order for the binding to work we have changed the variadic fold function into a function object.
More on function objects in a future article.
This article has used various facilities from newer C++ standards, but these facilities can be backported to C++11.