Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. A temporary array of const T
  2. 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?

CodeMechanismLifetime RuleStatus
auto il = {1, 2, 3};auto deduction happens after temporary creationTemporary array destroyed at end of statementDangling
std::initializer_list<int> il = {1, 2, 3};Explicitly typed variable binds to the temporaryLifetime of temporary is extended to match ilSafe
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:

  1. The temporary array containing {1, 2, 3} is created
  2. auto type deduction happens (determines il should be std::initializer_list<int>)
  3. The std::initializer_list is constructed to point to the temporary array
  4. The statement ends (semicolon is reached)
  5. The temporary array is immediately destroyed (standard C++ lifetime rules)
  6. il is 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:

  1. You explicitly declare il as std::initializer_list<int> (type is known upfront)
  2. The temporary array {1, 2, 3} is created
  3. The compiler binds the temporary array to the il variable’s scope
  4. Lifetime extension rule applies: the temporary array’s lifetime is extended to match il’s lifetime
  5. The array is guaranteed to exist as long as il is 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. {1, 2, 3, 4, 5} creates a temporary array
  2. std::initializer_list<int> points to this temporary
  3. After the constructor finishes, the temporary array is destroyed
  4. stored_list now contains dangling pointers
  5. 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

  1. Use as function parameters for convenient initialization
  2. Use explicit type when declaring as local variable: std::initializer_list<int> il = {1, 2, 3};
  3. Never use auto with initializer lists: auto il = {1, 2, 3}; creates dangling pointers
  4. Copy to owning containers (like std::vector) when you need to store data
  5. Never store std::initializer_list as a member variable
  6. Be aware of constructor overload resolution with {} vs () syntax
  7. Consider variadic templates when you need heterogeneous types or perfect forwarding

Key Takeaways

  • std::initializer_list is 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