std::initializer_list in C++11
What is std::initializer_list?
std::initializer_list<T> is a lightweight, read-only view over a fixed array of objects of type T, created from a brace-enclosed initializer list { ... }. It was introduced in C++11 to support uniform initialization and initializer-list constructors.
std::initializer_list is a C++11 utility type that provides a read-only view over a temporary array created from a brace-enclosed initializer list.
Characteristics
- Compile-time construct (the type and elements are known at compile time)
- Immutable (elements cannot be modified, means its const T)
- Cheap to copy (typically just two pointers)
- Does not own elements
- Elements’ lifetime is tied to the full expression
How It Works Conceptually
When you write:
{1, 2, 3}
The compiler translates it into:
- A temporary array of
const T - Wrapped in a
std::initializer_list<T>
This happens automatically behind the scenes.
Examples
Using std::initializer_list with Standard Containers
#include <iostream>
#include <vector>
#include <string>
#include <initializer_list>
int main() {
// Vector initialized with initializer_list
std::vector<int> numbers = {1, 2, 3, 4, 5};
// String initialized with initializer_list
std::vector<std::string> words = {"hello", "world", "C++11"};
// Direct use in range-based for loop
for (int value : {10, 20, 30, 40}) {
std::cout << value << " ";
}
std::cout << std::endl;
return 0;
}
Custom Function Taking std::initializer_list
#include <iostream>
#include <initializer_list>
// Function that accepts initializer_list
int sum(std::initializer_list<int> values) {
int total = 0;
for (int val : values) {
total += val;
}
return total;
}
int main() {
std::cout << sum({1, 2, 3, 4, 5}) << std::endl; // Output: 15
std::cout << sum({10, 20}) << std::endl; // Output: 30
return 0;
}
Custom Class with Initializer-List Constructor
#include <iostream>
#include <initializer_list>
#include <vector>
class MyContainer {
private:
std::vector<int> data;
public:
// Constructor accepting initializer_list
MyContainer(std::initializer_list<int> init) : data(init) {
std::cout << "Initializer-list constructor called with "
<< init.size() << " elements" << std::endl;
}
void print() const {
for (int val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
int main() {
MyContainer container = {1, 2, 3, 4, 5};
container.print(); // Output: 1 2 3 4 5
return 0;
}
Rules
Syntax-Driven, Not Type-Driven
Important: std::initializer_list is syntax-driven, not type-driven. It exists to support {} syntax — not to abstract containers.
The One-Line Rule
A std::initializer_list parameter can only bind to a brace-enclosed initializer list, never to a container object.
#include <vector>
#include <initializer_list>
void process(std::initializer_list<int> values) {
// ...
}
int main() {
process({1, 2, 3}); // Works - brace-enclosed list
std::vector<int> vec = {1, 2, 3};
// process(vec); // Error - cannot bind vector to initializer_list
return 0;
}
Overload Resolution Priority
Initializer-list constructors have higher priority than other constructors when using brace initialization:
#include <iostream>
#include <initializer_list>
class X {
public:
X(int a, int b) {
std::cout << "X(int, int) called" << std::endl;
}
X(std::initializer_list<int> init) {
std::cout << "X(std::initializer_list<int>) called" << std::endl;
}
};
int main() {
X x(1, 2); // Output: X(int, int) called
X y{1, 2}; // Output: X(std::initializer_list<int>) called
return 0;
}
This is a very common C++11 pitfall! Even when other constructors match perfectly, the initializer-list constructor takes precedence with {} syntax.
Lifetime Management of std::initializer_list
The Critical Rule You Must Remember
std::initializer_list does NOT own its elements. It only points to a temporary array created by the compiler.
The lifetime of the array behind a std::initializer_list is tied to the lifetime of the initializer_list object that is directly created from {} — NOT to copies made later.
Safe Usage
It is safe to use std::initializer_list as a function parameter:
#include <iostream>
#include <initializer_list>
void safe_usage(std::initializer_list<int> values) {
// Safe: using within the function scope
for (int val : values) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
safe_usage({1, 2, 3, 4, 5}); // ✅ Safe
return 0;
}
Lifetime Extension with Local Variables
When using std::initializer_list as a local variable, the type declaration matters:
#include <iostream>
#include <initializer_list>
int main() {
// DANGEROUS: auto deduction
auto il1 = {1, 2, 3};
// Temporary array destroyed at end of statement!
// il1 now holds dangling pointers
// AFE: Explicit type
std::initializer_list<int> il2 = {1, 2, 3};
// Lifetime of temporary array is extended to match il2's scope
// Using il1 here is undefined behavior
// for (int val : il1) { } // Dangling!
// Using il2 is safe
for (int val : il2) { // Safe
std::cout << val << " ";
}
std::cout << std::endl;
return 0;
}
Why the Difference?
| Code | Mechanism | Lifetime Rule | Status |
|---|---|---|---|
auto il = {1, 2, 3}; | auto deduction happens after temporary creation | Temporary array destroyed at end of statement | Dangling |
std::initializer_list<int> il = {1, 2, 3}; | Explicitly typed variable binds to the temporary | Lifetime of temporary is extended to match il | Safe |
Case 1: auto il = {1, 2, 3}; (Dangling)
This line works because C++ infers the type of il to be std::initializer_list<int>. However, the underlying temporary array is created within the full expression of that single statement.
The critical issue is the order of operations:
- The temporary array containing
{1, 2, 3}is created autotype deduction happens (determinesilshould bestd::initializer_list<int>)- The
std::initializer_listis constructed to point to the temporary array - The statement ends (semicolon is reached)
- The temporary array is immediately destroyed (standard C++ lifetime rules)
ilis left holding dangling pointers to deallocated memory
Why it fails:
In C++ rules, temporaries are destroyed at the end of the full expression that creates them. As soon as the semicolon is reached, the temporary array is destroyed. The variable il now contains pointers to invalid memory.
While some compilers might extend the lifetime in this specific auto case as an extension or optimization, relying on il after the declaration line is undefined behavior according to the C++ standard. The reason is that auto deduction happens after the temporary is already created, so the lifetime extension rule doesn’t apply.
Case 2: std::initializer_list<int> il = {1, 2, 3}; (Safe)
This works correctly due to a specific lifetime extension rule in the C++ standard.
How it works:
When a temporary object is used to initialize a variable with an explicitly declared type (especially one that acts like a reference to the underlying data), the lifetime of that temporary object is extended to match the lifetime of the variable.
The process:
- You explicitly declare
ilasstd::initializer_list<int>(type is known upfront) - The temporary array
{1, 2, 3}is created - The compiler binds the temporary array to the
ilvariable’s scope - Lifetime extension rule applies: the temporary array’s lifetime is extended to match
il’s lifetime - The array is guaranteed to exist as long as
ilis in scope
Why it succeeds:
Because you explicitly declared the variable type, the compiler knows from the beginning that it needs to bind the temporary to this variable, and therefore applies the lifetime extension rule.
Unsafe Usage: Storing Beyond Lifetime
It is unsafe to store std::initializer_list beyond the lifetime of the initializer expression.
#include <iostream>
#include <initializer_list>
class BuggyContainer {
private:
std::initializer_list<int> stored_list; // DANGER!
public:
BuggyContainer(std::initializer_list<int> init) : stored_list(init) {
// Storing the initializer_list directly!
}
void print() const {
// UNDEFINED BEHAVIOR: The temporary array is gone!
for (int val : stored_list) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
int main() {
BuggyContainer container({1, 2, 3, 4, 5});
container.print(); // Undefined behavior - accessing dangling pointers!
return 0;
}
Why This Fails:
{1, 2, 3, 4, 5}creates a temporary arraystd::initializer_list<int>points to this temporary- After the constructor finishes, the temporary array is destroyed
stored_listnow contains dangling pointers- Accessing it in
print()causes undefined behavior
Correct Approach: Copy to an Owning Container
Always copy the elements to an owning container when you need to store them:
#include <iostream>
#include <initializer_list>
#include <vector>
class CorrectContainer {
private:
std::vector<int> data; // Owns the data
public:
CorrectContainer(std::initializer_list<int> init) : data(init) {
// Copy elements from initializer_list to vector
// Vector now owns the data
}
void print() const {
// Safe: accessing owned data
for (int val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
int main() {
CorrectContainer container({1, 2, 3, 4, 5});
container.print(); // Safe and correct!
return 0;
}
Advanced: std::initializer_list with Variadic Templates
std::initializer_list can be combined with variadic templates to create flexible initialization patterns.
Example: Generic Initialization Function
#include <iostream>
#include <initializer_list>
#include <vector>
#include <string>
// Using variadic templates for type-safe initialization
template<typename T>
std::vector<T> make_vector(std::initializer_list<T> init) {
return std::vector<T>(init);
}
// Using variadic templates with perfect forwarding
template<typename T, typename... Args>
std::vector<T> make_vector_variadic(Args&&... args) {
return std::vector<T>{std::forward<Args>(args)...};
}
int main() {
// Using initializer_list
auto vec1 = make_vector({1, 2, 3, 4, 5});
// Using variadic templates
auto vec2 = make_vector_variadic<int>(1, 2, 3, 4, 5);
for (int val : vec1) std::cout << val << " ";
std::cout << std::endl;
for (int val : vec2) std::cout << val << " ";
std::cout << std::endl;
return 0;
}
Example: Combining Both Approaches
#include <iostream>
#include <initializer_list>
#include <vector>
template<typename T>
class FlexibleContainer {
private:
std::vector<T> data;
public:
// Constructor with initializer_list
FlexibleContainer(std::initializer_list<T> init) : data(init) {
std::cout << "Constructed with initializer_list" << std::endl;
}
// Variadic template constructor
template<typename... Args>
FlexibleContainer(Args&&... args) {
std::cout << "Constructed with variadic template" << std::endl;
(data.push_back(std::forward<Args>(args)), ...); // C++17 fold expression
}
void print() const {
for (const auto& val : data) {
std::cout << val << " ";
}
std::cout << std::endl;
}
};
int main() {
// Uses initializer_list constructor (higher priority!)
FlexibleContainer<int> c1{1, 2, 3, 4, 5};
c1.print();
// Uses variadic template constructor
FlexibleContainer<int> c2(1, 2, 3, 4, 5);
c2.print();
return 0;
}
Why Use Variadic Templates Instead?
While std::initializer_list is great for homogeneous collections, variadic templates offer more flexibility:
#include <iostream>
#include <tuple>
// With initializer_list - all same type
void print_same_type(std::initializer_list<int> values) {
for (int val : values) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// With variadic templates - different types allowed
template<typename... Args>
void print_different_types(Args&&... args) {
((std::cout << args << " "), ...); // C++17 fold expression
std::cout << std::endl;
}
int main() {
print_same_type({1, 2, 3}); // All must be int
print_different_types(1, 2.5, "hello", 'x'); // Different types allowed!
return 0;
}
Best Practices Summary
- Use as function parameters for convenient initialization
- Use explicit type when declaring as local variable:
std::initializer_list<int> il = {1, 2, 3}; - Never use
autowith initializer lists:auto il = {1, 2, 3};creates dangling pointers - Copy to owning containers (like
std::vector) when you need to store data - Never store
std::initializer_listas a member variable - Be aware of constructor overload resolution with
{}vs()syntax - Consider variadic templates when you need heterogeneous types or perfect forwarding
Key Takeaways
std::initializer_listis a non-owning view over a temporary array- It’s syntax-driven (works only with
{}) and immutable - Never store it beyond the scope of its creation
- Always copy elements to an owning container for long-term storage
- Be mindful of constructor overload resolution priority
- Combine with variadic templates for more advanced patterns