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

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.

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

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:

percent.cpp C++ · percent.cpp
// 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());
    }
}
Built and run with both compilers Python · percent_session.py
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:

Interactive · stage by stage One Percent made, one refused Step through the two constructor calls of percent.cpp. The stages run top to bottom, in the order they happen; each one's line shows the value it handles.
One Percent made, one refused, step 7 of 7. Showing A Percent is created (Percent wrong{140};), Member initialiser list (value_ is 140), Constructor body (140 > 100: throws), What main gets (no object called wrong). In focus: What main gets.A Percent is createdPercent wrong{140};Member initialiser listvalue_ is 140Constructor body140 > 100: throwsWhat main getsno object called wrongthe exception reaches main

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

Every step as text
  1. The call. Percent attendance{85}; calls Percent(int). Until the constructor finishes, there is no attendance object that other code could use.
  2. The list first. The member initialiser list runs before the body: value_{value} gives value_ the argument, 85.
  3. The body checks. The body tests the rule. 85 is between 0 and 100, so the body ends without throwing.
  4. A valid object. The constructor returns, and attendance exists, valid from its first moment: the program printed none = 0, attendance = 85.
  5. 140 this time. Percent wrong{140}; runs the same constructor. The list gives value_ the argument 140 before the body sees it.
  6. The body throws. The test fails, so the body throws std::invalid_argument, which ends the constructor at once.
  7. 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:

no_default.cpp C++ · no_default.cpp
// 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;
}
No Seat() once another constructor exists Python · no_default_session.py
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

  1. Question 1 of 2 Percent wrong{140}; throws from the constructor body. Which is true right after the throw?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No object called wrong exists, and the line after it in the try block never runs

    When a constructor cannot establish the invariant it throws, and no object exists. percent.cpp printed refused: a percentage must be between 0 and 100, never the line that would have used wrong.

  2. Question 2 of 2 A class declares only Seat(char row, int number);, and Seat any; no longer compiles. Which declaration brings it back, with the default member initialisers as the values?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Seat() = default;

    Declaring any constructor removes the implicit default constructor. Seat() = default; brings it back, and it uses the default member initialisers 'A' and 1.

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:

Assigning in the body, or initialising in the list

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)";
};
Three steps against one Python · init_or_assign_session.py
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:

Interactive · step by step Three steps in the body, one in the list Step through the two constructors of init_or_assign.cpp. The left column follows Assigned's member, the right one Initialised's.
Three steps in the body, one in the list, step 4 of 4. Showing Assigned's title_ (report, in 3 steps), Tracer{title} (report: constructed), Initialised's title_ (report, in 1 step). In focus: Initialised's title_.copied over itAssigned's title_report, in 3 stepsTracer{title}report: constructedInitialised's title_report, in 1 step

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

Every step as text
  1. 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.
  2. A second Tracer. The body runs title_ = Tracer{title};, which first builds a temporary Tracer from "report": the second line, report: constructed.
  3. 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.
  4. 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:

must_initialise.cpp C++ · must_initialise.cpp
// 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};
}
What the compilers say about the body's assignments Python · must_initialise_session.py
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

  1. Question 1 of 2 How many lines did Tracer print for the member of Assigned, the class that assigns in its constructor body?

    Type a number.

    A hint

    Count the lines under Assigned in the output.

    Show the answer: Question 1 of 2

    Answer: 3 lines

    Three: the member was default-constructed before the body, the body built a temporary Tracer from "report", and the copy assignment wrote it over the member. Initialised printed one line, because its list built the member once.

  2. Question 2 of 2 Exam has a member const int id_;, and its constructor writes id_ = id; in the body. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It does not compile, because a const member can only be given its value when it is initialised

    A const member, a reference member and a member without a default constructor must be initialised in the member initialiser list or by a default member initialiser: : id_{id} is the fix.

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.

Members are initialised one by one in declaration order, from the list or their defaults; then the constructor body runs.Grid board{8, 8};the constructor is called1. Every member, in declaration order:the list's value if the list names it,else its default member initialiser,else default-initialised2. The constructor body runs3. The object exists and is validafter the last member

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};.

  1. 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.
  2. Only after the last member has been initialised does the constructor body run.
  3. 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:

member_order.cpp C++ · member_order.cpp
// 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;
}
Declaration order wins Python · member_order_session.py
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:

grid_bug.cpp C++ · grid_bug.cpp
// 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;
}
Compiled only, with and without warning options Python · grid_bug_session.py
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:

Interactive · stage by stage Grid board{8, 8}, member by member Step through the constructor in the order the members are declared, which is the order they are initialised, whatever the list says.
Grid board{8, 8}, member by member, step 5 of 5. Showing 1. cells_ (cells_(width * height)), 2. width_ (width_ = 8), 3. height_ (height_ = 8), 4. The body. In focus: 1. cells_.1. cells_cells_(width * height)2. width_width_ = 83. height_height_ = 84. The bodyempty here

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

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. The body. The body runs last. Clang warned about this file with -Wreorder-ctor and -Wuninitialized, GCC 16 with -Wreorder and -Wuninitialized under -Wall.
  5. 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

  1. Question 1 of 2 Grid declares cells_, then width_, then height_, and its list is written : width_{width}, height_{height}, cells_(width_ * height_). Which member is initialised first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: cells_, because it is declared first

    The declaration order alone decides. cells_(width_ * height_) runs before width_ and height_ have values, which is undefined behaviour.

  2. Question 2 of 2 Which change removes the undefined behaviour from Grid without moving any declaration?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Initialise cells_ from the parameters: cells_(width * height)

    Rule C.47 of the C++ Core Guidelines asks for both habits: initialise from parameters where you can, and declare members in the order they depend on each other.

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

money.cpp C++ · money.cpp
// 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());
}
An int quietly becomes Money Python · money_session.py
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:

money_explicit.cpp C++ · money_explicit.cpp
// 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;
}
The three silent conversions are rejected Python · money_explicit_session.py
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:

The five lines of main without and with explicit
Criterion Without explicitWith explicit
pay_fee(Money{50'000}) CompilesCompiles
Money price{1'999} CompilesCompiles
pay_fee(500) Compiles: 500 paiseError
Money tip = 20 Compiles: 20 paiseError
Money gift = {2'000} Compiles: 2,000 paiseError

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

  1. Question 1 of 2 Money has a constructor Money(long long paise) without explicit, and pay_fee takes a Money. What does pay_fee(500); do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It compiles without a warning and pays 500 paise

    A constructor that can be called with one argument is a converting constructor. The call shows no sign that a Money was made, or in which unit.

  2. Question 2 of 2 Once Money(long long paise) is explicit, why is Money gift = {2'000}; rejected, although it uses braces?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Braces after = are copy-list-initialisation, which is ill-formed when the constructor it picks is explicit

    explicit keeps the constructor for direct initialisation, Money{50'000} and Money price{1'999}, and rejects every silent one: pay_fee(500), Money tip = 20; and Money gift = {2'000};.

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

colour.cpp C++ · colour.cpp
// 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());
    }
}
One check, three ways to make a colour Python · colour_session.py
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:

Interactive · stage by stage Colour grey{128} hands its work to another constructor Step through the delegation in the order it happens, top to bottom, then the same path for 300.
Colour grey{128} hands its work to another constructor, step 5 of 5. Showing Colour(int grey) (Colour too_bright{300};), The target constructor (Colour{300, 300, 300}), Its list and body (300 > 255: throws), The delegating body. In focus: Its list and body.Colour(int grey)Colour too_bright{300};The target constructorColour{300, 300, 300}Its list and body300 > 255: throwsThe delegating bodyempty here: {}the delegating body never runs

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

Every step as text
  1. The call. Colour grey{128}; calls Colour(int). Its member initialiser list holds only the delegation, Colour{grey, grey, grey}.
  2. 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).
  3. 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.
  4. Then its own body. Only then does the delegating constructor run its own body, empty here. The program printed grey #808080.
  5. 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

  1. Question 1 of 2 Colour too_bright{300}; uses Colour(int grey) : Colour{grey, grey, grey} {}. Where is the check that refuses 300?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In the body of the three-channel constructor, which runs completely before the delegating body

    A delegating constructor first runs its target completely, checks included. That is why the rule is written once and every way of making a colour obeys it.

  2. Question 2 of 2 What happens with this constructor?

    Read the code, then choose one answer.

    explicit Colour(int grey) : Colour{grey, grey, grey}, red_{grey} {}
    Show the answer: Question 2 of 2

    Answer: It does not compile, because a delegating constructor's list may hold only the delegation

    The target constructor initialises every member, so a delegating constructor's member initialiser list contains only the delegation. Its body may still do more work after the target has finished.

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:

The starter code and its sample tests

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;
}
The sample tests against the starter code Python · fraction_session.py
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:

  1. In Fraction(int numerator, int denominator), throw std::invalid_argument when the denominator is 0.
  2. When the denominator is negative, negate both numbers.
  3. Divide both by std::gcd(numerator, denominator) from <numeric>, which is never negative. std::gcd(0, 5) is 5, so 0/5 becomes 0/1.
  4. Make Fraction(int whole) explicit and let it delegate: : Fraction{whole, 1}. The last test checks that an int no longer turns into a Fraction by itself.
Fraction Calculator Simplify fractions such as 6/8 or -4/6 to check what your constructor should store.

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.

  1. Question 1 of 10 A class declares Seat(char row, int number); and no other constructor. What does Seat any; do?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: It does not compile, because declaring a constructor removes the implicit default constructor

    The compiler writes a default constructor only for a class with no user-declared constructor. Declare Seat() = default; to get it back, with the default member initialisers as the values.

  2. Question 2 of 10 A struct that used to be filled member by member from braces gets a user-declared constructor. What do braces do now?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: They always call a constructor, because the class is no longer an aggregate

    With a user-declared constructor the class stops being an aggregate, so every way of creating an object goes through a constructor, and the constructor can check the invariant.

  3. Question 3 of 10 When the body of a constructor starts, what state is each member in?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Already initialised, from the list, else its default member initialiser, else by default-initialisation

    The member initialiser list, then the default member initialisers, then default-initialisation: in that order of preference, every member is ready before the body. That is why an assignment in the body comes too late.

  4. Question 4 of 10 Which members must be initialised in the member initialiser list (or by a default member initialiser), because an assignment in the constructor body cannot do it?

    Choose every answer that is right.

    Show the answer: Question 4 of 10

    Answer:

    • A reference member
    • A const int member
    • A member whose class has no default constructor

    Before the body runs, every member has been initialised. A const member and a reference can be given their value only then, and a member without a default constructor cannot be default-initialised at all. A std::string can be assigned in the body, at the cost of building it twice.

  5. Question 5 of 10 In which order are the members of Box initialised?

    Read the code, then choose one answer.

    class Box {
    public:
        Box() : c_{3}, a_{1}, b_{2} {}
    private:
        int a_;
        int b_;
        int c_;
    };
    Show the answer: Question 5 of 10

    Answer: a_, then b_, then c_

    Members are initialised in the order they are declared in the class. The order of the member initialiser list changes nothing, which is why -Wall warns when the two differ.

  6. Question 6 of 10 Which build of grid_bug.cpp printed no warning at all?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: GCC 16 without -Wall and -Wextra

    Built without warning options, GCC said nothing about a read of two unset members, while Clang still reported the reads. Building with -Wall -Wextra is the habit that catches it on both.

  7. Question 7 of 10 Why is a non-explicit Money(long long paise) risky at a call such as pay_fee(500)?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: The call compiles silently, and nothing in it shows that a Money was made or in which unit

    A converting constructor lets the compiler make objects where the code never says so. explicit keeps the conversion only where the type is named.

  8. Question 8 of 10 Which line still compiles once the constructor is explicit?

    Read the code, then choose one answer.

    class Money {
    public:
        explicit Money(long long paise);
    };
    void pay_fee(Money fee);
    
    pay_fee(500);            // (1)
    Money a = 500;           // (2)
    Money b{500};            // (3)
    Money c = {500};         // (4)
    Show the answer: Question 8 of 10

    Answer: Only (3)

    An explicit constructor is used only where the code asks for a Money by name. (3) is direct-initialisation, so it compiles. (1) and (2) need an implicit conversion, and (4) is copy-list-initialisation, which the standard rejects when the constructor it picks is explicit.

  9. Question 9 of 10 In Colour(int grey) : Colour{grey, grey, grey} {}, when does the body of the one-argument constructor run?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: After the three-argument constructor has finished, its checks included

    A delegating constructor first runs its target constructor completely. Only when the target returns does the delegating constructor's own body run, so the target's checks have already passed.

  10. Question 10 of 10 Colour() = default; and the members are declared int red_ = 0; int green_ = 0; int blue_ = 0;. What does Colour black; print with the lesson's print()?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: black #000000

    Default member initialisers give every constructor that does not set a member the same starting value, so the defaulted constructor needs no code at all. The program printed black #000000.

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

  1. Question 1 of 2 Follow-up: when is it right to leave explicit off a constructor that takes one argument?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: When the conversion loses nothing and surprises nobody, as std::string from a string literal

    explicit is the default (rule C.46). A conversion earns its implicit form only when a reader would never be surprised by it.

  2. Question 2 of 2 Follow-up: does explicit change anything for a constructor with two parameters?

    Read the code, then choose one answer.

    struct Range {
        explicit Range(int low, int high);
    };
    Range make() { return {1, 5}; }   // (1)
    Range r = {1, 5};                   // (2)
    Range ok{1, 5};                     // (3)
    Show the answer: Question 2 of 2

    Answer: Yes. (1) and (2) are copy-list-initialisation and do not compile; (3) does

    explicit rejects every implicit use of a constructor, and since C++11 that includes brace lists with several values after = and in return statements.

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.

  1. 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.

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

  2. 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.

    Model answer, Pro. First step and follow-ups

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 const members, references and members without a default constructor.
  • Members are initialised in declaration order, never in list order, and building with -Wall -Wextra reports a member read too early, which plain GCC builds do not.
  • Constructors with one parameter are explicit unless 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

Related tools

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.