std::initializer_list
A lightweight, read-only view over a compiler-generated const array that enables brace-initialization syntax for functions and constructors.
std::initializer_list<T>since C++11A non-owning, immutable view over a compiler-generated array of const T elements, used to give functions and constructors a uniform interface for accepting brace-enclosed element lists.
Overview
When you write {1, 2, 3} in an initializing context, the compiler synthesizes a temporary const T[N] array and wraps it in an std::initializer_list<T> object. The list is a thin proxy: it stores a pointer and a size, neither of which you can reassign, and the elements themselves are const-qualified β you cannot modify them through the list.
The header is <initializer_list>, though it is implicitly included by many standard headers.
The core interface (all members constexpr since C++14):
| Member | Return type | Notes |
|---|---|---|
begin() | const T* | Pointer to first element |
end() | const T* | One past the last element |
size() | std::size_t | Number of elements |
Because begin() and end() are provided, std::initializer_list<T> satisfies the range concept and works with range-for loops and range algorithms since C++11.
Syntax
Initializer-list constructors
An initializer-list constructor takes std::initializer_list<T> as its first parameter with no additional required parameters:
#include <initializer_list>
#include <memory>
#include <stdexcept>
class SampleBuffer {
public:
explicit SampleBuffer(std::initializer_list<float> values)
: data_(std::make_unique<float[]>(values.size()))
, size_(values.size())
{
std::copy(values.begin(), values.end(), data_.get());
}
float operator[](std::size_t i) const { return data_[i]; }
std::size_t size() const noexcept { return size_; }
private:
std::unique_ptr<float[]> data_;
std::size_t size_;
};
SampleBuffer buf{0.1f, 0.5f, 0.9f, 1.0f}; // calls initializer-list constructorFunctions accepting uniform argument lists
std::initializer_list is also useful for functions that logically accept a variable number of homogeneous arguments without resorting to variadic templates or va_args:
#include <initializer_list>
#include <numeric>
double mean(std::initializer_list<double> vals) {
if (vals.size() == 0) return 0.0;
return std::accumulate(vals.begin(), vals.end(), 0.0) / vals.size();
}
double result = mean({1.0, 2.5, 3.0, 4.5}); // 2.75Examples
Forwarding to a container member
A common pattern is an initializer-list constructor that forwards directly into an internal std::vector:
#include <initializer_list>
#include <vector>
#include <algorithm>
class IntSet {
public:
IntSet(std::initializer_list<int> il) : data_(il) {
std::sort(data_.begin(), data_.end());
data_.erase(std::unique(data_.begin(), data_.end()), data_.end());
}
bool contains(int v) const {
return std::binary_search(data_.begin(), data_.end(), v);
}
private:
std::vector<int> data_;
};
IntSet primes{7, 2, 5, 3, 2, 7}; // stored as {2, 3, 5, 7}Iterating with range-for
#include <initializer_list>
#include <cstdio>
void log_all(std::initializer_list<const char*> messages) {
for (const char* msg : messages)
std::puts(msg);
}
log_all({"connected", "authenticated", "ready"});std::min / std::max with initializer lists
The standard library overloads of std::min and std::max (and std::minmax) accept initializer_list since C++11, allowing multi-argument calls without nesting:
#include <algorithm>
int lo = std::min({a, b, c, d}); // C++11
int hi = std::max({a, b, c, d}); // C++11
auto [mn, mx] = std::minmax({a, b, c, d}); // C++11, structured bindings C++17Best Practices
Store by value, not by reference. The backing array is a temporary. Storing an std::initializer_list<T> as a member or returning it from a function leaves a dangling pointer. Copy the elements into owned storage in the constructor body.
Accept by value. The object is already a pointer+size pair (typically 16 bytes on 64-bit). Passing by const& adds no benefit and can interact oddly with lifetime extension rules.
Prefer std::initializer_list over variadic templates for homogeneous sets. When all arguments must share the same type, the initializer_list overload produces cleaner call sites and simpler implementation compared to template<typename... Args>.
Validate in the constructor. Because callers can provide any number of elements, defensive constructors should check size constraints early:
EvenPairs(std::initializer_list<int> il) {
if (il.size() % 2 != 0)
throw std::invalid_argument("requires an even number of elements");
// ...
}Common Pitfalls
Overload resolution hijacking
When a class has both a regular constructor and an initializer-list constructor, brace-initialization always prefers the initializer-list constructor β even when the narrowing conversion would otherwise make the regular constructor the better match. This is the source of the classic std::vector surprise:
std::vector<int> v1(10, 0); // regular constructor: 10 zero-filled elements
std::vector<int> v2{10, 0}; // initializer-list constructor: {10, 0}, 2 elementsThe compiler will force a narrowing conversion to reach an initializer-list constructor before it considers a non-list constructor that is a perfect match. Only if no conversion to the element type exists at all does overload resolution fall through to the regular constructors.
auto deduction rules differ by standard
C++11 and C++14 treat both copy-list-initialization and direct-list-initialization with auto the same way β everything deduces to std::initializer_list<T>:
// C++11 / C++14
auto a = {1}; // std::initializer_list<int>
auto b{1}; // std::initializer_list<int> β surprising
auto c{1, 2}; // std::initializer_list<int>C++17 tightened the rules: direct-list-initialization (auto x{...}) with a single element now deduces the element type, and with multiple elements is ill-formed:
// C++17 and later
auto a = {1}; // still std::initializer_list<int>
auto b{1}; // int β changed
auto c{1, 2}; // error: cannot deduce element typeThis means code that relied on auto x{v} yielding std::initializer_list is silently broken when compiled as C++17. Audit any such declarations when upgrading.
Lifetime of the backing array
An std::initializer_list does not own the array behind it. The array's lifetime follows different rules depending on context:
- When used to initialize a named
std::initializer_list<T>variable, the backing array lives as long as that variable. - When used as a constructor argument or function argument inline (not bound to a named list variable), the array is a temporary that is destroyed at the end of the full expression.
Saving the list itself for later use is therefore undefined behavior:
struct Bad {
std::initializer_list<int> saved;
Bad(std::initializer_list<int> il) : saved(il) {} // dangling after constructor returns
};Always extract and own the elements before the initializer-list goes out of scope.
All elements must share a common type
The compiler must be able to deduce a single T from all elements in the braced list. Mixed arithmetic types that require narrowing conversions will be rejected, and mixed unrelated types produce a hard error:
auto x = {1, 2.0}; // error: no common type between int and double
auto y = {1, 2}; // ok: std::initializer_list<int>
auto z = {1.0, 2.0}; // ok: std::initializer_list<double>See Also
std::vector,std::array, and other sequence containers β all provide initializer-list constructors since C++11- Uniform initialization and brace-initialization β the language mechanism that produces
std::initializer_listobjects std::min/std::maxβ standard overloads acceptingstd::initializer_list<T>(C++11)