AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

08 Classes and objects / Lesson 2 of 6

Constructors — building a valid object

15 min · +10 XP · 2 exercises · 7 questions

Read the text15 min
Exercises0/2
Quiz7 q

Quick answer

What is a member initialiser list in C++?

A member initialiser list is the part of a C++ constructor between the colon and the body, as in Account(std::string o) : owner_(std::move(o)), balance_(0) {}. It constructs each member directly with its value. Assigning in the body instead default-constructs the member first and then assigns, and is impossible for const members, references and types without a default constructor. Members are initialised in declaration order, whatever order the list uses.

A constructor turns raw storage into an object that keeps its promises. It runs exactly once per object, before any other member function can, and it is the only code that ever sees the members before they hold meaningful values. This lesson covers the constructors you write and the ones the compiler writes for you, the member-initialiser list and why it beats assignment in the body, the rule that members are initialised in declaration order whatever the list says, delegating constructors, explicit, and = default / = delete.

Declaring constructors

#include <iostream>
#include <string>

class Account {
    std::string owner_;
    long long balance_ = 0;         // default member initialiser
public:
    Account() = default;                           // (1) default constructor
    Account(std::string owner, long long opening)  // (2) parameterised
        : owner_(std::move(owner)), balance_(opening) {}
    long long balance() const { return balance_; }
};

int main() {
    Account a;                       // (1)
    Account b{"ada", 500};           // (2)
    Account c("bob", 20);            // (2), parentheses also work
    std::cout << b.balance() << '\n';
    return 0;
}

A constructor has the class's name, no return type — not even void — and any number of parameters; constructors overload like functions (Module 4, lesson 3). A constructor with no parameters (or with defaults for all of them) is the default constructor, used by Account a;, by std::vector<Account> v(10), and by every array of Account. Braces and parentheses both call a constructor; braces reject narrowing and cannot be mistaken for a function declaration, which is why this track prefers them for objects.

What the compiler writes for you

If a class declares no constructor at all, the compiler generates a public default constructor that default-initialises each member: class-type members run their own default constructors, members with a default member initialiser take it, and fundamental members with neither are left indeterminate. Declare any constructor and the implicit default constructor is not generated — Account a; becomes a compile error the moment you add Account(std::string, long long). That is exactly the situation the = default line above handles: it asks for the compiler-generated version explicitly. The mirror image is = delete, which declares a constructor in order to forbid it:

class Session {
public:
    Session(int id) : id_(id) {}
    Session() = delete;         // "no Session without an id" — the error names the deleted function
private:
    int id_;
};

Copy and move constructors are generated by the same mechanism and are the subject of Module 9; for now, know that the rules for those are separate from the rule for the default constructor.

The member-initialiser list

The part between the : and the body is the member-initialiser list. It is not decoration — it is where members are constructed, and it is the difference between initialising and assigning:

class Widget {
    std::string name_;
    const int id_;
    std::vector<int>& log_;
public:
    Widget(std::string name, int id, std::vector<int>& log)
        : name_(std::move(name)), id_(id), log_(log) {}   // initialised

    // Widget(std::string name, int id, std::vector<int>& log) {
    //     name_ = name;   // name_ was default-constructed first, then assigned: two operations
    //     id_ = id;       // error: assignment to a const member
    //     log_ = log;     // error: a reference must be bound at initialisation
    // }
};

By the time the body's opening brace runs, every member already exists. A member you did not list was default-initialised — an empty std::string, a vector with nothing in it, an indeterminate int — and assigning to it in the body is a second operation on top of the first. For std::string that is a wasted construction; for a const member, a reference member, or a member whose type has no default constructor, it is a compile error, because the body is too late. The rule: initialise every member in the list; the body is for work that needs all the members to exist, such as validation or a log line. A default member initialiser (balance_ = 0 above) is the fallback the list does not override, which lets a class with several constructors state each default once.

Initialisation order is declaration order

class Buffer {
    std::vector<int> data_;
    std::size_t size_;
public:
    Buffer(std::size_t n) : size_(n), data_(size_) {}   // BUG: data_ is built first, from an indeterminate size_
};

Members are initialised in the order they are declared in the class, never in the order they appear in the list. Here data_ is declared first, so data_(size_) runs before size_(n), reading size_ before it holds anything — undefined behaviour that happens to work in a debug build and produces a random-length vector under -O2. GCC and Clang warn with -Wreorder (part of -Wall) whenever the list disagrees with the declaration order; treat it as an error. Write the list in declaration order, and never let one member's initialiser read a member declared after it — read the parameter (data_(n)) instead. Destruction runs in the reverse of construction: members are destroyed after the destructor body, last-declared first (Module 7, lesson 3).

Delegating constructors

class Money {
    long long cents_;
public:
    explicit Money(long long cents) : cents_(cents) {}
    Money(long long units, int hundredths) : Money(units * 100 + hundredths) {}   // delegates
    Money() : Money(0) {}
};

A constructor whose initialiser list names another constructor of the same class delegates the whole job to it; the delegating constructor's own body, if any, runs after the target finishes. The target must be the only entry in the list — you cannot delegate and also initialise a member. Delegation keeps validation in one place: whatever Money(long long) checks, every other constructor gets for free. Before C++11 the same effect needed a private init() member called from each constructor, which could not initialise const members and left objects half-built if it failed.

explicit

void charge(Money amount);

Money m = 5;      // without explicit: 5 converts to Money(5) — five cents, or five units?
charge(250);      // without explicit: an int silently becomes a Money

A constructor callable with one argument is a converting constructor: the compiler may use it to convert a value of that argument type into your class, implicitly, anywhere a value of your class is expected. That is what makes std::string s = "text"; work — and it is wrong for almost any class of your own, where a bare 250 has no obvious meaning. explicit switches the conversion off: Money m{250}; and Money(250) still work, Money m = 250; and charge(250) do not compile. The rule the standard library follows and this track adopts: every constructor that can be called with one argument is explicit unless the conversion is the whole point. explicit also applies to multi-argument constructors, where it blocks the brace form charge({2, 50}).

Validation belongs in the constructor

#include <stdexcept>

class Percentage {
    int value_;
public:
    explicit Percentage(int v) : value_(v) {
        if (v < 0 || v > 100) throw std::invalid_argument("percentage out of range");
    }
};

If an object can be built with inconsistent members, every member function has to defend against it forever. Checking in the constructor means a Percentage that exists is valid. When the arguments cannot make a valid object, the constructor's only escape is to throw — there is no return value to signal failure with — and a constructor that throws produces no object at all. Module 15 covers exceptions in full; lesson 6 of this module puts validation to work in a complete design.

Pitfalls

MistakeWhat happens
Account a();Declares a function; a.balance() fails with "request for member of non-class type".
Adding a constructor, keeping Account a; callersThe implicit default constructor vanished; add Account() = default; or initialise them.
Assigning members in the bodyDouble work at best; a compile error for const, references and non-default-constructible members.
Initialiser list out of declaration order-Wreorder; a member initialised from a later one reads garbage.
Delegating and initialising in the same listCompile error — the delegation must stand alone.
A single-argument constructor without explicitSilent conversions from int, double or const char* at every call site.

Key takeaways

  • Constructors have the class's name, no return type, and overload; declaring any constructor removes the implicit default one — = default restores it, = delete forbids it.
  • Initialise members in the member-initialiser list; the body runs after every member exists.
  • Members are initialised in declaration order regardless of the list; never read a later member.
  • A constructor can delegate to another; the target is then the whole list.
  • Mark single-argument constructors explicit unless an implicit conversion is intended.
  • Validate in the constructor, so that an object that exists is valid.

Common questions

In what order are class members initialised in C++?

In the order they are declared in the class, never the order written in the member-initialiser list. If a member's initialiser reads a member declared after it, it reads an uninitialised value. -Wreorder, part of -Wall, warns when the list's order differs from the declaration order.

What does the explicit keyword do in C++?

explicit stops a constructor from being used for implicit conversions. Without it, a one-argument constructor lets Money m = 250; or charge(250) silently turn an int into a Money; with it, only direct forms such as Money m{250}; compile. Mark single-argument constructors explicit unless the conversion is the point.

Why did my default constructor disappear?

The compiler generates a default constructor only when a class declares no constructors at all. As soon as you add any constructor, Account a; stops compiling. Write Account() = default; to get the compiler-generated one back, or = delete to forbid it on purpose.

What is a delegating constructor in C++?

A constructor whose member-initialiser list names another constructor of the same class, as in Money() : Money(0) {}. The target runs first, then the delegating constructor's body. The delegation must be the only entry in the list, and it keeps validation in one place.

How does a constructor report invalid arguments in C++?

By throwing an exception, such as std::invalid_argument, since a constructor has no return value. A constructor that throws produces no object at all, so validating there guarantees that every object which exists is valid and member functions need not check again.

Mark as readSign in to keep your place.

C++ Constructors and the Member Initialiser List Explained

How C++ constructors build a valid object: member initialiser lists, initialisation order, default and delegating constructors, and the explicit keyword.

  • Course: C++ study plan
  • Module: Classes and objects
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: Clang 18 · C++20

What is a member initialiser list in C++?

A member initialiser list is the part of a C++ constructor between the colon and the body, as in Account(std::string o) : owner_(std::move(o)), balance_(0) {}. It constructs each member directly with its value. Assigning in the body instead default-constructs the member first and then assigns, and is impossible for const members, references and types without a default constructor. Members are initialised in declaration order, whatever order the list uses.

Lesson

A constructor turns raw storage into an object that keeps its promises. It runs exactly once per object, before any other member function can, and it is the only code that ever sees the members before they hold meaningful values. This lesson covers the constructors you write and the ones the compiler writes for you, the member-initialiser list and why it beats assignment in the body, the rule that members are initialised in declaration order whatever the list says, delegating constructors, explicit, and = default / = delete.

Declaring constructors

#include <iostream>
#include <string>

class Account {
    std::string owner_;
    long long balance_ = 0;         // default member initialiser
public:
    Account() = default;                           // (1) default constructor
    Account(std::string owner, long long opening)  // (2) parameterised
        : owner_(std::move(owner)), balance_(opening) {}
    long long balance() const { return balance_; }
};

int main() {
    Account a;                       // (1)
    Account b{"ada", 500};           // (2)
    Account c("bob", 20);            // (2), parentheses also work
    std::cout << b.balance() << '\n';
    return 0;
}

A constructor has the class's name, no return type — not even void — and any number of parameters; constructors overload like functions (Module 4, lesson 3). A constructor with no parameters (or with defaults for all of them) is the default constructor, used by Account a;, by std::vector<Account> v(10), and by every array of Account. Braces and parentheses both call a constructor; braces reject narrowing and cannot be mistaken for a function declaration, which is why this track prefers them for objects.

What the compiler writes for you

If a class declares no constructor at all, the compiler generates a public default constructor that default-initialises each member: class-type members run their own default constructors, members with a default member initialiser take it, and fundamental members with neither are left indeterminate. Declare any constructor and the implicit default constructor is not generated — Account a; becomes a compile error the moment you add Account(std::string, long long). That is exactly the situation the = default line above handles: it asks for the compiler-generated version explicitly. The mirror image is = delete, which declares a constructor in order to forbid it:

class Session {
public:
    Session(int id) : id_(id) {}
    Session() = delete;         // "no Session without an id" — the error names the deleted function
private:
    int id_;
};

Copy and move constructors are generated by the same mechanism and are the subject of Module 9; for now, know that the rules for those are separate from the rule for the default constructor.

The member-initialiser list

The part between the : and the body is the member-initialiser list. It is not decoration — it is where members are constructed, and it is the difference between initialising and assigning:

class Widget {
    std::string name_;
    const int id_;
    std::vector<int>& log_;
public:
    Widget(std::string name, int id, std::vector<int>& log)
        : name_(std::move(name)), id_(id), log_(log) {}   // initialised

    // Widget(std::string name, int id, std::vector<int>& log) {
    //     name_ = name;   // name_ was default-constructed first, then assigned: two operations
    //     id_ = id;       // error: assignment to a const member
    //     log_ = log;     // error: a reference must be bound at initialisation
    // }
};

By the time the body's opening brace runs, every member already exists. A member you did not list was default-initialised — an empty std::string, a vector with nothing in it, an indeterminate int — and assigning to it in the body is a second operation on top of the first. For std::string that is a wasted construction; for a const member, a reference member, or a member whose type has no default constructor, it is a compile error, because the body is too late. The rule: initialise every member in the list; the body is for work that needs all the members to exist, such as validation or a log line. A default member initialiser (balance_ = 0 above) is the fallback the list does not override, which lets a class with several constructors state each default once.

Initialisation order is declaration order

class Buffer {
    std::vector<int> data_;
    std::size_t size_;
public:
    Buffer(std::size_t n) : size_(n), data_(size_) {}   // BUG: data_ is built first, from an indeterminate size_
};

Members are initialised in the order they are declared in the class, never in the order they appear in the list. Here data_ is declared first, so data_(size_) runs before size_(n), reading size_ before it holds anything — undefined behaviour that happens to work in a debug build and produces a random-length vector under -O2. GCC and Clang warn with -Wreorder (part of -Wall) whenever the list disagrees with the declaration order; treat it as an error. Write the list in declaration order, and never let one member's initialiser read a member declared after it — read the parameter (data_(n)) instead. Destruction runs in the reverse of construction: members are destroyed after the destructor body, last-declared first (Module 7, lesson 3).

Delegating constructors

class Money {
    long long cents_;
public:
    explicit Money(long long cents) : cents_(cents) {}
    Money(long long units, int hundredths) : Money(units * 100 + hundredths) {}   // delegates
    Money() : Money(0) {}
};

A constructor whose initialiser list names another constructor of the same class delegates the whole job to it; the delegating constructor's own body, if any, runs after the target finishes. The target must be the only entry in the list — you cannot delegate and also initialise a member. Delegation keeps validation in one place: whatever Money(long long) checks, every other constructor gets for free. Before C++11 the same effect needed a private init() member called from each constructor, which could not initialise const members and left objects half-built if it failed.

explicit

void charge(Money amount);

Money m = 5;      // without explicit: 5 converts to Money(5) — five cents, or five units?
charge(250);      // without explicit: an int silently becomes a Money

A constructor callable with one argument is a converting constructor: the compiler may use it to convert a value of that argument type into your class, implicitly, anywhere a value of your class is expected. That is what makes std::string s = "text"; work — and it is wrong for almost any class of your own, where a bare 250 has no obvious meaning. explicit switches the conversion off: Money m{250}; and Money(250) still work, Money m = 250; and charge(250) do not compile. The rule the standard library follows and this track adopts: every constructor that can be called with one argument is explicit unless the conversion is the whole point. explicit also applies to multi-argument constructors, where it blocks the brace form charge({2, 50}).

Validation belongs in the constructor

#include <stdexcept>

class Percentage {
    int value_;
public:
    explicit Percentage(int v) : value_(v) {
        if (v < 0 || v > 100) throw std::invalid_argument("percentage out of range");
    }
};

If an object can be built with inconsistent members, every member function has to defend against it forever. Checking in the constructor means a Percentage that exists is valid. When the arguments cannot make a valid object, the constructor's only escape is to throw — there is no return value to signal failure with — and a constructor that throws produces no object at all. Module 15 covers exceptions in full; lesson 6 of this module puts validation to work in a complete design.

Pitfalls

MistakeWhat happens
Account a();Declares a function; a.balance() fails with "request for member of non-class type".
Adding a constructor, keeping Account a; callersThe implicit default constructor vanished; add Account() = default; or initialise them.
Assigning members in the bodyDouble work at best; a compile error for const, references and non-default-constructible members.
Initialiser list out of declaration order-Wreorder; a member initialised from a later one reads garbage.
Delegating and initialising in the same listCompile error — the delegation must stand alone.
A single-argument constructor without explicitSilent conversions from int, double or const char* at every call site.

Key takeaways

  • Constructors have the class's name, no return type, and overload; declaring any constructor removes the implicit default one — = default restores it, = delete forbids it.
  • Initialise members in the member-initialiser list; the body runs after every member exists.
  • Members are initialised in declaration order regardless of the list; never read a later member.
  • A constructor can delegate to another; the target is then the whole list.
  • Mark single-argument constructors explicit unless an implicit conversion is intended.
  • Validate in the constructor, so that an object that exists is valid.

Common questions

In what order are class members initialised in C++?

In the order they are declared in the class, never the order written in the member-initialiser list. If a member's initialiser reads a member declared after it, it reads an uninitialised value. -Wreorder, part of -Wall, warns when the list's order differs from the declaration order.

What does the explicit keyword do in C++?

explicit stops a constructor from being used for implicit conversions. Without it, a one-argument constructor lets Money m = 250; or charge(250) silently turn an int into a Money; with it, only direct forms such as Money m{250}; compile. Mark single-argument constructors explicit unless the conversion is the point.

Why did my default constructor disappear?

The compiler generates a default constructor only when a class declares no constructors at all. As soon as you add any constructor, Account a; stops compiling. Write Account() = default; to get the compiler-generated one back, or = delete to forbid it on purpose.

What is a delegating constructor in C++?

A constructor whose member-initialiser list names another constructor of the same class, as in Money() : Money(0) {}. The target runs first, then the delegating constructor's body. The delegation must be the only entry in the list, and it keeps validation in one place.

How does a constructor report invalid arguments in C++?

By throwing an exception, such as std::invalid_argument, since a constructor has no return value. A constructor that throws produces no object at all, so validating there guarantees that every object which exists is valid and member functions need not check again.

Exercises

Money, three ways

Write class Money holding long long cents_ with three constructors. explicit Money(long long cents) is the target: it initialises the member in its member-initialiser list and throws std::invalid_argument when cents is negative. Money(long long units, int hundredths) delegates to it with units * 100 + hundredths, then checks in its own body that hundredths is 0–99 and throws otherwise. Money() delegates with 0. Add long long cents() const, Money add(const Money&) const (a new object; Money is a value) and std::string text() const printing <units>.<two digits>.

Read n commands. c <cents> builds with the first constructor, u <units> <hundredths> with the second, z with the third. Print each amount's text(), or invalid when the constructor threw (catch const std::invalid_argument&), and keep a running total of the valid ones starting from Money(). Finish with total <text>.

Input: n, then n command lines. Output: n + 1 lines.

4
c 1234
u 5 7
z
u 2 150

→

12.34
5.07
0.00
invalid
total 17.41

Robots, traced

Part is an instrumented member type: its constructor prints build <name> and its destructor scrap <name>. Robot has three Part members declared in the order head_, arm_, leg_, each with a default member initialiser naming it head, arm and leg, and a defaulted default constructor. Add two constructors:

  • explicit Robot(const std::string& prefix) initialises all three members in the member-initialiser list as <prefix>-head, <prefix>-arm, <prefix>-leg, then its body prints robot <prefix> ready;
  • explicit Robot(int serial) delegates to the first with "r" + std::to_string(serial), then its body prints serial <serial>.

Read n commands, each run in its own block so the robot is destroyed at the closing brace: d builds Robot r;, p <prefix> builds Robot r(prefix);, n <serial> builds Robot r(serial);. Print -- after each block. Predict the trace before you run it: members are built in declaration order and scrapped in reverse, whatever any list says.

Input: n, then n commands. Output: the trace.

2
d
n 7

→

build head
build arm
build leg
scrap leg
scrap arm
scrap head
--
build r7-head
build r7-arm
build r7-leg
robot r7 ready
serial 7
scrap r7-leg
scrap r7-arm
scrap r7-head
--

In this module: Classes and objects

← Structs and classes — a type of your own · this, const member functions and chaining →