Ellipsis is the C++ syntax used to create functions accepting varying number of arguments. It has following syntax:
return_type function(args_list, ...)
Where args_list are one or more normal function parameters. There should be at least one normal function parameter for args_list.
... should be the last parameter in the function.
Ellipsis captures any additional arguments passed to the function. We can think of it as an array that holds additional arguments passed.
Let’s understand it with an example:
#include <iostream>
#include <cstdarg>
int add(int count, ...)
{
int result{0};
va_list nums;
va_start(nums, count);
for (int i{0}; i < count; ++i)
{
result += va_arg(nums, int);
}
return result;
}
int main(int argc, char const *argv[])
{
std::cout << add(3, 1, 2, 3) << '\n';
return 0;
}As there is no variable (and type) to ellipsis parameter, there no direct way to access parameters captured by ellipsis. We need to use macros va_list, va_start, and va_args from header file cstdarg.
va_listcreates a variable of special type used to access the ellipsis parameters. We declarenumsvariable usingva_list.va_startthen starts pointingva_listvariable to the parameters list. It accepts two arguments, listva_listvariable and name of the last non-ellipsis parameter (if there are more non-ellipsis parameter, we need to take last one).
Note
In the example, it may seem that
va_starttakes number of total ellipsis parameters, but that is not the case. It is always last non-ellipsis parameter.
- We then use
va_argsto get the current parameter. It accepts theva_listand target type of the parameter. It increments the pointer to next parameter after returning the current one.
Issues with ellipsis
Although ellipsis lets you write functions with varying number of parameters, this has several problems:
- No type specification: Compiler can’t help with typing checking. If any wrong type is provided, the program would not work as expected. For example:
std::cout << add(3, 1, 2, 3.3) << '\n';This gave 229902227 on my machine. What happened here? These reason as follows:
- All the data is stored as sequence of bits. The type tells how to interpret the given sequence of bits. But here, the type information is not given.
- Let’s say
inthas 4bytes size and double has 8bytes. For first two elements1and2, these get interpreted correctly. But for3.3, only first 4 bytes get interpret (because target type isintinva_arg) which gets interpreted to some garbage value.
- Missing number of parameters: There is no information on how many dynamic parameters are passed to a function. There are methods to overcome this issue using:
- Explicitly passing parameter count as we did in this example.
- Using sentinel value to check the end of parameter list.
- Using decoder string which tells how to interpret each argument. It solves the missing type information issue.
Following example shows these two methods:
int addUsingSentinal(int first, ...)
{
int result{first};
va_list otherNums;
va_start(otherNums, first);
while (true)
{
const int item{va_arg(otherNums, int)};
if (item == -1)
break;
result += item;
}
return result;
}
int addUsingDecoder(std::string_view decoder, ...)
{
int result{0};
va_list nums;
va_start(nums, decoder);
double item;
for (char type : decoder)
{
switch (type)
{
case 'i':
result += va_arg(nums, int);
break;
case 'd':
item = va_arg(nums, double);
std::cout << item << '\n';
result += item;
break;
default:
break;
}
}
return result;
}
std::cout << addUsingSentinal(1, 2, 3, -1) << '\n';
std::cout << addUsingDecoder("iii", 1, 2, 3) << '\n';
std::cout << addUsingDecoder("iiid", 1, 2, 3, 1.0) << '\n';Decoder string method has issue. If number of character in string and number of dynamic parameters do not match, bad things can happen. Furthermore, if types do not match, program would not work as expected.
Avoid ellipsis
Because of above reasons, it advisable to avoid using ellipsis and try to solve the problem using alternative methods if exists.
For example, we can pass a dynamic array of int to the function that would provide type checking feature and do the same work.
For example:
#include <iostream>
int add(int count, const int *nums)
{
int result{0};
for (int i{0}; i < count; ++i)
{
result += nums[i];
}
return result;
}
int main(int argc, char const *argv[])
{
int *arr{new int[3]{}};
std::cout << "Enter 3 number" << '\n';
for (int i{0}; i < 3; ++i)
{
std::cin >> arr[i];
if (!std::cin)
{
std::cout << "Invalid input" << '\n';
break;
}
}
std::cout << add(3, arr) << '\n';
return 0;
}