C++ Module 7 – Classes, objects and RAII
Constructors, member initialiser lists and explicit
Write C++ constructors that initialise members in the initialiser list, avoid declaration-order bugs, and use explicit to stop silent conversions.
What you will learn
- Write member initialiser lists that initialise every member
- Avoid member initialisation order bugs
- Prevent unwanted conversions with explicit
- Reuse constructor code with delegating constructors and default member initialisers
Before you start
What you’ll learn · Before you start
What you will learn
- Write member initialiser lists that initialise every member
- Avoid member initialisation order bugs
- Prevent unwanted conversions with explicit
- Reuse constructor code with delegating constructors and default member initialisers
Before you start
On this page
Percent wrong{140}; should never produce a percentage of 140, and pay_fee(500) should not quietly turn an int
into 500 paise of Money. A constructor: A member function with the class's name and no return type, run when an object is created. It gives the members their first values and makes the object valid, or throws when it cannot, and then no object exists. is where a class prevents both: a member
function with the class’s name and no return type that runs when an object is created, and makes it valid or refuses.
Its member initialiser list: The part of a constructor after the colon, as in Percent(int value) : value_{value} {}. It initialises the named members before the body starts, in the order the members are declared, not the order of the list., the part after the colon in
Percent(int value) : value_{value} {}, initialises the members before the body starts.
Three rules explain most constructor bugs, and each one runs below with Apple clang 21 and GCC 16. Members are
initialised in the order they are declared in the class, whatever order the list names them in. A member that is
only assigned in the body has already been initialised once. And a constructor with one parameter doubles as a silent
conversion unless it is marked explicit.
A constructor makes the object valid
The previous lesson’s account started valid because its one member had a default of 0. A percentage needs a value from the caller, and a rule about that value:
// A constructor sets up the invariant before anyone can use the object.
#include <print>
#include <stdexcept>
class Percent {
public:
Percent() = default; // 0 %, from the default member initialiser below
Percent(int value) : value_{value} { // the member initialiser list sets value_
if (value < 0 || value > 100) {
throw std::invalid_argument{"a percentage must be between 0 and 100"};
}
}
int value() const { return value_; }
private:
int value_ = 0; // the invariant: 0 <= value_ <= 100
};
int main() {
Percent none;
Percent attendance{85};
std::println("none = {}, attendance = {}", none.value(), attendance.value());
try {
Percent wrong{140}; // the constructor throws, so no object called wrong exists
std::println("not printed: {}", wrong.value());
} catch (const std::invalid_argument& error) {
std::println("refused: {}", error.what());
}
} from session import run
# percent.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra percent.cpp -o percent && ./percent
g++-16 -std=c++23 -Wall -Wextra percent.cpp -o percent-gcc && ./percent-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra percent.cpp -o percent && ./percent none = 0, attendance = 85 refused: a percentage must be between 0 and 100 $ g++-16 -std=c++23 -Wall -Wextra percent.cpp -o percent-gcc && ./percent-gcc none = 0, attendance = 85 refused: a percentage 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 percent_session.py
Percent attendance{85}; calls the constructor that takes an int. Its member initialiser list gives value_ the
argument, then the body checks the rule. For 140 the body cannot make a valid object, so it throws an exception,
which ends the constructor at once. Step through both calls:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The call. Percent attendance{85}; calls Percent(int). Until the constructor finishes, there is no attendance object that other code could use.
- The list first. The member initialiser list runs before the body: value_{value} gives value_ the argument, 85.
- The body checks. The body tests the rule. 85 is between 0 and 100, so the body ends without throwing.
- A valid object. The constructor returns, and attendance exists, valid from its first moment: the program printed none = 0, attendance = 85.
- 140 this time. Percent wrong{140}; runs the same constructor. The list gives value_ the argument 140 before the body sees it.
- The body throws. The test fails, so the body throws std::invalid_argument, which ends the constructor at once.
- No object at all. No object called wrong ever exists, and the line that would print it never runs. The catch block in main printed refused: a percentage must be between 0 and 100.
The object called wrong never exists, the line that would print it never runs, and the catch block in main
receives the error instead. Rules C.40 and C.42 of the C++ Core Guidelines say exactly this: give a class with an
invariant a constructor, and throw when that constructor cannot establish the invariant. The module on error handling
covers exceptions in full; here they are the constructor’s way to say no.
Percent none; uses the other constructor. Declaring any constructor stops the compiler from writing the
default constructor: A constructor that can be called with no arguments, as in Percent none;. The compiler writes one only for a class that declares no constructor; T() = default; asks for it back., the one that takes no arguments, by itself. The line
Percent() = default; asks for it back, and it gives value_ its default member initialiser, 0. Without such a
line, creating an object with no arguments fails:
// Declaring any constructor stops the compiler from writing the default one.
class Seat {
public:
Seat(char row, int number) : row_{row}, number_{number} {}
private:
char row_ = 'A';
int number_ = 1;
};
int main() {
Seat chosen{'C', 12}; // fine
Seat any; // rejected: there is no Seat() any more
(void)chosen;
(void)any;
} from session import run
# Only the error lines, from each compiler.
run("""
clang++ -std=c++23 -Wall -Wextra -c no_default.cpp 2>&1 | grep 'error:'
g++-16 -std=c++23 -Wall -Wextra -c no_default.cpp 2>&1 | grep 'error:'
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c no_default.cpp 2>&1 | grep 'error:' no_default.cpp:13:10: error: no matching constructor for initialization of 'Seat' $ g++-16 -std=c++23 -Wall -Wextra -c no_default.cpp 2>&1 | grep 'error:' no_default.cpp:13:10: error: no matching function for call to 'Seat::Seat()'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_default_session.py
A user-declared constructor also means the class is no longer an aggregate, so braces now always call a constructor instead of filling the members one by one, as they did for the structs of an earlier lesson.
Checkpoint
Check: A constructor makes the object valid
Members are initialised before the body runs
By the time the body of a constructor starts, every member has been initialised already: from the member initialiser
list if the list names it, otherwise from its default member initialiser, and otherwise by default-initialisation,
which for a class type calls its default constructor and for an int leaves no value at all. An assignment in the
body is therefore a second step. The member type Tracer below prints what is done to it, which makes the steps
visible:
C++ · init_or_assign.cpp
// Two ways to give a member its value: the member initialiser list, or an
// assignment in the constructor body.
#include <print>
#include "tracer.h"
class Assigned {
public:
Assigned(const char* title) {
title_ = Tracer{title}; // too late: title_ was default-constructed already
}
private:
Tracer title_;
};
class Initialised {
public:
Initialised(const char* title) : title_{title} {} // built once, with its value
private:
Tracer title_;
};
int main() {
std::println("Assigned:");
Assigned a{"report"};
std::println("Initialised:");
Initialised b{"report"};
} C header · tracer.h
// A member type that reports what is done to it, for the examples of this lesson.
#pragma once
#include <print>
#include <string>
class Tracer {
public:
Tracer() { std::println(" {}: default-constructed", name_); }
Tracer(const char* name) : name_{name} { std::println(" {}: constructed", name_); }
Tracer(const Tracer& other) : name_{other.name_} { std::println(" {}: copied", name_); }
Tracer& operator=(const Tracer& other) {
std::println(" {}: assigned the value {}", name_, other.name_);
name_ = other.name_;
return *this;
}
private:
std::string name_ = "(no name)";
}; from session import run
# What happens to the member in each constructor, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra init_or_assign.cpp -o init_or_assign && ./init_or_assign
g++-16 -std=c++23 -Wall -Wextra init_or_assign.cpp -o init_or_assign-gcc && ./init_or_assign-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra init_or_assign.cpp -o init_or_assign && ./init_or_assign Assigned: (no name): default-constructed report: constructed (no name): assigned the value report Initialised: report: constructed $ g++-16 -std=c++23 -Wall -Wextra init_or_assign.cpp -o init_or_assign-gcc && ./init_or_assign-gcc Assigned: (no name): default-constructed report: constructed (no name): assigned the value report Initialised: report: constructed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 init_or_assign_session.py
Step through the two constructors and count what happens to each member:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- Before the body. Assigned's constructor names no member in a list, so before its body starts, title_ is default-constructed: the first line under Assigned, (no name): default-constructed.
- A second Tracer. The body runs title_ = Tracer{title};, which first builds a temporary Tracer from "report": the second line, report: constructed.
- Copied over the first. Then the copy assignment overwrites the default-constructed member: (no name): assigned the value report. Three operations, and the first one was wasted.
- Built once. Initialised's list, : title_{title}, builds its member once, with its value: the only line under Initialised, report: constructed.
Assigned default-constructs its member, builds a second Tracer from the title, and copies it over the first:
three operations, and the first one was wasted. Initialised builds its member once, with its value. For a
std::string member the waste is small, but some members cannot be assigned later at all:
// Members that cannot be assigned later: a const member, a reference member and a
// member whose type has no default constructor.
#include <string>
class Percent {
public:
Percent(int value) : value_{value} {}
private:
int value_;
};
class Exam {
public:
Exam(int id, std::string& subject, int pass_mark) {
id_ = id; // a const member cannot be assigned
subject_ = subject; // copies into whatever subject_ refers to: nothing yet
pass_mark_ = pass_mark;
}
private:
const int id_;
std::string& subject_; // a reference must refer to something from the start
Percent pass_mark_; // Percent has no default constructor
};
int main() {
std::string maths = "Maths";
Exam exam{1, maths, 40};
} from session import run
# Only the error lines, from each compiler: assignments in the body cannot
# initialise these members.
run("""
clang++ -std=c++23 -Wall -Wextra -c must_initialise.cpp 2>&1 | grep 'error:'
g++-16 -std=c++23 -Wall -Wextra -c must_initialise.cpp 2>&1 | grep 'error:'
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c must_initialise.cpp 2>&1 | grep 'error:'
must_initialise.cpp:15:5: error: constructor for 'Exam' must explicitly initialize the const member 'id_'
must_initialise.cpp:15:5: error: constructor for 'Exam' must explicitly initialize the reference member 'subject_'
must_initialise.cpp:15:5: error: constructor for 'Exam' must explicitly initialize the member 'pass_mark_' which does not have a default constructor
must_initialise.cpp:16:13: error: cannot assign to non-static data member 'id_' with const-qualified type 'const int'
$ g++-16 -std=c++23 -Wall -Wextra -c must_initialise.cpp 2>&1 | grep 'error:'
must_initialise.cpp:15:5: error: uninitialized const member in 'const int' [-fpermissive]
must_initialise.cpp:15:5: error: uninitialized reference member in 'std::string&' {aka 'class std::__cxx11::basic_string<char>&'} [-fpermissive]
must_initialise.cpp:15:55: error: no matching function for call to 'Percent::Percent()'
must_initialise.cpp:16:13: error: assignment of read-only member 'Exam::id_'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 must_initialise_session.py
A const member can be given a value only when it is initialised. A reference member must refer to an object from
the moment it exists, and assigning to it later would copy into whatever it refers to, not rebind it. A member whose
type has no default constructor, such as Percent, cannot even be default-initialised before the body runs. Clang
names all three cases in one sentence each; GCC calls the first two “uninitialized const member” and “uninitialized
reference member”, and reports the third as a missing Percent::Percent().
Giving members their values in the body
Exam(int id, std::string& subject, int pass_mark) { id_ = id; … } cannot compile with these three members, and in
any other class it builds each member twice. Initialise them in the list instead:
: id_{id}, subject_{subject}, pass_mark_{pass_mark}. Rule C.49 of the Core Guidelines makes the list the default
choice for every member.
Checkpoint
Check: Members are initialised before the body
Members are initialised in declaration order
The order of the names in a member initialiser list does not matter. Section [class.base.init] of the standard
fixes the sequence: base classes come first, the data members follow in declaration order, and the constructor body
runs last.
The order in which a constructor initialises an object
Text description of the diagram
The diagram is a flow from top to bottom that starts when a constructor is called, for example by Grid board{8, 8};.
- Every member is initialised in turn, in the order the members are declared in the class. A member that the member initialiser list names gets the list's value; otherwise it uses its default member initialiser, and a member with neither is default-initialised.
- Only after the last member has been initialised does the constructor body run.
- When the body finishes, the object exists and is valid.
The order of the names inside the member initialiser list plays no part: the declaration order alone decides which member is initialised first.
Pair writes its list in the opposite order to its members, and both compilers warn about it with -Wall:
// Members are initialised in the order they are declared in the class, whatever
// order the member initialiser list is written in.
#include <print>
#include "tracer.h"
class Pair {
public:
Pair() : second_{"second"}, first_{"first"} { // the list names second_ first
std::println(" constructor body runs");
}
private:
Tracer first_; // declared first, so initialised first
Tracer second_;
};
int main() {
Pair p;
} from session import run
# -Wall reports the mismatch; the program shows the order that really happens.
run("""
clang++ -std=c++23 -Wall -Wextra member_order.cpp -o member_order && ./member_order
g++-16 -std=c++23 -Wall -Wextra member_order.cpp -o member_order-gcc && ./member_order-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra member_order.cpp -o member_order && ./member_order
member_order.cpp:8:14: warning: field 'second_' will be initialized after field 'first_' [-Wreorder-ctor]
8 | Pair() : second_{"second"}, first_{"first"} { // the list names second_ first
| ^~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
| first_{"first"} second_{"second"}
1 warning generated.
first: constructed
second: constructed
constructor body runs
$ g++-16 -std=c++23 -Wall -Wextra member_order.cpp -o member_order-gcc && ./member_order-gcc
member_order.cpp: In constructor 'Pair::Pair()':
member_order.cpp:14:12: warning: 'Pair::second_' will be initialized after [-Wreorder]
14 | Tracer second_;
| ^~~~~~~
member_order.cpp:13:12: warning: 'Tracer Pair::first_' [-Wreorder]
13 | Tracer first_; // declared first, so initialised first
| ^~~~~~
member_order.cpp:8:5: warning: when initialized here [-Wreorder]
8 | Pair() : second_{"second"}, first_{"first"} { // the list names second_ first
| ^~~~
first: constructed
second: constructed
constructor body runs
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 member_order_session.py
Clang’s warning even prints the list rewritten in the real order. The output confirms it: first is constructed
before second, and both before the body. On its own the mismatch is harmless here. It turns into a bug as soon as
one member’s initialiser reads another member:
// cells_ is declared first, so it is initialised first: before width_ and height_
// have values. Reading them there is undefined behaviour.
#include <cstddef>
#include <vector>
class Grid {
public:
Grid(std::size_t width, std::size_t height)
: width_{width}, height_{height}, cells_(width_ * height_) {}
std::size_t size() const { return cells_.size(); }
private:
std::vector<int> cells_;
std::size_t width_;
std::size_t height_;
};
int main() {
Grid board{8, 8};
return board.size() == 64 ? 0 : 1;
} from session import run
# Compiled only: running it would read two members that have no value yet.
# The last two builds leave out -Wall and -Wextra.
run("""
clang++ -std=c++23 -Wall -Wextra -c grid_bug.cpp
g++-16 -std=c++23 -Wall -Wextra -c grid_bug.cpp
clang++ -std=c++23 -c grid_bug.cpp
g++-16 -std=c++23 -c grid_bug.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c grid_bug.cpp
grid_bug.cpp:9:26: warning: field 'height_' will be initialized after field 'cells_' [-Wreorder-ctor]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ~~~~~~~~~~~~~ ^~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
| cells_(width_ * height_) width_{width} height_{height}
grid_bug.cpp:9:50: warning: field 'width_' is uninitialized when used here [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^
grid_bug.cpp:9:59: warning: field 'height_' is uninitialized when used here [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^
3 warnings generated.
$ g++-16 -std=c++23 -Wall -Wextra -c grid_bug.cpp
grid_bug.cpp: In constructor 'Grid::Grid(std::size_t, std::size_t)':
grid_bug.cpp:16:17: warning: 'Grid::height_' will be initialized after [-Wreorder]
16 | std::size_t height_;
| ^~~~~~~
grid_bug.cpp:14:22: warning: 'std::vector<int> Grid::cells_' [-Wreorder]
14 | std::vector<int> cells_;
| ^~~~~~
grid_bug.cpp:8:5: warning: when initialized here [-Wreorder]
8 | Grid(std::size_t width, std::size_t height)
| ^~~~
grid_bug.cpp:9:50: warning: member 'Grid::width_' is used uninitialized [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^~~~~~
grid_bug.cpp:9:59: warning: member 'Grid::height_' is used uninitialized [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^~~~~~~
grid_bug.cpp: In constructor 'Grid::Grid(std::size_t, std::size_t)':
grid_bug.cpp:9:50: warning: '*this.Grid::width_' is used uninitialized [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^~~~~~
grid_bug.cpp:9:59: warning: '*this.Grid::height_' is used uninitialized [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^~~~~~~
$ clang++ -std=c++23 -c grid_bug.cpp
grid_bug.cpp:9:50: warning: field 'width_' is uninitialized when used here [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^
grid_bug.cpp:9:59: warning: field 'height_' is uninitialized when used here [-Wuninitialized]
9 | : width_{width}, height_{height}, cells_(width_ * height_) {}
| ^
2 warnings generated.
$ g++-16 -std=c++23 -c grid_bug.cpp
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grid_bug_session.py
Step through the constructor of Grid board{8, 8} in the order that really happens:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Declaration order. Grid board{8, 8}; calls the constructor. Its list names width_, height_ and then cells_, but members are initialised in the order the class declares them: cells_ first.
- The bug, on this step. cells_(width_ * height_) runs now, while width_ and height_ hold no value yet. Reading them is undefined behaviour: the size could come out 0, or so large that the allocation fails.
- Too late. Only then does width_{width} give width_ the value 8, and height_{height} give height_ the value 8, after cells_ has used them.
- The body. The body runs last. Clang warned about this file with -Wreorder-ctor and -Wuninitialized, GCC 16 with -Wreorder and -Wuninitialized under -Wall.
- The repair. cells_(width * height) reads the parameters, which exist before any member, so the order stops mattering. Declaring the members in the order they depend on each other is the other habit.
cells_ is declared first, so cells_(width_ * height_) runs before width_ and height_ have been given their
values. Reading them there is undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”: the vector may get a size of
0, or a size so large that the allocation fails, and the result can change between builds. That is why this example
is compiled but not run. Both compilers notice with -Wall -Wextra: Clang reports the order with -Wreorder-ctor and
the reads with -Wuninitialized, GCC 16 the order with -Wreorder and the reads twice. Without warning options Clang
still reports the two reads, while GCC prints nothing at all for this file, as the last command of the session shows.
The repair takes one of two forms, and rule C.47 of the Core Guidelines asks for both habits. Initialise members from
the constructor’s parameters rather than from other members, cells_(width * height), so the order stops mattering;
and declare members in the order they depend on each other, then write the list in that same order.
Key idea
Members are initialised in the order the class declares them, never in the order of the list, so a member’s initialiser may read only the parameters and the members declared before it.
Checkpoint
Check: Initialisation follows declaration order
explicit stops silent conversions
A constructor that takes one argument tells the compiler how to turn that argument into an object, and the compiler is allowed to do it on its own whenever a function needs the object. The standard calls such a constructor a converting constructor: A constructor that is not explicit, so the compiler may call it on its own to turn its argument into an object, as when pay_fee(500) silently makes a Money from an int.:
// Without explicit, a constructor with one parameter is also a conversion: the
// compiler may call it on its own to turn an int into a Money.
#include <print>
class Money {
public:
Money(long long paise) : paise_{paise} {}
long long paise() const { return paise_; }
private:
long long paise_ = 0;
};
void pay_fee(Money fee) {
std::println("paying {} paise", fee.paise());
}
int main() {
pay_fee(Money{50'000}); // clear: 50,000 paise
Money price{1'999}; // clear too: the code names the type
pay_fee(500); // also compiles: 500 paise, or did the caller mean 500 rupees?
Money tip = 20; // and this: an int silently becomes Money
Money gift = {2'000}; // and braces after = as well
std::println("price {}, tip {}, gift {} (paise)", price.paise(), tip.paise(), gift.paise());
} from session import run
# money.cpp built and run with Apple clang, then with GCC 16: no warning at all.
run("""
clang++ -std=c++23 -Wall -Wextra money.cpp -o money && ./money
g++-16 -std=c++23 -Wall -Wextra money.cpp -o money-gcc && ./money-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra money.cpp -o money && ./money paying 50000 paise paying 500 paise price 1999, tip 20, gift 2000 (paise) $ g++-16 -std=c++23 -Wall -Wextra money.cpp -o money-gcc && ./money-gcc paying 50000 paise paying 500 paise price 1999, tip 20, gift 2000 (paise)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_session.py
pay_fee(500) compiles without a warning on either compiler and pays 500 paise. If the caller meant 500 rupees, the
fee is off by a factor of 100, and nothing in the call shows that a Money was made at all. Marking the constructor
explicit keeps it available, but only where the code asks for a Money by name:
// The same class with an explicit constructor: a Money is made only when the code
// says so.
class Money {
public:
explicit Money(long long paise) : paise_{paise} {}
long long paise() const { return paise_; }
private:
long long paise_ = 0;
};
void pay_fee(Money fee) {
(void)fee;
}
int main() {
pay_fee(Money{50'000}); // still fine: the conversion is written out
Money price{1'999}; // fine: direct-initialisation may use an explicit constructor
pay_fee(500); // rejected
Money tip = 20; // rejected
Money gift = {2'000}; // rejected: copy-list-initialisation cannot choose an explicit constructor
(void)price;
(void)tip;
(void)gift;
} from session import run
# Only the error lines, from each compiler.
run("""
clang++ -std=c++23 -Wall -Wextra -c money_explicit.cpp 2>&1 | grep 'error:'
g++-16 -std=c++23 -Wall -Wextra -c money_explicit.cpp 2>&1 | grep 'error:'
""") Output
$ clang++ -std=c++23 -Wall -Wextra -c money_explicit.cpp 2>&1 | grep 'error:' money_explicit.cpp:19:5: error: no matching function for call to 'pay_fee' money_explicit.cpp:20:11: error: no viable conversion from 'int' to 'Money' money_explicit.cpp:21:11: error: chosen constructor is explicit in copy-initialization $ g++-16 -std=c++23 -Wall -Wextra -c money_explicit.cpp 2>&1 | grep 'error:' money_explicit.cpp:19:13: error: could not convert '500' from 'int' to 'Money' money_explicit.cpp:20:17: error: conversion from 'int' to non-scalar type 'Money' requested money_explicit.cpp:21:24: error: converting to 'Money' from initializer list would use explicit constructor 'Money::Money(long long int)'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 money_explicit_session.py
The two files hold the same five lines of main. Read each line across the two runs:
| Criterion | Without explicit | With explicit |
|---|---|---|
| pay_fee(Money{50'000}) | Compiles | Compiles |
| Money price{1'999} | Compiles | Compiles |
| pay_fee(500) | Compiles: 500 paise | Error |
| Money tip = 20 | Compiles: 20 paise | Error |
| Money gift = {2'000} | Compiles: 2,000 paise | Error |
The calls that name the type, Money{50'000} and Money price{1'999}, still compile. The three that relied on a
silent conversion are rejected, including Money gift = {2'000};: braces after an equals sign are
copy-list-initialisation: Initialisation from a brace list written after an equals sign, as in Money gift = {2'000};, or given as a function argument or in a return statement. It may not use an explicit constructor., and the standard makes that ill-formed whenever
the constructor it picks is explicit.
Rule C.46 of the Core Guidelines makes explicit the default for constructors with one parameter. Leave it off only
when the conversion loses nothing and surprises nobody, the way the standard library lets std::string s = "text";
convert a string literal.
Checkpoint
Check: explicit and silent conversions
One constructor can hand its work to another
When several constructors share their checks, one can hand its work to another. That is a delegating constructor: A constructor whose member initialiser list names another constructor of the same class, as in Colour(int grey) : Colour{grey, grey, grey} {}. The target runs completely first, then the delegating constructor's own body.:
// A delegating constructor hands the work to another constructor of the same class,
// and default member initialisers give every member a value when no constructor does.
#include <print>
#include <stdexcept>
class Colour {
public:
Colour() = default; // black, from the default member initialisers
Colour(int red, int green, int blue) : red_{red}, green_{green}, blue_{blue} {
if (!fits(red) || !fits(green) || !fits(blue)) {
throw std::invalid_argument{"each channel must be between 0 and 255"};
}
}
explicit Colour(int grey) : Colour{grey, grey, grey} {} // delegates, checks included
void print(const char* name) const {
std::println("{:<6} #{:02x}{:02x}{:02x}", name, red_, green_, blue_);
}
private:
static bool fits(int channel) { return channel >= 0 && channel <= 255; }
int red_ = 0;
int green_ = 0;
int blue_ = 0;
};
int main() {
Colour black;
Colour orange{255, 153, 51};
Colour grey{128};
black.print("black");
orange.print("orange");
grey.print("grey");
try {
Colour too_bright{300};
} catch (const std::invalid_argument& error) {
std::println("refused: {}", error.what());
}
} from session import run
# colour.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra colour.cpp -o colour && ./colour
g++-16 -std=c++23 -Wall -Wextra colour.cpp -o colour-gcc && ./colour-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra colour.cpp -o colour && ./colour black #000000 orange #ff9933 grey #808080 refused: each channel must be between 0 and 255 $ g++-16 -std=c++23 -Wall -Wextra colour.cpp -o colour-gcc && ./colour-gcc black #000000 orange #ff9933 grey #808080 refused: each channel must be between 0 and 255
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 colour_session.py
Colour(int grey) : Colour{grey, grey, grey} {} names another constructor of the same class instead of members.
Step through what happens for a grey of 128, and then for 300:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The call. Colour grey{128}; calls Colour(int). Its member initialiser list holds only the delegation, Colour{grey, grey, grey}.
- The target runs first. The target is the three-channel constructor, called with 128, 128 and 128. It runs completely before anything else of Colour(int).
- One set of checks. Its list sets red_, green_ and blue_ to 128, and its body checks each channel. 128 is between 0 and 255, so nothing is thrown.
- Then its own body. Only then does the delegating constructor run its own body, empty here. The program printed grey #808080.
- The same path for 300. Colour too_bright{300}; delegates the same way, and the target body throws: refused: each channel must be between 0 and 255, the message of a three-channel colour, because the rule is written once.
The target constructor runs completely, its checks included, and then the delegating constructor’s own body, empty
here, runs after it. That is why Colour too_bright{300} is refused with the same message as a three-channel colour:
the rule is written once. A delegating constructor’s list may hold only the delegation, and rule C.51 of the Core
Guidelines recommends the technique for exactly this reason.
The other tool for sharing defaults is the default member initialiser. int red_ = 0; gives every constructor that
does not mention red_ the same starting value, so Colour() = default; is all a black colour needs, with no
hand-written default constructor that would repeat the zeros.
Checkpoint
Check: Delegating constructors and member defaults
Practise: a Fraction in lowest terms
Fraction keeps a fraction in lowest terms with a positive denominator, so 6/8 is stored as 3/4 and 1/-2 as
-1/2. Normalising once, in the constructor, means every other member function can trust the invariant. The starter
code only copies its arguments:
C header · fraction.h
// Starter code for "Your turn": finish the constructors of Fraction.
#pragma once
#include <format>
#include <numeric> // std::gcd
#include <stdexcept> // std::invalid_argument
#include <string>
// A fraction in lowest terms with a positive denominator: 6/8 is kept as 3/4 and
// 1/-2 as -1/2. The invariant: denominator() > 0, and the two numbers have no common
// factor other than 1.
class Fraction {
public:
Fraction() = default; // 0/1
// TODO: throw std::invalid_argument when denominator is 0, make the denominator
// positive, and divide both numbers by their greatest common divisor (std::gcd).
Fraction(int numerator, int denominator) : num_{numerator}, den_{denominator} {}
// TODO: a whole number n is n/1. Make this constructor explicit, and let it
// delegate to the constructor above.
Fraction(int whole) : num_{whole} {}
int numerator() const { return num_; }
int denominator() const { return den_; }
std::string text() const { return std::format("{}/{}", num_, den_); }
private:
int num_ = 0;
int den_ = 1;
}; C++ · fraction_tests.cpp
// Sample tests for fraction.h. Build: clang++ -std=c++23 -Wall -Wextra fraction_tests.cpp -o fraction_tests
#include <print>
#include <stdexcept>
#include <type_traits>
#include "fraction.h"
int passed = 0;
int failed = 0;
void expect(bool ok, const char* what) {
if (ok) {
++passed;
} else {
++failed;
std::println("FAIL {}", what);
}
}
bool zero_denominator_throws() {
try {
Fraction broken{1, 0};
(void)broken;
} catch (const std::invalid_argument&) {
return true;
}
return false;
}
int main() {
expect(Fraction{}.text() == "0/1", "Fraction{} is 0/1");
expect(Fraction{6, 8}.text() == "3/4", "6/8 is kept as 3/4");
expect(Fraction{1, -2}.text() == "-1/2", "1/-2 is kept as -1/2");
expect(Fraction{-3, -9}.text() == "1/3", "-3/-9 is kept as 1/3");
expect(Fraction{-4, 6}.text() == "-2/3", "-4/6 is kept as -2/3");
expect(Fraction{0, 5}.text() == "0/1", "0/5 is kept as 0/1");
expect(Fraction{7}.text() == "7/1", "the whole number 7 is 7/1");
expect(zero_denominator_throws(), "a zero denominator throws std::invalid_argument");
expect(!std::is_convertible_v<int, Fraction>, "an int does not turn into a Fraction by itself");
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 TODOs are done.
run("""
clang++ -std=c++23 -Wall -Wextra fraction_tests.cpp -o fraction_tests && ./fraction_tests
g++-16 -std=c++23 -Wall -Wextra fraction_tests.cpp -o fraction_tests-gcc && ./fraction_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra fraction_tests.cpp -o fraction_tests && ./fraction_tests FAIL 6/8 is kept as 3/4 FAIL 1/-2 is kept as -1/2 FAIL -3/-9 is kept as 1/3 FAIL -4/6 is kept as -2/3 FAIL 0/5 is kept as 0/1 FAIL a zero denominator throws std::invalid_argument FAIL an int does not turn into a Fraction by itself 2 of 9 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra fraction_tests.cpp -o fraction_tests-gcc && ./fraction_tests-gcc FAIL 6/8 is kept as 3/4 FAIL 1/-2 is kept as -1/2 FAIL -3/-9 is kept as 1/3 FAIL -4/6 is kept as -2/3 FAIL 0/5 is kept as 0/1 FAIL a zero denominator throws std::invalid_argument FAIL an int does not turn into a Fraction by itself 2 of 9 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 fraction_session.py
Change the two constructors until both compilers print 9 of 9 checks passed:
- In
Fraction(int numerator, int denominator), throwstd::invalid_argumentwhen the denominator is 0. - When the denominator is negative, negate both numbers.
- Divide both by
std::gcd(numerator, denominator)from<numeric>, which is never negative.std::gcd(0, 5)is 5, so0/5becomes0/1. - Make
Fraction(int whole)explicit and let it delegate:: Fraction{whole, 1}. The last test checks that anintno longer turns into aFractionby itself.
Before you call it done, decide what Fraction{INT_MIN, -1} should do. Negating INT_MIN does not fit in an int,
which makes it undefined behaviour, so a careful constructor either computes in long long or refuses that input.
Then see which of the five rules held up, from the refused Percent to the delegating Colour. Each round asks first
about the rule your earlier answers missed most, and a wrong choice tells you which slip it was:
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 · Why should most constructors that take one argument be explicit?
A constructor that can be called with one argument is also an implicit conversion: wherever the class is expected,
the compiler may build an object from that argument without the code saying so. That makes pay_fee(500) compile
when pay_fee takes a Money, and the reader cannot see that a Money was made, or in which unit. Marking the
constructor explicit keeps direct uses such as Money{500} and Money m{500}, and rejects the silent ones:
passing an int where Money is expected, Money m = 500; and Money m = {500};. A good answer gives the
exception too: leave explicit off when the conversion is lossless and expected, such as a string from a string
literal, and mentions that this is rule C.46 of the C++ Core Guidelines.
An interviewer often probes the edges next: when leaving explicit off is right, and whether it means anything for a
constructor with two parameters. Decide both:
Checkpoint
Interview round: two follow-up questions
Two harder constructor cases follow: a base class that copies a member of the derived class before that member
exists, and a logging call that silently picks the bool overload:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid
Sensor derives from Labelled and initialises it with one of its own members: Sensor(const std::string& id) : id_{"sensor-" + id}, Labelled{id_} {}. Every label comes out empty, and AddressSanitizer reports nothing. Explain why, and give two fixes.
-
Mid
A logging API has log(bool flag) and log(const Message& m), and Message has a non-explicit constructor from std::string. The call log("disk full") prints "flag: true". Explain why the compiler chose that overload without a warning, and redesign the API so the mistake cannot happen.
Key takeaways
- A constructor makes a new object valid; when it cannot, it throws, and no object exists.
- Declaring any constructor removes the implicit default constructor, and
T() = default;brings it back. - Every member is initialised before the body runs, so an assignment in the body is a second step, and impossible for
constmembers, references and members without a default constructor. - Members are initialised in declaration order, never in list order, and building with
-Wall -Wextrareports a member read too early, which plain GCC builds do not. - Constructors with one parameter are
explicitunless the conversion is lossless and expected. - Delegating constructors and default member initialisers keep each check and each default in one place.
Terms in this lesson
- constructor
- A member function with the class's name and no return type, run when an object is created. It gives the members their first values and makes the object valid, or throws when it cannot, and then no object exists.
- converting constructor
- A constructor that is not explicit, so the compiler may call it on its own to turn its argument into an object, as when pay_fee(500) silently makes a Money from an int.
- copy-list-initialisation
- Initialisation from a brace list written after an equals sign, as in Money gift = {2'000};, or given as a function argument or in a return statement. It may not use an explicit constructor.
- default constructor
- A constructor that can be called with no arguments, as in Percent none;. The compiler writes one only for a class that declares no constructor; T() = default; asks for it back.
- delegating constructor
- A constructor whose member initialiser list names another constructor of the same class, as in Colour(int grey) : Colour{grey, grey, grey} {}. The target runs completely first, then the delegating constructor's own body.
- member initialiser list
- The part of a constructor after the colon, as in Percent(int value) : value_{value} {}. It initialises the named members before the body starts, in the order the members are declared, not the order of the list.
References
- Working Draft, Programming Languages: C++ (N4950): [class.base.init], [class.conv.ctor], [class.default.ctor], [over.match.list] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: C.40, define a constructor if a class has an invariant (Standard C++ Foundation)
- C++ Core Guidelines: C.42, if a constructor cannot construct a valid object, throw an exception (Standard C++ Foundation)
- C++ Core Guidelines: C.46, by default declare single-argument constructors explicit (Standard C++ Foundation)
- C++ Core Guidelines: C.47, define and initialize data members in the order of member declaration (Standard C++ Foundation)
- C++ Core Guidelines: C.49, prefer initialization to assignment in constructors (Standard C++ Foundation)
- C++ Core Guidelines: C.51, use delegating constructors to represent common actions (Standard C++ Foundation)
- Options Controlling C++ Dialect (-Wreorder) (GNU Project)
- Diagnostic flags in Clang (-Wreorder-ctor, -Wuninitialized) (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress