Skip to content
C++
Library
since C++11
Intermediate

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++11

A 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):

MemberReturn typeNotes
begin()const T*Pointer to first element
end()const T*One past the last element
size()std::size_tNumber 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:

cpp
#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 constructor

Functions 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:

cpp
#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.75

Examples

Forwarding to a container member

A common pattern is an initializer-list constructor that forwards directly into an internal std::vector:

cpp
#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

cpp
#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:

cpp
#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++17

Best 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:

cpp
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:

cpp
std::vector<int> v1(10, 0);   // regular constructor: 10 zero-filled elements
std::vector<int> v2{10, 0};   // initializer-list constructor: {10, 0}, 2 elements

The 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>:

cpp
// 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:

cpp
// 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 type

This 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:

cpp
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:

cpp
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_list objects
  • std::min / std::max β€” standard overloads accepting std::initializer_list<T> (C++11)