Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 7 – Classes, objects and RAII

Brace initialisation and initializer_list traps

Predict which constructor C++ braces call, see why vector(3, 7) and vector{3, 7} differ, and write a class that accepts a brace list safely.

  • Beginner
  • 15 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Predict which constructor a brace initialiser selects
  • Write a class that accepts a brace-enclosed list
  • Avoid the vector(3, 7) versus vector{3, 7} trap
  • Explain why move-only types cannot be moved out of an initializer_list

Before you start

What you’ll learn · Before you start

What you will learn

  • Predict which constructor a brace initialiser selects
  • Write a class that accepts a brace-enclosed list
  • Avoid the vector(3, 7) versus vector{3, 7} trap
  • Explain why move-only types cannot be moved out of an initializer_list

Before you start

On this page

std::vector<int>(3, 7) holds three 7s, while std::vector<int>{3, 7} holds two elements, 3 and 7. The braces made the difference: when a class has a constructor that takes a std::initializer_list: The standard type, from <initializer_list>, that a brace list such as {78, 91, 64} becomes when it initialises one: a small view of an array of const elements that the compiler makes from the braces. Copying it copies the view, not the elements., braces prefer that constructor to every other one. The standard chooses in two rounds: first it tries only the initializer-list constructors, with the whole brace list as their argument, and only if none of them can take it does it try every constructor with the elements as separate arguments. Empty braces are the exception: when the class has a default constructor, {} calls it.

Those rules explain every surprise in this lesson: strings that come out two characters long, a brace list that refuses to compile because of a narrowing conversion, and vectors that cannot hold move-only types from a list. Each one is run with Apple clang 21 and GCC 16, and the lesson ends with a class of your own that takes a brace list.

A brace list becomes a read-only view of an array

A brace list such as {78, 91, 64} has no type of its own. When it initialises a parameter or a variable of type std::initializer_list<int>, from the header <initializer_list>, the compiler creates an array of three const int and gives the code a small object that refers to it:

list_basics.cpp C++ · list_basics.cpp
// std::initializer_list is a small, read-only view of an array that the compiler
// builds from the braces.
#include <initializer_list>
#include <print>

int highest(std::initializer_list<int> values) {   // a function can take a list too
    int best = *values.begin();
    for (int v : values) {
        if (v > best) best = v;
    }
    return best;
}

int main() {
    std::initializer_list<int> marks = {78, 91, 64};
    std::println("size {}, first {}, highest {}", marks.size(), *marks.begin(), highest(marks));
    std::println("highest of a new list: {}", highest({12, 5, 40, 33}));
    std::println("sizeof(std::initializer_list<int>) = {}", sizeof(marks));

    std::initializer_list<int> copy = marks;   // copies the view, not the three ints
    std::println("same array: {}", copy.begin() == marks.begin());
}
A view of an array the compiler made Python · list_basics_session.py
from session import run

# list_basics.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra list_basics.cpp -o list_basics && ./list_basics
g++-16 -std=c++23 -Wall -Wextra list_basics.cpp -o list_basics-gcc && ./list_basics-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra list_basics.cpp -o list_basics && ./list_basics
size 3, first 78, highest 91
highest of a new list: 40
sizeof(std::initializer_list<int>) = 16
same array: true
$ g++-16 -std=c++23 -Wall -Wextra list_basics.cpp -o list_basics-gcc && ./list_basics-gcc
size 3, first 78, highest 91
highest of a new list: 40
sizeof(std::initializer_list<int>) = 16
same array: true

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 list_basics_session.py

Step through the program and watch what each list object holds, and what it only refers to:

Interactive · step by step What std::initializer_list<int> marks = {78, 91, 64} holds Step through list_basics.cpp. The left column holds the two list objects, the right one the array the compiler made from the braces.
What std::initializer_list<int> marks = {78, 91, 64} holds, step 5 of 5. Showing marks (16 bytes, size 3), copy (16 bytes, the same array), const int[3] (lives as long as marks). In focus: const int[3].refers torefers tomarks16 bytes, size 3copy16 bytes, the same arrayconst int[3]lives as long as marks

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The braces make an array. std::initializer_list<int> marks = {78, 91, 64}; first makes an array of three const int from the braces. The program never names that array.
  2. A small view of it. marks only refers to the array: where it starts and how many elements it has. That is why sizeof printed 16 with both standard libraries, whatever the length of the list.
  3. Read, never written. size(), begin() and a range-based for read the elements: the program printed size 3, first 78, highest 91. The elements are const, so nothing can change them.
  4. Copying the view. copy = marks copies where and how long, not the three ints, so both views refer to one array: the program printed same array: true.
  5. How long the array lives. A list variable such as marks keeps its array alive, as a reference keeps a temporary alive. A list passed to a function lives only until the end of that call's statement, so a class that keeps the values copies them.

A std::initializer_list offers size(), begin() and end(), so it works with a range-based for loop, and a function such as highest can take any number of values in one call. On both standard libraries it is 16 bytes on this 64-bit Mac whatever the list’s length, because it holds only where the array is and how long it is. Copying it copies those two values, which is why copy and marks share one array. The standard library uses the same idea in std::max({12, 5, 40}) and std::min.

Two properties of that array matter for classes. Its elements are const, so code that receives a list can read them but never change or move them. And the array lives only as long as a temporary would: for a list passed to a function, until the end of the statement that makes the call. A constructor that wants to keep the values must copy them into a member of its own, such as a std::vector. Both mistakes are caught by the compilers, the second one only partly:

const_elements.cpp C++ · const_elements.cpp
// The elements of an initializer_list are const, and a list member would dangle.
#include <initializer_list>

void double_all(std::initializer_list<int> values) {
    for (int& v : values) {   // the elements are const int: no int& can refer to them
        v *= 2;
    }
}

class Keeper {
public:
    Keeper() : kept_{1, 2, 3} {}   // the array would die when the constructor ends

private:
    std::initializer_list<int> kept_;
};

int main() {
    double_all({1, 2, 3});
    Keeper k;
    (void)k;
}
Const elements, and a list kept past its lifetime Python · const_elements_session.py
from session import run

# Only the error and warning lines, from each compiler.
run("""
clang++ -std=c++23 -Wall -Wextra -c const_elements.cpp 2>&1 | grep -E 'error:|warning:'
g++-16 -std=c++23 -Wall -Wextra -c const_elements.cpp 2>&1 | grep -E 'error:|warning:'
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c const_elements.cpp 2>&1 | grep -E 'error:|warning:'
const_elements.cpp:5:15: error: binding reference of type 'int' to value of type 'const int' drops 'const' qualifier
const_elements.cpp:12:21: error: backing array for 'std::initializer_list' member 'kept_' is a temporary object whose lifetime would be shorter than the lifetime of the constructed object
$ g++-16 -std=c++23 -Wall -Wextra -c const_elements.cpp 2>&1 | grep -E 'error:|warning:'
const_elements.cpp:5:19: error: binding reference of type 'int&' to 'const int' discards qualifiers
const_elements.cpp:12:16: warning: a temporary bound to 'Keeper::kept_' only persists until the constructor exits [-Wextra]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 const_elements_session.py

int& v cannot refer to a const int, so the loop that tries to double the elements is rejected by both compilers. Keeping the list itself as a member is worse, because the array would be destroyed when the constructor finishes and kept_ would refer to nothing. The standard makes it ill-formed. Clang rejects it with an error that says so; GCC 16 only warns, and only with -Wextra, that the temporary “only persists until the constructor exits”.

Checkpoint

Check: A brace list as a view of an array

  1. Question 1 of 2 marks holds three ints. How many bytes did sizeof(std::initializer_list<int>) print on this Mac, with both standard libraries?

    Type a number.

    A hint

    The object holds where the array starts and how long it is.

    Show the answer: Question 1 of 2

    Answer: 16 bytes

    16 bytes: a pointer and a length, or two pointers, 8 bytes each on a 64-bit Mac. The three ints are not inside the list object; they are in the array the compiler made from the braces.

  2. Question 2 of 2 After std::initializer_list<int> copy = marks;, what does copy.begin() == marks.begin() give?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: true, because copying a list copies the view, not the elements

    A std::initializer_list is a small view: copying it is cheap and gives a second view of the same const array.

Which constructor do braces call?

Probe has three constructors and prints the one that runs, so each line of main shows the choice for one way of writing an initialiser:

which_constructor.cpp C++ · which_constructor.cpp
// Which constructor does each initialisation call? Probe prints the one that runs.
#include <initializer_list>
#include <print>

class Probe {
public:
    Probe() { std::println("Probe()"); }
    Probe(int count, int value) { std::println("Probe(int, int) with {} and {}", count, value); }
    Probe(std::initializer_list<int> values) {
        std::println("Probe(std::initializer_list<int>) with {} values", values.size());
    }
};

int main() {
    std::print("a  "); Probe a;               // no initialiser
    std::print("b  "); Probe b{};             // empty braces
    std::print("c  "); Probe c(3, 7);         // parentheses
    std::print("d  "); Probe d{3, 7};         // braces with two ints
    std::print("e  "); Probe e{3};            // braces with one int
    std::print("f  "); Probe f = {1, 2, 3};   // = and braces
    std::print("g  "); Probe g({});           // an empty list, passed as the argument
    std::print("h  "); Probe h{{}};           // braces holding one empty pair of braces

    // The objects are not used again; these casts keep the compilers from warning about it.
    (void)a, (void)b, (void)c, (void)d, (void)e, (void)f, (void)g, (void)h;
}
The same choices with both compilers Python · which_constructor_session.py
from session import run

# which_constructor.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra which_constructor.cpp -o which_constructor && ./which_constructor
g++-16 -std=c++23 -Wall -Wextra which_constructor.cpp -o which_constructor-gcc && ./which_constructor-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra which_constructor.cpp -o which_constructor && ./which_constructor
a  Probe()
b  Probe()
c  Probe(int, int) with 3 and 7
d  Probe(std::initializer_list<int>) with 2 values
e  Probe(std::initializer_list<int>) with 1 values
f  Probe(std::initializer_list<int>) with 3 values
g  Probe(std::initializer_list<int>) with 0 values
h  Probe(std::initializer_list<int>) with 1 values
$ g++-16 -std=c++23 -Wall -Wextra which_constructor.cpp -o which_constructor-gcc && ./which_constructor-gcc
a  Probe()
b  Probe()
c  Probe(int, int) with 3 and 7
d  Probe(std::initializer_list<int>) with 2 values
e  Probe(std::initializer_list<int>) with 1 values
f  Probe(std::initializer_list<int>) with 3 values
g  Probe(std::initializer_list<int>) with 0 values
h  Probe(std::initializer_list<int>) with 1 values

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_constructor_session.py

Empty braces call the default constructor; otherwise initializer_list constructors are tried first, then every constructor.T object{...};Empty braces, and T has a defaultconstructor? Then T() is called(value-initialisation)Otherwise, round 1: only theinitializer_list constructors, withthe whole list. If one can take it,it is called, and a narrowingelement makes that an errorIf none can, round 2: everyconstructor, with the elementsas separate argumentsnot empty, or nodefault constructorno list constructorcan take the list

How a brace initialiser chooses a constructor

Text description of the diagram

The diagram is a column of three steps for an initialisation such as T object{...}; where T is a class with constructors.

  1. If the braces are empty and T has a default constructor, the default constructor is called. This is value-initialisation, and the initializer_list constructors are not considered.
  2. Otherwise, in round 1, overload resolution looks only at the initializer_list constructors and passes the whole brace list as one argument.
  3. If one of them can take the list, it is called. If an element needs a narrowing conversion to fit the list's element type, the program is ill-formed at this point: the compiler does not go on to round 2.
  4. If none of them can take the list, round 2 looks at every constructor and passes the elements as separate arguments, as parentheses would.

Read the output against the diagram:

  • a has no initialiser and b has empty braces. Both call Probe(): empty braces with a default constructor value-initialise the object, and the list constructor is not even considered.
  • c uses parentheses, which never look for an initializer-list constructor first, so it calls Probe(int, int).
  • d, e and f use braces with values, so round 1 offers them to the initializer-list constructor, and every int fits a std::initializer_list<int>. Probe(int, int) never had a chance, not even for d, where its two parameters match exactly. Writing = in front of the braces, as in f, changes nothing here.
  • g passes an empty list as an ordinary argument, so the list constructor runs with 0 values. h puts one pair of empty braces inside the outer ones: the outer braces make a list with one element, and that element, {}, is an int with the value 0.

When round 1 picks a constructor that cannot work

Round 1 asks only whether each element can be converted to the list’s element type. Whether that conversion is a narrowing conversion: A conversion that can lose information: a floating-point value to an integer type, double to float, an integer to a floating-point type, or an integer to an integer type that cannot hold all its values. Braces reject it unless the value is a constant that fits, never for floating-point to integer. More in “Variables and initialisation: =, () and {}” one, such as double to int, is checked afterwards, and by then the choice is made:

narrowing_trap.cpp C++ · narrowing_trap.cpp
// The initializer_list constructor wins the first round of overload resolution even
// when it needs a narrowing conversion, and then the initialisation is an error.
#include <initializer_list>

class Scale {
public:
    Scale(std::initializer_list<int> marks) : count_{static_cast<int>(marks.size())} {}
    Scale(double low, double high) : low_{low}, high_{high} {}

private:
    int count_ = 0;
    double low_ = 0.0;
    double high_ = 1.0;
};

int main() {
    Scale fine(0.5, 2.5);     // parentheses: Scale(double, double)
    Scale broken{0.5, 2.5};   // braces: the list constructor, double to int narrows
    (void)fine;
    (void)broken;
}
Braces pick the list constructor, then fail Python · narrowing_trap_session.py
from session import run

# Only the error lines, from each compiler.
run("""
clang++ -std=c++23 -Wall -Wextra -c narrowing_trap.cpp 2>&1 | grep 'error:'
g++-16 -std=c++23 -Wall -Wextra -c narrowing_trap.cpp 2>&1 | grep -E 'error:|warning:'
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c narrowing_trap.cpp 2>&1 | grep 'error:'
narrowing_trap.cpp:18:18: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
narrowing_trap.cpp:18:23: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
$ g++-16 -std=c++23 -Wall -Wextra -c narrowing_trap.cpp 2>&1 | grep -E 'error:|warning:'
narrowing_trap.cpp:18:26: error: narrowing conversion of '5.0e-1' from 'double' to 'int' [-Wnarrowing]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 narrowing_trap_session.py

Scale(double, double) would accept 0.5 and 2.5 exactly, yet braces never reach it: round 1 found the list constructor viable, so the narrowing of both values to int makes the program ill-formed. Clang reports each element; GCC reports the first. With parentheses, Scale fine(0.5, 2.5); calls the constructor that takes two doubles.

Now step through five initialisers of this lesson, the narrowing one and one from the next section included, and watch which question of the diagram settles each:

Interactive · stage by stage Five brace initialisers through the two rounds Step through five initialisers of this lesson. The stages are the questions the compiler asks, top to bottom; each line shows the answer for the initialiser in focus.
Five brace initialisers through the two rounds, step 5 of 5. Showing Braces initialise a class (Probe h{{}};), Empty braces? (no: it holds one element), Round 1 (list constructor, 1 value), Round 2 (each element an argument). In focus: Round 1.Braces initialise a classProbe h{{}};Empty braces?no: it holds one elementRound 1list constructor, 1 valueRound 2each element an argument

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Probe b{}. Probe b{}; has empty braces, and Probe has a default constructor, so the object is value-initialised: Probe() runs, and the list constructor is not even considered. Line b of the output is Probe().
  2. Probe d{3, 7}. Probe d{3, 7}; is not empty, so round 1 offers the whole list to the initializer-list constructors. Both ints fit std::initializer_list<int>, so it runs with 2 values, although Probe(int, int) matched exactly.
  3. Scale broken{0.5, 2.5}. Round 1 asks only whether each double converts to int. It does, so the list constructor is chosen, and then the narrowing makes the program ill-formed: the compiler never reaches round 2 and Scale(double, double).
  4. vector<string> f{3, "x"}. For std::vector<std::string> f{3, "x"}, round 1 finds no list constructor that can take the list, because 3 cannot become a std::string. Round 2 passes 3 and "x" as separate arguments: ["x", "x", "x"], as with parentheses.
  5. Probe h{{}}. Probe h{{}}; is not empty: the outer braces hold one element, {}, an int with the value 0. Round 1 takes it, and the list constructor runs with 1 value, line h of the output.

Checkpoint

Check: How braces choose a constructor

  1. Question 1 of 2 Probe has Probe(), Probe(int count, int value) and Probe(std::initializer_list<int>). Which one does Probe d{3, 7}; call?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Probe(std::initializer_list<int>), with 2 values

    Braces with values try the initializer-list constructors first, with the whole brace list as their argument. The program printed d Probe(std::initializer_list<int>) with 2 values.

  2. Question 2 of 2 With the same Probe, what does Probe h{{}}; call?

    Choose one answer.

    A hint

    Count the elements inside the outer braces.

    Show the answer: Question 2 of 2

    Answer: The list constructor, with 1 value

    The program printed h Probe(std::initializer_list<int>) with 1 values. One pair of braces inside another makes a list of one element, here 0.

Braces hold elements, parentheses hold arguments

Here are two numbers in parentheses and in braces, with three standard types:

containers.cpp C++ · containers.cpp
// The same two numbers in parentheses and in braces, with three standard types.
#include <print>
#include <string>
#include <vector>

int main() {
    std::vector<int> a(3, 7);               // count and value
    std::vector<int> b{3, 7};               // the elements 3 and 7
    std::string c(3, 'x');                  // count and character
    std::string d{3, 'x'};                  // the characters with codes 3 and 'x'
    std::vector<std::string> e(3, "x");     // count and value
    std::vector<std::string> f{3, "x"};     // 3 is not a string: count and value again
    std::vector<int> g(3);                  // three zeros
    std::vector<int> h{3};                  // the element 3

    std::println("a {}", a);
    std::println("b {}", b);
    std::println("c {:?} (size {})", c, c.size());
    std::println("d {:?} (size {})", d, d.size());
    std::println("e {}", e);
    std::println("f {}", f);
    std::println("g {}", g);
    std::println("h {}", h);
}
Parentheses and braces with vector and string Python · containers_session.py
from session import run

# containers.cpp built and run with Apple clang (libc++), then with GCC 16 (libstdc++).
run("""
clang++ -std=c++23 -Wall -Wextra containers.cpp -o containers && ./containers
g++-16 -std=c++23 -Wall -Wextra containers.cpp -o containers-gcc && ./containers-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra containers.cpp -o containers && ./containers
a [7, 7, 7]
b [3, 7]
c "xxx" (size 3)
d "\u{3}x" (size 2)
e ["x", "x", "x"]
f ["x", "x", "x"]
g [0, 0, 0]
h [3]
$ g++-16 -std=c++23 -Wall -Wextra containers.cpp -o containers-gcc && ./containers-gcc
a [7, 7, 7]
b [3, 7]
c "xxx" (size 3)
d "\u{3}x" (size 2)
e ["x", "x", "x"]
f ["x", "x", "x"]
g [0, 0, 0]
h [3]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 containers_session.py

What the same values mean in parentheses and in braces (all the types are in std)
Criterion With ( )With { }
vector<int>, 3, 7 7, 7, 73, 7
string, 3, 'x' "xxx"code 3, then 'x'
vector<string>, 3, "x" "x", "x", "x""x", "x", "x"
vector<int>, 3 0, 0, 03

The string case is the nastiest, because it compiles silently. In std::string d{3, 'x'} the 3 is a constant that fits in a char, so it is not a narrowing conversion, and the initializer-list constructor builds a string from the character with code 3, an invisible control character, followed by x. The debug format {:?}, added to C++23 with paper P2286R8, escapes it as \u{3}, which is how the output shows it. The std::vector<std::string> case goes the other way: 3 cannot become a std::string, so no list constructor is viable and round 2 calls the count and value constructor, exactly like the parentheses.

Key idea

Use braces when the values inside are the elements, and parentheses when they are arguments such as a count, a size or a fill value.

Rule ES.23 of the C++ Core Guidelines prefers braces in general and names this exception for containers.

Checkpoint

Check: Braces for elements, parentheses for arguments

  1. Question 1 of 2 What do g and h hold?

    Read the code, then choose one answer.

    std::vector<int> g(3);
    std::vector<int> h{3};
    Show the answer: Question 1 of 2

    Answer: g holds 0, 0, 0 and h holds 3

    With parentheses the 3 is an argument, a count. With braces it is an element. containers.cpp printed exactly this.

  2. Question 2 of 2 How many characters does s hold?

    Read the code, then choose one answer.

    std::string s{3, 'x'};
    Show the answer: Question 2 of 2

    Answer: 2

    The initializer_list<char> constructor wins, and 3 converts to the char with code 3 without narrowing, because it is a constant that fits. s holds that control character and 'x'.

A class that takes a brace list copies it into a member

Give a class an initializer-list constructor: A constructor whose first parameter is a std::initializer_list<E>, with no other parameters or only ones that have defaults, as in Marks(std::initializer_list<int> marks). Braces with values try these constructors first. when its objects are naturally built from a list of values, the way a container is. Marks checks each mark and keeps its own copy:

marks.cpp C++ · marks.cpp
// A class that takes a brace-enclosed list of marks, checks each one, and keeps
// its own copy in a std::vector.
#include <initializer_list>
#include <print>
#include <stdexcept>
#include <vector>

class Marks {
public:
    Marks() = default;   // no marks yet

    Marks(std::initializer_list<int> marks) : marks_(marks) {   // parentheses: vector's list constructor
        for (int m : marks_) {
            if (m < 0 || m > 100) throw std::invalid_argument{"a mark must be between 0 and 100"};
        }
    }

    double mean() const {
        if (marks_.empty()) return 0.0;
        double sum = 0;
        for (int m : marks_) sum += m;
        return sum / static_cast<double>(marks_.size());
    }

    int count() const { return static_cast<int>(marks_.size()); }

private:
    std::vector<int> marks_;   // a copy: the list's own array is gone after the constructor
};

int main() {
    Marks maths{78, 91, 64};
    Marks physics = {55, 70};
    Marks none{};             // empty braces: the default constructor, not the list one
    std::println("maths:   {} marks, mean {:.1f}", maths.count(), maths.mean());
    std::println("physics: {} marks, mean {:.1f}", physics.count(), physics.mean());
    std::println("none:    {} marks, mean {:.1f}", none.count(), none.mean());
    try {
        Marks typo{78, 910};
    } catch (const std::invalid_argument& error) {
        std::println("refused: {}", error.what());
    }
}
Marks from three different brace forms Python · marks_session.py
from session import run

# marks.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra marks.cpp -o marks && ./marks
g++-16 -std=c++23 -Wall -Wextra marks.cpp -o marks-gcc && ./marks-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra marks.cpp -o marks && ./marks
maths:   3 marks, mean 77.7
physics: 2 marks, mean 62.5
none:    0 marks, mean 0.0
refused: a mark must be between 0 and 100
$ g++-16 -std=c++23 -Wall -Wextra marks.cpp -o marks-gcc && ./marks-gcc
maths:   3 marks, mean 77.7
physics: 2 marks, mean 62.5
none:    0 marks, mean 0.0
refused: a mark must be between 0 and 100

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_session.py

Four details in this class are worth copying:

  • The constructor copies the list into a member. marks_(marks) uses parentheses on purpose: it passes the list to std::vector’s own list constructor. The array behind marks is gone once the constructor returns; marks_ is not.
  • Validation happens once, in the constructor, as in the previous lesson. A typo such as 910 is refused before any Marks object exists.
  • A default constructor is declared too, so Marks none{}; means “no marks” and calls Marks(). Without it, {} would call the list constructor with an empty list, which here gives the same result.
  • The list constructor is not explicit. Marks physics = {55, 70}; reads naturally, and a list of marks is exactly what a Marks is, so there is no surprising conversion to prevent.

Decide on a list constructor when you design a class, not later: because round 1 prefers it, adding one to a class that is already in use changes what some existing brace initialisations of that class mean.

Checkpoint

Check: Writing a list constructor

  1. Question 1 of 2 Marks has Marks() = default; and a list constructor. Which one does Marks none{}; call?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The default constructor

    The program printed none: 0 marks, through the default constructor. Without one, {} would have called the list constructor with an empty list, which here gives the same result.

  2. Question 2 of 2 Why does the Marks constructor copy the list into a std::vector<int> member instead of keeping the std::initializer_list itself?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The array behind the list is gone once the constructor returns, and the vector keeps its own copy

    Keeping the list itself as a member is ill-formed: Clang rejects it, and GCC 16 warns with -Wextra that the temporary only persists until the constructor exits. A std::vector member owns its elements.

Lists copy their elements

Because the elements of a list are const, a container built from braces copies each one out of the list. For cheap values that does not matter. For larger objects it is a copy per element, and for a type that cannot be copied it is a compile error. Counted counts how often it is made and copied:

copies.cpp C++ · copies.cpp
// Building a vector from braces copies every element out of the list.
#include <print>
#include <vector>

struct Counted {
    static inline int made = 0;
    static inline int copied = 0;
    Counted() { ++made; }
    Counted(const Counted&) { ++copied; }
};

int main() {
    std::vector<Counted> from_braces{Counted{}, Counted{}, Counted{}};
    std::println("from braces:  {} made, {} copied", Counted::made, Counted::copied);

    Counted::made = 0;
    Counted::copied = 0;
    std::vector<Counted> grown;
    grown.reserve(3);
    for (int i = 0; i < 3; ++i) grown.emplace_back();   // built in place, in the vector
    std::println("emplace_back: {} made, {} copied", Counted::made, Counted::copied);
}
Three copies from braces, none from emplace_back Python · copies_session.py
from session import run

# copies.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra copies.cpp -o copies && ./copies
g++-16 -std=c++23 -Wall -Wextra copies.cpp -o copies-gcc && ./copies-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra copies.cpp -o copies && ./copies
from braces:  3 made, 3 copied
emplace_back: 3 made, 0 copied
$ g++-16 -std=c++23 -Wall -Wextra copies.cpp -o copies-gcc && ./copies-gcc
from braces:  3 made, 3 copied
emplace_back: 3 made, 0 copied

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 copies_session.py

From braces, each of the three objects is created once as an element of the list and copied once more into the vector: 3 made and 3 copied, with both libraries. reserve followed by emplace_back, which builds each element directly inside the vector, makes 3 and copies none. std::unique_ptr, a move-only type: A type whose objects can be moved but not copied, because its copy constructor is deleted, such as std::unique_ptr. Its objects cannot come out of a std::initializer_list, whose elements can only be copied. that owns its object, shows the hard limit:

move_only.cpp C++ · move_only.cpp
// std::unique_ptr can be moved but never copied, so it cannot come out of a list.
#include <memory>
#include <vector>

int main() {
    std::vector<std::unique_ptr<int>> owners{std::make_unique<int>(1), std::make_unique<int>(2)};
    return static_cast<int>(owners.size());
}
The deleted copy constructor, found deep in the vector Python · move_only_session.py
from session import run

# Only the lines that name the deleted copy constructor, from each compiler.
run("""
clang++ -std=c++23 -Wall -Wextra -c move_only.cpp 2>&1 | grep deleted
g++-16 -std=c++23 -Wall -Wextra -c move_only.cpp 2>&1 | grep deleted
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -c move_only.cpp 2>&1 | grep deleted
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__memory/construct_at.h:43:58: note: candidate template ignored: substitution failure [with _Tp = std::unique_ptr<int>, _Args = <const std::unique_ptr<int> &>]: call to implicitly-deleted copy constructor of 'std::unique_ptr<int>'
$ g++-16 -std=c++23 -Wall -Wextra -c move_only.cpp 2>&1 | grep deleted
/opt/homebrew/Cellar/gcc/16.2.0/include/c++/16/bits/stl_construct.h:133:7: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]'

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 move_only_session.py

Both errors point into the standard library, because that is where the copy happens, and both name the reason: the copy constructor of std::unique_ptr is deleted. The module on move semantics and smart pointers explains std::unique_ptr itself.

Filling a vector of owners from braces

std::vector<std::unique_ptr<int>> owners{std::make_unique<int>(1), std::make_unique<int>(2)}; cannot compile, because the vector would have to copy each owner out of the list. Create the vector empty and add each one: owners.push_back(std::make_unique<int>(1));, or emplace_back.

Checkpoint

Check: Lists copy their elements

  1. Question 1 of 2 std::vector<Counted> from_braces{Counted{}, Counted{}, Counted{}}; How many copies did Counted count?

    Type a number.

    A hint

    The elements of a list are const.

    Show the answer: Question 1 of 2

    Answer: 3 copies

    Each Counted was made once as an element of the list and copied once more into the vector: 3 made, 3 copied, with both libraries. reserve and emplace_back made 3 and copied none.

  2. Question 2 of 2 std::vector<std::unique_ptr<int>> owners{std::make_unique<int>(1), std::make_unique<int>(2)}; does not compile. What is the fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Create the vector empty, then add each owner with push_back or emplace_back

    unique_ptr's copy constructor is deleted, which is what both compilers reported deep in the standard library. Adding the owners one by one moves them in instead.

Practise: a SmallSet that keeps its order

SmallSet keeps a set of small integers in increasing order without duplicates. It has a constructor for a range, SmallSet(1, 5) for the numbers 1 to 5, and a list constructor that should keep the invariant but does not yet:

The starter code and its sample tests

C header · small_set.h

// Starter code for "Your turn": finish the list constructor of SmallSet.
#pragma once
#include <algorithm>
#include <format>
#include <initializer_list>
#include <string>
#include <vector>

// A set of small integers, kept in increasing order without duplicates.
class SmallSet {
public:
    SmallSet() = default;   // the empty set

    // Every integer from low to high, both included: SmallSet(1, 5) holds 1, 2, 3, 4, 5.
    SmallSet(int low, int high) {
        for (int v = low; v <= high; ++v) items_.push_back(v);
    }

    // TODO: keep the values in increasing order and drop duplicates, so that
    // SmallSet{3, 1, 3, 2} holds 1, 2, 3.
    SmallSet(std::initializer_list<int> values) : items_(values) {}

    int size() const { return static_cast<int>(items_.size()); }

    bool contains(int value) const {
        return std::find(items_.begin(), items_.end(), value) != items_.end();
    }

    // "{1, 2, 3}"
    std::string text() const {
        std::string out = "{";
        for (int i = 0; i < size(); ++i) {
            if (i > 0) out += ", ";
            out += std::format("{}", items_[i]);
        }
        return out + "}";
    }

private:
    std::vector<int> items_;   // the invariant: strictly increasing
};

C++ · small_set_tests.cpp

// Sample tests for small_set.h. Build: clang++ -std=c++23 -Wall -Wextra small_set_tests.cpp -o small_set_tests
#include <print>
#include "small_set.h"

int passed = 0;
int failed = 0;

void expect(bool ok, const char* what) {
    if (ok) {
        ++passed;
    } else {
        ++failed;
        std::println("FAIL {}", what);
    }
}

int main() {
    expect(SmallSet{}.text() == "{}", "empty braces make the empty set");
    expect(SmallSet{3, 1, 3, 2}.text() == "{1, 2, 3}", "{3, 1, 3, 2} holds 1, 2, 3");
    expect(SmallSet{3, 1, 3, 2}.size() == 3, "{3, 1, 3, 2} has 3 elements");
    expect(SmallSet{5}.text() == "{5}", "{5} holds only 5");
    expect(SmallSet{1, 5}.text() == "{1, 5}", "braces: the list 1, 5");
    expect(SmallSet(1, 5).text() == "{1, 2, 3, 4, 5}", "parentheses: the range 1 to 5");
    expect(SmallSet(5, 1).text() == "{}", "an empty range");

    SmallSet sevens = {7, 7, 7};
    expect(sevens.size() == 1, "= {7, 7, 7} holds one 7");

    SmallSet mixed{9, -2, 9};
    expect(mixed.contains(-2) && !mixed.contains(3), "contains finds -2 and not 3");
    expect(mixed.text() == "{-2, 9}", "{9, -2, 9} holds -2, 9");

    std::println("{} of {} checks passed", passed, passed + failed);
    return failed == 0 ? 0 : 1;
}
The sample tests against the starter code Python · small_set_session.py
from session import run

# The sample tests against the starter code, before the TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra small_set_tests.cpp -o small_set_tests && ./small_set_tests
g++-16 -std=c++23 -Wall -Wextra small_set_tests.cpp -o small_set_tests-gcc && ./small_set_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra small_set_tests.cpp -o small_set_tests && ./small_set_tests
FAIL {3, 1, 3, 2} holds 1, 2, 3
FAIL {3, 1, 3, 2} has 3 elements
FAIL = {7, 7, 7} holds one 7
FAIL {9, -2, 9} holds -2, 9
6 of 10 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra small_set_tests.cpp -o small_set_tests-gcc && ./small_set_tests-gcc
FAIL {3, 1, 3, 2} holds 1, 2, 3
FAIL {3, 1, 3, 2} has 3 elements
FAIL = {7, 7, 7} holds one 7
FAIL {9, -2, 9} holds -2, 9
6 of 10 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 small_set_session.py

Change the list constructor until both compilers print 10 of 10 checks passed. Copy the values into items_, sort them with std::ranges::sort(items_), then remove the repeats: std::ranges::unique(items_) closes up the values it keeps at the front of the vector and returns what is left over behind them, the tail that items_.erase(tail.begin(), tail.end()) removes. Its values are leftovers, not necessarily the repeats, so never read them.

Two of the tests check the trap of this lesson from the other side. SmallSet(1, 5) must hold five numbers and SmallSet{1, 5} two, and they pass already, because the starter’s constructors are chosen correctly. Before you run the tests, predict what SmallSet{5, 1} holds once your constructor sorts, and add a check for it; SmallSet(5, 1), an empty range, has one already.

Then put the brace rules to work on new cases. Practice starts with the rule you have answered worst so far, from the 16-byte view to the copies a list makes, and each wrong choice names the slip behind it:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 void double_all(std::initializer_list<int> values) tries for (int& v : values) v *= 2;. What happens?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It does not compile, because the elements are const int

    Code that receives a list can read its elements but never change or move them. Copy them into a container of your own to change them.

  2. Question 2 of 10 A class keeps std::initializer_list<int> kept_ and initialises it from {1, 2, 3} in its constructor. What did the two compilers say?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Clang refused it with an error; GCC 16 only warned, and only with -Wextra

    The array would die when the constructor finishes, and kept_ would refer to nothing. Copy the values into a std::vector member instead.

  3. Question 3 of 10 A class has a default constructor and an initializer_list constructor. Which one does Widget w{}; call?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: The default constructor

    Empty braces with a default constructor value-initialise the object, and the initializer_list constructors are not considered. Widget w({}); would pass an empty list to the list constructor.

  4. Question 4 of 10 What happens on the last line?

    Read the code, then choose one answer.

    struct Scale {
        Scale(std::initializer_list<int> marks);
        Scale(double low, double high);
    };
    Scale s{0.5, 2.5};
    Show the answer: Question 4 of 10

    Answer: It does not compile, because the list constructor is chosen and double to int narrows

    A double can be converted to an int, so round 1 finds the initializer_list constructor viable and picks it. Only then is the narrowing checked, and it makes the program ill-formed; the compiler never goes back to round 2. Scale s(0.5, 2.5) calls the other constructor.

  5. Question 5 of 10 What does each vector hold?

    Read the code, then choose one answer.

    std::vector<int> p(3, 7);
    std::vector<int> q{3, 7};
    Show the answer: Question 5 of 10

    Answer: p holds 7, 7, 7 and q holds 3, 7

    Parentheses call the constructor that takes a count and a value. Braces try the initializer_list constructor first, and a list of ints can initialise a std::initializer_list<int>, so q holds the two elements.

  6. Question 6 of 10 What does v hold?

    Read the code, then choose one answer.

    std::vector<std::string> v{3, "x"};
    Show the answer: Question 6 of 10

    Answer: Three strings, each "x"

    Round 1 tries the initializer_list<std::string> constructor, but 3 cannot become a std::string, so no list constructor is viable. Round 2 then calls the count and value constructor, so braces and parentheses mean the same thing here.

  7. Question 7 of 10 A class in use has Grid(int rows, int cols), and someone adds Grid(std::initializer_list<int> values). What happens to existing code that says Grid g{3, 4};?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: It now calls the list constructor with 2 values, without any error or warning

    Adding a list constructor to a class that is already in use changes what some existing brace initialisations mean. Decide on a list constructor when you design the class.

  8. Question 8 of 10 Why is the list constructor of Marks not explicit?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: A list of marks is exactly what a Marks is, so Marks physics = {55, 70}; surprises nobody

    Rule C.46 makes explicit the default for single-argument constructors, with an exception for conversions that are lossless and expected. A container-like class built from a list is that exception.

  9. Question 9 of 10 Why does std::vector<std::unique_ptr<int>> v{std::make_unique<int>(1)}; fail to compile?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The vector copies its elements out of the list, and a unique_ptr cannot be copied

    The elements of a std::initializer_list are const, so the vector's constructor can only copy them, and unique_ptr's copy constructor is deleted. Create the vector empty and call push_back or emplace_back for each element instead.

  10. Question 10 of 10 grown.reserve(3); and then three calls of grown.emplace_back();. How many copies of Counted were made?

    Type a number.

    A hint

    Where does emplace_back build each element?

    Show the answer: Question 10 of 10

    Answer: 0 copies

    emplace_back builds each element directly inside the vector, and reserve made room for all three first, so nothing is moved or copied: the program printed emplace_back: 3 made, 0 copied.

Interview questions

Fresher · What does std::vector<int> v{3, 7}; contain, and how is that different from v(3, 7)?

v{3, 7} holds two elements, 3 and 7, while v(3, 7) holds three elements, each 7. When braces initialise a class that has an initializer-list constructor, overload resolution first considers only those constructors, with the whole brace list as the argument; a list of ints fits std::initializer_list<int>, so the vector’s list constructor wins even though the count and value constructor matches the two ints exactly. Parentheses never give list constructors priority. A strong answer adds the exceptions: empty braces call the default constructor, and when the elements cannot convert to the list’s type, as in std::vector<std::string> s{3, "x"}, the other constructors are tried and the braces mean the same as parentheses.

An interviewer usually tests both exceptions on a fresh case. Decide each one:

Checkpoint

Interview round: two follow-up questions

  1. Question 1 of 2 Follow-up: what does std::vector<int> v{}; contain, and which constructor made it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No elements; the default constructor, because empty braces value-initialise

    Empty braces with a default constructor value-initialise, so v.size() is 0. To pass an empty list to the list constructor, write std::vector<int> v({});.

  2. Question 2 of 2 Follow-up: what does names hold?

    Read the code, then choose one answer.

    std::vector<std::string> names{2};
    Show the answer: Question 2 of 2

    Answer: Two empty strings

    The same exception as std::vector<std::string> f{3, "x"}: when no element can convert to the list's type, braces mean the same as parentheses.

Brace lists also go wrong in larger code, as in these two cases: a helper that returns a brace list and loses its values in a release build, and a list constructor added to a widely used Matrix class:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid

    A helper std::initializer_list<int> default_limits() { return {1, 2, 3}; } passes every unit test, but a release build sums the limits to 0 instead of 6. Explain why, say what the compilers and tools report, and give two safer designs.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior

    You maintain a widely used Matrix class with a Matrix(int rows, int cols) constructor and want to add a constructor that takes a brace list of values. What can break for existing users, how do you find it, and how would you design the API to avoid the problem?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A std::initializer_list<T> is a 16-byte view of an array of const T that the compiler makes from the braces; copying it copies the view, and a class that keeps the values copies them into a member of its own.
  • Braces try the initializer-list constructors first, with the whole list, and fall back to the other constructors only when none can take it; empty braces with a default constructor call the default constructor, and a narrowing element turns round 1’s choice into an error.
  • std::vector<int>(3, 7) is three 7s, std::vector<int>{3, 7} is 3 and 7, and std::string{3, 'x'} is two characters, so braces are for elements and parentheses for counts and sizes.
  • A list constructor checks and copies its values, sits beside a default constructor for {}, and belongs in the first design of a class, because adding one later changes what existing brace initialisations mean.
  • Elements are copied out of a list, so a vector built from braces copies every element, and move-only types such as std::unique_ptr go into a vector with push_back or emplace_back.

Terms in this lesson

initializer-list constructor
A constructor whose first parameter is a std::initializer_list<E>, with no other parameters or only ones that have defaults, as in Marks(std::initializer_list<int> marks). Braces with values try these constructors first.
move-only type
A type whose objects can be moved but not copied, because its copy constructor is deleted, such as std::unique_ptr. Its objects cannot come out of a std::initializer_list, whose elements can only be copied.
std::initializer_list
The standard type, from <initializer_list>, that a brace list such as {78, 91, 64} becomes when it initialises one: a small view of an array of const elements that the compiler makes from the braces. Copying it copies the view, not the elements.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.