Folding expressions in C++


Welcome to the next pikoTutorial !

What is a folding expression?

Imagine that you have a vector of integers and you need to add some predefined values to it. The obvious choice seems to be just using push_back() function applied a couple of times:

C++
# include <vector>

int main()
{
    std::vector<int> vec;
    
    vec.push_back(-3);
    vec.push_back(5);
    vec.push_back(8);
    vec.push_back(12);
    vec.push_back(47);
}

This works, but look how much vertical space it took and how many times you had to call push_back() function. To make it more concise, we could define a function which uses a C++ folding expression. Such expression applies certain function (in this case push_back()) to all elements of the parameters pack:

C++
# include <vector>

template <typename ... Args>
void PushBackAtOnce(std::vector<int> &vec, Args&& ... args)
{
    (vec.push_back(std::forward<Args>(args)), ...); // folding expression
}

int main()
{
    std::vector<int> vec;
    PushBackAtOnce(vec, -3, 5, 8, 12, 47);
}

Folding expressions and function overloading

Such syntax can be also combined with the function’s overloading. In such case, you can apply a different implementation of the function to each of the parameters pack element, depending on the type of that element.

C++
#include <iostream>
// implementation for integer type
void Process(const int input)
{
    std::cout << "Doing something with an integer..." << std::endl;
}
// implementation for float type
void Process(const float input)
{
    std::cout << "Doing something with a float..." << std::endl;
}

template <typename ... Args>
void DoSomething(Args&& ... args)
{
    (Process(std::forward<Args>(args)), ...);
}

int main()
{
    DoSomething(12, 24.0F);
}

The output of the above code is:

Doing something with an integer...
Doing something with a float...

Potential pitfalls

You must remember that when your code relies fully on function overloading within a folding expression, you are responsible for providing overloaded function implementations for all the types – otherwise, the compilation will fail. In the example above I can add just one more argument 24.0 at the end and the compilation fails:

C++
DoSomething(12, 24.0F, 24.0);

Literals like 24.0 (without F suffix) are resolved to a double type and because there’s no implementation of Process() function for double, compiler reports an error. In such case you must either provide such implementation or create a generic function which will perform some default behavior for all the types which don’t have their dedicated implementations. For example:

C++
// generic implementation
template <typename T>
void Process(const T input)
{
    std::cout << "Unknown type - skipping..." << std::endl;
}

Now, the program compiles again and the output is:

Doing something with an integer...
Doing something with a float...
Unknown type - skipping...