In the current state of c++11 (say gcc 4.7.2), how should I choose between using a variadic-template or a std::initializer_list
when I need a constructor that can take variable arguments?

- 14,936
- 14
- 74
- 140
-
3I'm not sure (which is why this is a comment) but can't variadic templates handle different types while a initializer list has to be all the same type? – Some programmer dude Feb 16 '13 at 19:27
-
@JoachimPileborg, Absolutely correct, though you could then choose between `int...` and `std::initializer_list
`. I say pick whichever feels more natural to use. – chris Feb 16 '13 at 19:29
3 Answers
A variadic template allows you providing arguments of different types, while an std::initializer_list
is templated with the type of the argument. This means the type of all the elements in the list must be the same (or convertible to the underlying type, but no narrowing conversions are allowed). Depending on whether or not this is desirable for you, you may choose one or the other.
Also, a variadic template is usually the default choice if you need perfect forwarding, in that the syntactic form T&&
can bind to both lvalue references and rvalue references, while a similar type deduction cannot be performed for initializer_list
:
struct A
{
// Deduces T& for lvalue references, T for rvalue references, and binds to both
template<typename... Ts>
A(Ts&&...) { }
// This is an rvalue reference to an initializer_list. The above type deduction
// does not apply here
template<typename T>
A(initializer_list<T>&&) { }
};
Also notice, that a constructor accepting an initializer_list
will be invoked by default when you use uniform initialization syntax (i.e. curly braces), even though another viable constructor exists. This may or may not be something you wish to have:
struct A
{
A(int i) { }
};
struct B
{
B(int) { }
B(std::initializer_list<A>) { }
};
int main()
{
B b {1}; // Will invoke the constructor accepting initializer_list
}

- 124,023
- 23
- 387
- 451
-
3Expanding on the bit about perfect forwarding, `std::initializer_list
::reference` (which is the result type of e.g. `*l.begin()`) is `T const&`, which prohibits moves. You can put move-only values in, but you can't move them out. – Luc Danton Feb 16 '13 at 19:36
With a variadic template, the number of arguments is known during compilation (and accessible via sizeof...
). With a std::initializer_list
, the number of arguments is known only at runtime. So part of the decision depends on when you need or want to know how many arguments you have.

- 12,972
- 8
- 50
- 91
-
the container has compile-time size, as it must be initialised in complete state (you can't push_back, etc.). if you meant the issue of its `::size()` method not being marked `constexpr`, this was a defect in C++11 and fixed in C++14. so you could template on the size of a named `initializer_list`, etc. in many ways, the size is available at compile-time. but sure, perhaps not as many ways as the variadic option! – underscore_d Apr 14 '16 at 17:35
-
2A constructor _parameter_ that's an `initializer_list` doesn't have a size known during compilation, because the constructor may be called from multiple sites with different-sized initializers at each site. – KnowItAllWannabe Apr 14 '16 at 21:47
I recommend always chosing variadic templates and avoid std::initializer_list
whenever possible.
This is how I would have implemented std::vector
with C++11:
#include <iostream>
#include <vector>
struct exp_sequence {
template <typename... T>
exp_sequence(T&&...) {}
};
struct from_arglist_t {} from_arglist;
template <typename T>
class my_vector {
std::vector<T> data;
public:
my_vector(int n, T const& e) : data(n, e) {}
template <typename... Args>
my_vector(from_arglist_t, Args&&... args) {
data.reserve(sizeof...(Args));
exp_sequence{(data.push_back(std::forward<Args>(args)),1)...};
}
std::size_t size() { return data.size(); }
};
int main()
{
std::vector<int> v1{13, 13}; std::cout << v1.size() << '\n'; // 2
std::vector<int> v2(13, 13); std::cout << v2.size() << '\n'; // 13
my_vector<int> v3{13, 13}; std::cout << v3.size() << '\n'; // 13
my_vector<int> v4(13, 13); std::cout << v4.size() << '\n'; // 13
my_vector<int> v5(from_arglist, 13, 13); std::cout << v5.size() << '\n'; // 2
my_vector<int> v6{from_arglist, 13, 13}; std::cout << v6.size() << '\n'; // 2
}
The reason is as showed in main
, using initializer_list
in generic code can lead to different behaviour depending on which type of parentheses was chosen. There is also the possibility to silently change code by adding such an constructor.
Another reason are move-only types:
//std::vector<move_only> m1{move_only{}}; // won't compile
my_vector<move_only> m2{from_arglist, move_only{}}; // works fine

- 8,045
- 29
- 33
-
The `exp_sequence` trick is neat, but does it actually guarantee to insert the elements in the correct order? AFAIR, C and C++ never promised to evaluate function arguments in lexical order. So unless there's a special guarantee in the C++11 standard about the argument evaluation order for variadic template constructors, this is non-portable. – fgp Oct 25 '13 at 09:16
-
1@fgp: The argument evaluation order is indeed guaranteed to be sequenced from left to right. This holds for every use of brace initialisation (therefore exp_sequence needs to be a class). – ipc Nov 09 '13 at 11:09
-
1@ipc What exactly is going on at `exp_sequence{ (data.push_back(std::forward
(args)), 1)... }`? Why do we need to surround `data.push_back(std::forward – 0xbadf00d Mar 04 '16 at 20:41(args))` by `(` and `, 1)`? And why `1`?