Why use variadic arguments now when initializer lists are available?

dtech picture dtech · Mar 17, 2013 · Viewed 13.2k times · Source

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.

What I personally think is initializer lists are a little more elegant. Syntax is less awkward.

Also, it appears that initializer lists have significantly better performance as the number of arguments grows.

So what am I missing, besides the possibility to use use variadic arguments in C as well?

Answer

Andy Prowl picture Andy Prowl · Mar 17, 2013

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 type const 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
}