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.
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:
// 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());
} 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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:
// 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;
} 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
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 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;
} 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
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.
- 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.
- Otherwise, in round 1, overload resolution looks only at the initializer_list constructors and passes the whole brace list as one argument.
- 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.
- 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:
ahas no initialiser andbhas empty braces. Both callProbe(): empty braces with a default constructor value-initialise the object, and the list constructor is not even considered.cuses parentheses, which never look for an initializer-list constructor first, so it callsProbe(int, int).d,eandfuse braces with values, so round 1 offers them to the initializer-list constructor, and everyintfits astd::initializer_list<int>.Probe(int, int)never had a chance, not even ford, where its two parameters match exactly. Writing=in front of the braces, as inf, changes nothing here.gpasses an empty list as an ordinary argument, so the list constructor runs with 0 values.hputs one pair of empty braces inside the outer ones: the outer braces make a list with one element, and that element,{}, is anintwith 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:
// 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;
} 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:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- 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().
- 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.
- 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).
- 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.
- 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
Braces hold elements, parentheses hold arguments
Here are two numbers in parentheses and in braces, with three standard types:
// 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);
} 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
| Criterion | With ( ) | With { } |
|---|---|---|
| vector<int>, 3, 7 | 7, 7, 7 | 3, 7 |
| string, 3, 'x' | "xxx" | code 3, then 'x' |
| vector<string>, 3, "x" | "x", "x", "x" | "x", "x", "x" |
| vector<int>, 3 | 0, 0, 0 | 3 |
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
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:
// 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());
}
} 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 tostd::vector’s own list constructor. The array behindmarksis 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
Marksobject exists. - A default constructor is declared too, so
Marks none{};means “no marks” and callsMarks(). 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 aMarksis, 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
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:
// 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);
} 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:
// 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());
} 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
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:
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;
} 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.
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
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.
-
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.
-
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?
Key takeaways
- A
std::initializer_list<T>is a 16-byte view of an array ofconst Tthat 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, andstd::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_ptrgo into a vector withpush_backoremplace_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
- Working Draft, Programming Languages: C++ (N4950): [dcl.init.list], [over.match.list], [support.initlist], [alg.unique] (ISO/IEC JTC1/SC22/WG21)
- N2672: Initializer List proposed wording (ISO/IEC JTC1/SC22/WG21)
- P2286R8: Formatting Ranges (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: ES.23, prefer the {}-initializer syntax (Standard C++ Foundation)
- Diagnostic flags in Clang (-Wc++11-narrowing, -Wdangling-initializer-list) (LLVM Project)
- Options Controlling C++ Dialect (-Wnarrowing) (GNU Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress