Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Variable Argument Lists
Understand variadic C functions and why modern alternatives are safer.
Ellipsis (and Why to Avoid Them)
In all the functions we've examined so far, the number of parameters has been fixed at compile time, even when using default parameters. However, C++ provides a mechanism called ellipsis (represented as ...) that allows functions to accept a variable number of arguments.
Ellipsis are rarely used in modern C++, potentially dangerous, and we strongly recommend avoiding them. This section is provided for completeness and to help you understand legacy code.
Ellipsis Syntax
Functions using ellipsis follow this pattern:
return_type function_name(parameter_list, ...)
Key requirements:
- At least one regular (non-ellipsis) parameter is required
- Regular parameters must come first
- The ellipsis must always be the last parameter
The ellipsis captures any additional arguments beyond those specified in parameter_list. While not technically accurate, you can think of the ellipsis conceptually as an array holding extra parameters.
Basic Ellipsis Example
Let's write a function that calculates the mean of several doubles:
#include <iostream>
#include <cstdarg> // required for ellipsis support
// count specifies how many additional numbers follow
double calculateMean(int count, ...)
{
double sum{0.0};
// Declare a va_list to access ellipsis arguments
std::va_list arguments;
// Initialize va_list. First parameter: the va_list
// Second parameter: the last non-ellipsis parameter
va_start(arguments, count);
// Extract each argument from the ellipsis
for (int i{0}; i < count; ++i)
{
// va_arg extracts values from ellipsis
// First parameter: the va_list
// Second parameter: the type to extract
sum += va_arg(arguments, double);
}
// Clean up the va_list
va_end(arguments);
return sum / count;
}
int main()
{
std::cout << calculateMean(4, 10.0, 20.0, 30.0, 40.0) << '\n';
std::cout << calculateMean(6, 5.5, 10.5, 15.5, 20.5, 25.5, 30.5) << '\n';
return 0;
}
Output:
25
18
Let's break down the components:
- #include
: Provides va_list,va_start,va_arg, andva_endmacros - std::va_list: A type that represents the ellipsis arguments
- va_start(): Initializes the
va_listto point to the first ellipsis argument - va_arg(): Retrieves the current argument and advances to the next one
- va_end(): Cleans up the
va_listwhen finished
The ellipsis parameter has no name - you access its values through the va_list object.
Danger 1: No Type Checking
With regular function parameters, the compiler verifies that argument types match parameter types. This type safety helps catch errors.
Ellipsis completely suspend type checking. You can pass any type to an ellipsis parameter, and the compiler won't warn you if it's wrong.
Consider this dangerous example:
std::cout << calculateMean(5, 12.5, 20.0, 30.0, 40.0, 50.0) << '\n';
This looks correct, but what if we accidentally use an integer instead of a double?
std::cout << calculateMean(5, 100, 20.0, 30.0, 40.0, 50.0) << '\n';
Output:
2.38658e+07
That's clearly wrong! Here's what happened:
Computers store all data as sequences of bits. A variable's type tells the computer how to interpret those bits. Ellipsis throw away type information, so when you call va_arg(arguments, double), you're telling the system "interpret the next chunk of data as a double."
An int typically uses 4 bytes of storage, while a double uses 8 bytes. When we passed 100 (an int) as the first ellipsis argument, va_arg(arguments, double) read 8 bytes: the 4 bytes of the integer plus 4 bytes from the next argument. This produces garbage. The next va_arg call then reads the remaining 4 bytes of the second argument plus 4 bytes of the third, producing more garbage.
Even worse - this compiles without warnings:
double weight{75.5};
std::cout << calculateMean(5, 100, "invalid", 'X', &weight, &calculateMean) << '\n';
This mixes integers, strings, characters, pointers - complete nonsense. Yet it compiles and runs, producing garbage output. This exemplifies "garbage in, garbage out."
Danger 2: No Argument Count Tracking
Ellipsis don't track how many arguments were passed. You must devise your own tracking mechanism, typically using one of three methods:
Method 1: Pass a Length Parameter
Pass the count as a regular parameter (as in our calculateMean example above).
However, this relies on the caller providing the correct count:
std::cout << calculateMean(5, 10.0, 20.0, 30.0, 40.0) << '\n';
Output:
-1.18584e+08
We said 5 arguments but only provided 4. The function reads past the valid arguments into garbage memory values.
Opposite problem:
std::cout << calculateMean(4, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0) << '\n';
Output:
25
We provided 6 values but said 4. The function only processes the first 4 and ignores the rest. These errors are extremely difficult to detect.
Method 2: Use a Sentinel Value
A sentinel is a special value that marks the end of data. For example, C-style strings use '\0' as a sentinel.
Here's calculateMean rewritten with a sentinel value of -1.0:
#include <iostream>
#include <cstdarg>
double calculateMean(double first, ...)
{
// Handle first value specially (it's not in the ellipsis)
double sum{first};
int count{1};
std::va_list arguments;
va_start(arguments, first);
while (true)
{
double value{va_arg(arguments, double)};
// Check for sentinel value
if (value == -1.0)
break;
sum += value;
++count;
}
va_end(arguments);
return sum / count;
}
int main()
{
std::cout << calculateMean(10.0, 20.0, 30.0, 40.0, -1.0) << '\n';
std::cout << calculateMean(5.5, 10.5, 15.5, 20.5, -1.0) << '\n';
return 0;
}
Challenges with sentinels:
- C++ requires at least one non-ellipsis parameter, so we must handle
firstspecially - If the user forgets the sentinel, the function loops through garbage until it finds a matching value (or crashes)
- Sentinels only work when a value exists outside the valid range (what if we need negative numbers?)
Method 3: Use a Decoder String
A decoder string specifies the type of each argument:
#include <iostream>
#include <string_view>
#include <cstdarg>
double calculateMean(std::string_view types, ...)
{
double sum{0.0};
int count{0};
std::va_list arguments;
va_start(arguments, types);
for (char type : types)
{
if (type == 'd') // double
{
sum += va_arg(arguments, double);
++count;
}
else if (type == 'i') // int
{
sum += va_arg(arguments, int);
++count;
}
}
va_end(arguments);
return count > 0 ? sum / count : 0.0;
}
int main()
{
std::cout << calculateMean("dddd", 10.0, 20.0, 30.0, 40.0) << '\n';
std::cout << calculateMean("ddiddi", 5.5, 10.5, 100, 15.5, 20.5, 200) << '\n';
return 0;
}
This method can handle mixed types, but the decoder string is cryptic and must match the arguments exactly, or undefined behavior occurs.
Recommendations
First choice: Don't use ellipsis at all! Better alternatives usually exist. For our calculateMean example, we could pass a std::vector<double> or an array, providing type safety and automatic size tracking.
If you must use ellipsis:
- Use consistent types: All ellipsis arguments should be the same type to minimize type confusion
- Prefer count or decoder parameters over sentinels: This forces explicit specification of argument count, reducing sentinel-related errors
- Document extensively: Clearly document expected types and argument count requirements
Advanced note: C++11 introduced parameter packs and variadic templates, which provide ellipsis-like functionality with full type safety. C++17 added fold expressions, making parameter packs much more practical. These modern features should be used instead of traditional ellipsis in new code.
Summary
Ellipsis (...): A mechanism that allows functions to accept a variable number of arguments, though with significant limitations and dangers.
Key components: The <cstdarg> header provides std::va_list for representing ellipsis arguments, va_start() for initialization, va_arg() for retrieval, and va_end() for cleanup.
Major danger 1 - No type checking: Ellipsis completely suspend type checking, allowing any type to be passed. Using va_arg() with the wrong type leads to undefined behavior as the function misinterprets the bit patterns of the arguments.
Major danger 2 - No argument count tracking: Ellipsis don't track how many arguments were passed. You must implement your own tracking using: a length parameter (error-prone if count is wrong), a sentinel value (fails if sentinel is forgotten or is a valid value), or a decoder string (cryptic and error-prone).
Partial extraction problem: When extracting multiple values, errors can leave objects in inconsistent states with some values updated and others not, which is particularly dangerous.
Best practices for ellipsis: Use consistent types for all ellipsis arguments, prefer count/decoder parameters over sentinels, and document extensively. However, the overarching recommendation is simple: avoid ellipsis entirely in modern C++.
Modern C++ provides far better alternatives: use containers like std::vector for type-safe variable-length argument lists, or use parameter packs and variadic templates (C++11) with fold expressions (C++17) for compile-time type-safe variadic functions. These modern features eliminate all the dangers of traditional ellipsis while providing cleaner, safer code.
Variable Argument Lists - Quiz
Test your understanding of the lesson.
Practice Exercises
Variadic Sum Function (Educational Only)
Implement a function using ellipsis to sum a variable number of integers. This exercise demonstrates why ellipsis are dangerous - you'll see how easy it is to make mistakes. Note: In real code, use std::vector or variadic templates instead.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!