C++ How to simply handle variable number of function arguments

This is the first of posts, I am going to write primarily for C++ beginners.  Where this title “beginner” is a misnomer in the context of C++. To become a solid C++ beginner takes time and effort.

This is not a good situation. And the reason why, among other reasons, C++ creator (Bjarne Stroustrup) has written very short but, I think a very important “message to all” called  “Remember the Vasa!“. Please read it.

A variable number of arguments

is a name of c++ function capability to be declared to receive zero or more, or a variable number of, arguments.

This capability is inherited from C and exists as a such for decades now.

#include <stdio.h>
#include <stdarg.h>
// this is not C++, this is C
double average(int num,...) {

   va_list valist;
   double sum = 0.0;
   int i;

   /* initialize valist for num number of arguments */
   va_start(valist, num);

   /* access all the arguments assigned to valist */
   for (i = 0; i < num; i++) {
      sum += va_arg(valist, int);
   }
    
   /* clean memory reserved for valist */
   va_end(valist);

   return sum/num;
}

int main() {
   printf("Average of 2, 3, 4, 5 = %f\n", average(4, 2,3,4,5));
   printf("Average of 5, 10, 15 = %f\n", average(3, 5,10,15));
}

 

Since C++11 this was better formalized and standard C++ has been adjusted to handle this in a bit more high-level way, with the help of templates too.

Since C++11, variable set of arguments is called “parameter pack”. For a good level of detail about it please see here.

In this post, we are dealing with functions with any number of function arguments, so-called (scarily): Variadic function template.

// declaration of a variadic function template
template<class ... Types> 
   void vf(Types ... args);

// usage
vf();       // OK: args contains no arguments
vf(1);      // OK: args contains one argument: int
vf(2, 1.0); // OK: args contains two arguments: int and double

Now I am sure since you are reading this you know already all this stuff. But you might think it is non-trivial to deal with C++ parameter packs and as a such, you are avoiding them or blindly copy-paste some “scary code” from Stack Overflow to use them.

Standard C++ parameter pack is easy to handle

I am sure you have seen many scary looking articles and forum discussions about programming parameter packs. And perhaps you have decided to “deal with it later”.

There is no need for that. Standard C++  makes for easy programming of parameter packs. I have prepared two functions to show you how easy.

First here is the code. It is pretty well documented. To make this code work for you with no hassle, please install and use Visual Studio 2017 community edition.

// in stdafx.h add all the includes necessary
#include "stdafx.h"
// beware of anonymous namespace in headers
namespace 
{ 
// handling aguments param pack when all the 
// arguments are of the same type
auto args_initlist = [] (auto && ... args)
{
// if no arguments sent
// return empty result
if constexpr(1 > (sizeof... (args))) {
// C++ compiler "knows" the type 
// of the return value  
   return{};
} else {
  using namespace std;
  // here we transform arguments parameter pack to init list
  // aka "list initialization" 
  // https://en.cppreference.com/w/cpp/language/list_initialization
  // NOTE: since C++17 more than one element in the init list can be deduced
  // only by copy declaration
  // this is thus error -> auto  arg_list{ args ... };
  // and this is not
   auto arg_list = { args ... };
   // the type of the init list instance
   using    arg_list_type = decltype(arg_list);
   // the type of the elements in the init list instance
   using   arg_list_value_type = typename arg_list_type::value_type;
   /* 
     init list is a kind of a range 
     so we can handle its elements in an range for loop
    for (element && : arg_list ) {  ... do something with element ...  }
    */
  return arg_list;
}
}; // end of lambda needs semi colon

// handling aguments param pack when 
// arguments are of different types
// template< typename ... Args >
auto args_tuple = [] (auto && ... args)
{
if constexpr(1 > (sizeof... (args))) {
    return{};
}
else
{
 using namespace std;
 // here we transform arguments parameter pack to tuple
  auto  tup_list =  make_tuple( args ... );
 // the type of the tuple instance
  using arg_list_type = decltype(tup_list);

 /*
  tuple is not a range 
  see here how to get to the tuple elements
  https://en.cppreference.com/w/cpp/utility/tuple/get
  */
  return tup_list;
}
}; // eof lambda
} // namespace 

int main ()
{
// use the first variadic function template
// with arguments of a single type
auto init_list = args_initlist(1, 2, 3);

// use the second variadic function template 
// with arguments of any type
auto tup_ = args_tuple(1, 2.34, true, L"Abra", " Ka", " Dabra!");

// tuple can be easily decomposed with 
// "structured binding"  into separate variables each of
// required type
auto [a, b, c, d, e, f] = tup_;

}

Being a “C++ beginner”, you are not actually such a beginner. And as a such, you do realize we have a standard console application above and you would know which headers to include and where to put those includes.

Please compile and follow, several times, above code through very good Visual Studio debugger. That it indispensable tool when studying C++.

Discussion

You perhaps know we are dealing with two generic lambdas with a variable number of arguments.

// receive param pack 
// each argument must be of the same typr
// return std::initializer_list<T> 
auto args_initlist = [] (auto && ... args);

// receive param pack 
// arguments can be of different types 
// return std::tuple< class ... Types> 
auto args_tuple = [] (auto && ... args);

This is just “proof of concept” code. It is here to show you how to use standard C++ mechanisms and coding idioms to deal with param pack in an easy and standard fashion.

First, we deal with param packs where each of them is of the same type, like in the main() above.

auto init_list = args_initlist(1, 2, 3);

Peeking into the init_list function we immediately spot the “wonder” line:

auto arg_list = { args ... };

In standard C++, this declaration automatically transforms param pack expansion { args ... } into the constructor call to the std::initializer_list<T>. C++ compiler effectively replaces that line with the proper declaration of the init list

std::initializer_list<int> arg_list{ 1,2,3 } ;

Init list has begin() and end() methods and can be used to reach to parameter pack elements, and a such. No scary template programing. Thus you can use it in a C++ for range loop:

for ( auto  && element  : arg_list ) {
// do something with each element
// in this case do something with each argument of the
// param pack 
}

There are also a few type declarations in there you might find useful. Good, nice, simple …. But. What do we do if we need to pass variable number of arguments but of a different type?

We will use C++ tuple.

Another function is dealing with param pack of a multitude of types. As you can see the key line is this one:

auto tup_list = make_tuple( args … );

Again we simply expand the param pack, but this time we use it to provide a variable number of argument to pass to the std::make_tuple function.

In here you do whatever you need to do. You have all the arguments nicely handled as elements of the std::tuple.

Conclusion

The point is here to show how standard C++ can be really simple, but functional and powerful. Not just simple looking.

Standard C++ has many powerful tools to solve and program each development task. The trick is to know them all and select the best one. Which is almost always not the most complex one.

List of different types
List of different types