Michael Rizkalla
by Michael Rizkalla

Until C++17, expanding parameter packs required a lot of boilerplate such as recursive template functions. Recently, binary and unary folding expressions were introduced in C++17. Folding expressions reduces the code that is needed to un-fold the parameter pack around binary and unary operators.

pre-C++17 parameter packs

Lets assume we want to create a function that takes arbitrary number of arguments and concatenate them into a std::string. This function has to be enabled for streamable types for compiler errors instead of run-time errors, however, this part is out of the scope of this article. Just keep this in mind :D

// Base case of the concatenation function
template < class TType >
std::string concatenate(TType _Ty) {
    std::stringstream ss;
    ss << _Ty;
    return ss.str();
}

// Construct a string by concatenation aribtrary 
// number of arguments
template < class TFirst, class... Args >
std::string concatenate(TFirst _Ty, Args... args) {
    std::stringstream ss;
    ss << args << concatenate(args...);
    return ss.str();
}

In this example, we had to expand the parameter pack using recursive functions. This means constructing number of std::stringstreams objects equal to the number of parameters, this is inefficient.

C++17 folding expressions

In C++17, parameter packs can be un-folded around 32 binary operators which are (+ - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*). Fold operation can be done in four different ways. For a pack E with N elements, operator op, and optional initial arguments I:

Syntax Interpretation Explanation
(E op ...) unary right fold (E1 op (E2 (… op (EN-1 op EN))))
(... op E) unary left fold ((((E1 op E2) op …) op EN-1 ) op EN)
(E op ... op I) binary right fold (E1 op ( E2 op (… op ( EN-1 op (EN op I)))))
(I op ... op E) binary left fold (((((I op E1) op E2) op …) op EN-1) op EN)

Now, we can re-write the concatenate function in a more efficient way by creating one std::stringstream object and unfold the parameter pack without the need of recursion.

template < class... Args >
std::string concatenate(Args... args) {
    std::stringstream ss;
    // ss << E_1 << E_2 << .. << E_N-1 << E_N
    (ss << ... << args); // C++17 fold expression
    return ss.str();
}

Parameter packs of length zero

As always, we do not want to fall into the infamous undefined behaviour. Applying unary operators to a parameter pack of length zero could lead to undefined behaviour except three cases

logical operator AND &&, the expression evaluates to true

logical operator OR ||, the expression evaluates to false

comma operator ,, the expression evaluates to void()