for_each_arg: Applying a Function to Each Argument of a Function in C++

How to apply a function to each of the parameter of another function?

For example, consider the following function:

template<typename... Args> void g(Args&&... args) { // ... } 1 2 3 4 5 template < typename . . . Args > void g ( Args && . . . args ) { // ... }

How can we apply a function f to each of the parameters of g ?

Mixing the code of f with the mechanics of going over all the arguments passed to g makes for code that is hard to write and hard to read.

A better solution is to use for_each_arg , that encapsulates the concept of applying a function each element in a pack of template parameters:

template<typename... Args> void g(Args&&... args) { for_each_arg(f, args...); } 1 2 3 4 5 template < typename . . . Args > void g ( Args && . . . args ) { for_each_arg ( f , args . . . ) ; }

But for_each_arg is not a standard construct. Let’s how it is implemented!

C++17 fold expressions

If you have C++17, then implementing for_each_arg is a breeze thanks to fold expressions:

template<class F, class...Args> F for_each_arg(F f, Args&&...args) { (f(std::forward<Args>(args)),...); return f; } 1 2 3 4 5 template < class F , class . . . Args > F for_each_arg ( F f , Args && . . . args ) { ( f ( std :: forward < Args > ( args ) ) , . . . ) ; return f ; }

The only technical artefact here is std::forward , that allows to treat args as rvalues if they were initialized from rvalues. Check out lvalues, rvalues and their references for a refresher on rvalues and std::forward .

Note that we return f , in order to be consistent with the behaviour of std::for_each . Indeed std::for_each applies a function to each element of a runtime collection, and returns that function.

With C++17, that’s the end of the story. But before C++17, the story goes on. Or more exactly, the story goes on with C++11, because with C++03 the story doesn’t even start.

C++11 initializer_list trick

It is possible as soon as C++11 to emulate the effect of the fold expression, by using a std::initializer_list in an astute way:

template<class F, class...Args> F for_each_arg(F f, Args&&...args) { std::initializer_list<int>{((void)f(std::forward<Args>(args)), 0)...}; return f; } 1 2 3 4 5 template < class F , class . . . Args > F for_each_arg ( F f , Args && . . . args ) { std :: initializer_list < int > { ( ( void ) f ( std :: forward < Args > ( args ) ) , 0 ) . . . } ; return f ; }

This code has been slightly adapted from an iteration between Sean Parent and Eric Niebler on Twitter at the beginning of 2015.

It contains quite a few tricks, that we shall examine one by one:

Before delving into each of those C++ constructs, note the the basic structure consists in apply f to each element:

Let’s now see how each of the accompanying constructs makes it compliant with C++11.

This is the main idea of this implementation. We are building a std::initializer_list with the results of applying f to each of the elements in args . To construct a std::initializer_list , the compiler has to resolve the expressions passed as its elements. What’s more, it does it in order from left to right.

An initializer_list , yes, but of what types? The simplest type to use is int . But f may well not return int s. This is why we use the comma operator between the result of calling f and the int of value 0 . The comma operator executes both expressions and returns the one on the right, so 0.

What we’ve said above stands if we use the build-in comma operator. But in the (unlikely) even that the comma operator is overloaded for the return type of f and int , this could fail to compile. This is why we use the expression (void) , that casts the left-hand expression into type void .

We do that because the C++ standards considers for the comma operator that if there is no viable function, then the operator used is the built-in one. And no viable function can accept a void parameter.

Not specific to C++11, and similar to the implementation using fold expressions, this std::forward allows to keep the information that the values used to ininitialize args were lvalues or rvalues, and to treat it accordingly (pass them by reference or by move).

Encapsulation works with all C++ versions

Whether you are in C++11 or C++17 or later, the best option is to encapsulate all the corresponding code in a dedicated for_each_arg function. This will decouple it from the code that use it, and will allow you to change its implementation once you upgrade your compiler.

You may also like

Share this post! Don't want to miss out ?