40

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?

Alexis
  • 707
  • 8
  • 33
dtech
  • 47,916
  • 17
  • 112
  • 190
  • 13
    Initializer lists can only have one type. Keep in mind there are variadic templates, as opposed to the non-type safe C variadic arguments. – chris Mar 17 '13 at 20:04
  • @chris: And it's a shame, too. :( – GManNickG Mar 17 '13 at 20:06
  • @GManNickG, Yeah, `std::tuple` built into initializer lists would be cool, but there are undoubtedly things that would be hard to work out that I just can't think of right now. – chris Mar 17 '13 at 20:07
  • Initializer lists don't allow to pass move-only types and have some subtle bugs. I always prefer variadic templates over initializer lists. – ipc Mar 17 '13 at 20:08
  • @ipc: There is no prohibition on using move-only types to initialize an initializer list. Of course, you have to use rvalues, because lvalues can't be moved. Try initializing a std::initializer_list> with std::unique_ptr rvalues. Works fine. Check out http://liveworkspace.org/code/2xRk1g$1. – KnowItAllWannabe Mar 18 '13 at 23:43
  • 1
    @KnowItAllWannabe: And how can you move it out again? – ipc Mar 18 '13 at 23:52
  • @ipc: Cast the constness away from the const T* pointer you get from std::initializer_list's iterators, dereference the resulting pointer, and apply std::move to it. I've updated the code at LWS to show the basic idea: http://liveworkspace.org/code/2xRk1g$2 – KnowItAllWannabe Mar 19 '13 at 00:02
  • 1
    @KnowItAllWannabe: Don't do this. [You can't be sure that always works.](http://liveworkspace.org/code/2RV9EI$0) I'm not even sure if this is defined behaviour. – ipc Mar 19 '13 at 00:13
  • 1
    @ipc: The only way the behavior would be undefined would be if the elements in the temporary array behind the braced initializer were const. But 8.5.4/5 doesn't say the array is const, nor does it say that the array's elements are const. The example in that section (non-normative, but still) shows code that includes no mention of const. What is the basis for your belief that the behavior is undefined? (Note, BTW, that I'm not advocating coding in this way. I'm just challenging your claim that initializer lists don't support move-only types.) – KnowItAllWannabe Mar 19 '13 at 03:47
  • 2
    @ipc: Your code at LWS asserts, because you're using a moved-from init list to initialize w2. The revised code at http://liveworkspace.org/code/2xRk1g$3 makes this clear. – KnowItAllWannabe Mar 19 '13 at 03:59
  • 1
    @KnowItAllWannabe: Per 18.9/1: "An object of type initializer_list provides access to an array of objects of type const E. [...]" – Andy Prowl Mar 19 '13 at 09:04
  • @Andy Prowl: That seals it, the contents of std::initializer_list can't be moved wihthout UB. But the more general point that move-only types can be stored in initializer_lists stands. Code could dereference std::unique_ptrs in an initializer list, for example. – KnowItAllWannabe Mar 19 '13 at 16:56

2 Answers2

58

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
}
Community
  • 1
  • 1
Andy Prowl
  • 124,023
  • 23
  • 387
  • 451
  • @Gui13: Thank you, actually there would be more to be added, but I tried to make it concise enough to include just the points which seem to me most relevant to the question being asked :) – Andy Prowl Mar 17 '13 at 20:38
  • Another important difference between variadic templates and C-style varargs is that a C-style vararg function can be separately compiled. A variadic template cannot. – Eric Niebler Mar 18 '13 at 22:31
  • 1
    Per my comment to ipc above, it's untrue that initializer_lists can't hold move_only types: http://liveworkspace.org/code/2xRk1g$1. – KnowItAllWannabe Mar 18 '13 at 23:48
  • 1
    @KnowItAllWannabe: That's correct, they can indeed hold move-only types (I edited my answer), but you won't be able to move away from them, because they are `const` objects. – Andy Prowl Mar 19 '13 at 09:17
  • implementing `operator=` could be a reason to use an `initializer_list`, as it only takes 1 or 2 arguments. – user1095108 Oct 04 '13 at 08:32
  • 1
    maybe worth adding initializer lists are easier to iterate over than argument packs? – jiggunjer Jul 04 '15 at 05:24
  • variadic templates are recursive, not as straight forward as iterating over a list of arguments. `const` is Ok, only if I could use `initializer_list` that could be the prefect way to replace homogeneous variadic functions with something more typesafe. – dashesy Jul 10 '16 at 17:03
2

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.

Jeffrey Yasskin
  • 5,171
  • 2
  • 27
  • 39