I\'ve been wondering what are the advantages of variadic arguments over initializer lists. Both offer the same ability - to pass indefinite number of arguments to a function
Briefly, C-style variadic functions produce less code when compiled than C++-style variadic templates, so if you're concerned about binary size or instruction cache pressure, you should consider implementing your functionality with varargs instead of as a template.
However, variadic templates are significantly safer and produce far more usable error messages, so you'll often want to wrap your out-of-line variadic function with an inline variadic template, and have users call the template.
If by variadic arguments you mean the ellipses (as in void foo(...)
), then those are made more or less obsolete by variadic templates rather than by initializer lists - there still could be some use cases for the ellipses when working with SFINAE to implement (for instance) type traits, or for C compatibility, but I will talk about ordinary use cases here.
Variadic templates, in fact, allow different types for the argument pack (in fact, any type), while the values of an initializer lists must be convertible to the underlying type of the initalizer list (and narrowing conversions are not allowed):
#include <utility>
template<typename... Ts>
void foo(Ts...) { }
template<typename T>
void bar(std::initializer_list<T>) { }
int main()
{
foo("Hello World!", 3.14, 42); // OK
bar({"Hello World!", 3.14, 42}); // ERROR! Cannot deduce T
}
Because of this, initializer lists are less often used when type deduction is required, unless the type of the arguments is indeed meant to be homogenous. Variadic templates, on the other hand, provide a type-safe version of the ellipses variadic argument list.
Also, invoking a function that takes an initializer list requires enclosing the arguments in a pair of braces, which is not the case for a function taking a variadic argument pack.
Finally (well, there are other differences, but these are the ones more relevant to your question), values in an initializer lists are const
objects. Per Paragraph 18.9/1 of the C++11 Standard:
An object of type
initializer_list<E>
provides access to an array of objects of typeconst E
. [...] Copying an initializer list does not copy the underlying elements. [...]
This means that although non-copyable types can be moved into an initializer lists, they cannot be moved out of it. This limitation may or may not meet a program's requirement, but generally makes initializer lists a limiting choice for holding non-copyable types.
More generally, anyway, when using an object as an element of an initializer list, we will either make a copy of it (if it is an lvalue) or move away from it (if it is an rvalue):
#include <utility>
#include <iostream>
struct X
{
X() { }
X(X const &x) { std::cout << "X(const&)" << std::endl; }
X(X&&) { std::cout << "X(X&&)" << std::endl; }
};
void foo(std::initializer_list<X> const& l) { }
int main()
{
X x, y, z, w;
foo({x, y, z, std::move(w)}); // Will print "X(X const&)" three times
// and "X(X&&)" once
}
In other words, initializer lists cannot be used to pass arguments by reference (*), let alone performing perfect forwarding:
template<typename... Ts>
void bar(Ts&&... args)
{
std::cout << "bar(Ts&&...)" << std::endl;
// Possibly do perfect forwarding here and pass the
// arguments to another function...
}
int main()
{
X x, y, z, w;
bar(x, y, z, std::move(w)); // Will only print "bar(Ts&&...)"
}
(*) It must be noted, however, that initializer lists (unlike all other containers of the C++ Standard Library) do have reference semantics, so although a copy/move of the elements is performed when inserting elements into an initializer list, copying the initializer list itself won't cause any copy/move of the contained objects (as mentioned in the paragraph of the Standard quoted above):
int main()
{
X x, y, z, w;
auto l1 = {x, y, z, std::move(w)}; // Will print "X(X const&)" three times
// and "X(X&&)" once
auto l2 = l1; // Will print nothing
}