Skip to content
VLSI Mentor

C++ Part I · Module 12 · Constructors and Destructors

Member Initializer Lists

The colon-and-comma clause between a constructor's signature and its body has now been used in every class in two modules and explained in none of them. The delay was deliberate, because its usual justification is the weaker one. It is normally introduced as a performance improvement — initializing a member is cheaper than default-constructing it and then assigning over the top — and that is entirely true, it is measurable, and this chapter measures it: three operations became one, and two of the three were pure waste. But a performance argument invites a judgement call, and readers reasonably conclude that assigning in the body is a slightly slower style they can use when it reads better. For three specific kinds of member that conclusion is wrong in a way that does not compile. A const member, a reference member, and a member whose own type has no default constructor all share one property: there is no valid earlier state for the body to assign over, because getting such a member into existence at all requires knowing its value. The compiler is unusually clear about this, producing three almost identically worded messages that name the member and say what is missing — and all three disappear the moment the initialization moves where it belongs.

Intermediate17 min readC++Part IFundamentalsConstructors

Part I · Module 12 · Chapter 12.3 · Member Initializer Lists

1. The Form

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Field(int offset, int width)
        : offset_(offset), width_(width)      // the member initializer list
    {
        /* body */
    }

Between the signature and the body: a colon, then a comma-separated list of member(value) or member{value}. Each entry initializes that member.

The braces form works too and is the form in this chapter's frozen index annotation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer(std::size_t n) : size_{n}, data_{new std::byte[n]} {}

2. Why the Body Can Only Assign

Chapter 12.1 §3 measured the fact this whole chapter rests on:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    member a constructed
    member b constructed
    member c constructed
  Owner body runs (all three members already exist)

Every member already exists when the body starts. So anything you write in the body happens to an object that is already there — which is, by definition, an assignment and not an initialization.

That means the real choice is not where to put the initialization. It is:

In the initializer listIn the body
initialize the member with the valueconstruct the member somehow, then assign over it

The second column's "somehow" is the problem, and §5 to §7 are the three cases where there is no somehow.

3. The Initializer List and Default Member Initializers

Chapter 11.2 §2 introduced the other way to initialize a member:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int n_ = 0;        // default member initializer

The two work together, and the rule is simple: a constructor's initializer list wins. If a member has a = 0 and a constructor initializes it to 5, it is 5 — the default is what happens when no constructor says otherwise.

So the practical arrangement is:

Default member initializer for a member with a sensible starting value that most constructors will not change.

Initializer list for anything a constructor computes or receives.

Both, commonly: the default guarantees nothing is ever indeterminate (Chapter 12.1 §4 measured an int reading 1 without one), and the list handles the cases that differ.

4. The Cost, Measured

A member type that announces every operation performed on it:

Azvya Education Pvt. Ltd.VLSI Mentor
work.cpp — the two classes
class AssignsInBody {
public:
    AssignsInBody() { std::printf("  AssignsInBody: body begins\n"); m_ = Tracked("hello"); }
private:
    Tracked m_;
};

class InitializesInList {
public:
    InitializesInList() : m_("hello") { std::printf("  InitializesInList: body begins\n"); }
private:
    Tracked m_;
};

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
AssignsInBody:
    default ctor
  AssignsInBody: body begins
    ctor from "hello"
    copy ASSIGNMENT

InitializesInList:
    ctor from "hello"
  InitializesInList: body begins

Three operations against one. And it is worth naming which three, because the waste is specific:

A default construction that happened before the body, producing a Tracked holding an empty string — immediately discarded.

A second construction, of the temporary Tracked("hello").

A copy assignment, moving the value from the temporary into the member.

The initializer-list version did exactly one thing: constructed the member from "hello" directly.

For a Tracked holding a std::string that is a real cost — Chapter 9.6 measured a std::string copy requiring a heap allocation in four of five cases, and Chapter 9.5 measured the inline capacity at 22 characters. For a bare int the wasted work is one store that the optimiser will usually remove. That variability is exactly why the performance argument is the weaker one: it is sometimes negligible, so it invites a judgement call. §5 removes the judgement.

5. Case One — A const Member

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class C {
    public:
        C(int v) { width_ = v; }      // assignment in the body
    private:
        const int width_;
    };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: constructor for 'C' must explicitly initialize the const member 'width_'
error: cannot assign to non-static data member 'width_' with const-qualified type 'const int'

Two errors, and they are two different complaints. The first says the constructor failed to initialize a member that must be initialized. The second says the assignment in the body is itself illegal — Chapter 3.9 established that a const object cannot be assigned to after it exists, and a const member is no exception.

With the initializer list — C(int v) : width_(v) {} — 0 errors.

The reason is the one that makes this chapter structural: a const member can be given a value exactly once, at the moment it comes into existence. The initializer list is that moment. The body is afterwards.

6. Case Two — A Reference Member

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class C {
    public:
        C(int& v) { ref_ = v; }
    private:
        int& ref_;
    };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: constructor for 'C' must explicitly initialize the reference member 'ref_'

With the list — C(int& v) : ref_(v) {} — 0 errors.

Note what the body's ref_ = v would have meant even if the member had somehow existed: Chapter 8.5 established that a reference cannot be reseated, so assigning to a reference assigns through it, to the referred-to object. It would not have bound the reference; it would have overwritten whatever it was already bound to. Since an uninitialized reference cannot exist in the first place, the compiler stops at the earlier error.

A reference must be bound when it is created and can never be rebound. The initializer list is the only place that binding can happen.

7. Case Three — A Member With No Default Constructor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class NoDefault {
    public:
        explicit NoDefault(int v) : v_(v) {}
    private:
        int v_;
    };

    class C {
    public:
        C(int v) { m_ = NoDefault(v); }    // needs m_ to already exist
    private:
        NoDefault m_;
    };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: constructor for 'C' must explicitly initialize the member 'm_' which does not have a default constructor
note: member is declared here
note: 'NoDefault' declared here

With the list — C(int v) : m_(v) {} — 0 errors.

This is the case that occurs most often in real code, and it is the one Chapter 11.1 §6 already set up: a type with a genuine invariant usually has no valid default state, so it has no default constructor, so it cannot be a member of anything that tries to assign to it in the body. Chapter 11.1 measured Field h; correctly failing for exactly this reason.

So the three cases are not a list of exotic corners. They are: const data, references, and any well-designed type with an invariant.

8. The Three Messages Side by Side

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
const member      : must explicitly initialize the const member 'width_'
reference member  : must explicitly initialize the reference member 'ref_'
no default ctor   : must explicitly initialize the member 'm_' which does not
                    have a default constructor

One sentence, three endings. The compiler is saying the same thing each time: this member cannot be brought into existence without a value, so you must supply one where values are supplied.

And the worked example from this chapter's index annotation, which has one of each kind of consideration:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    class Buffer {
    public:
        Buffer(std::size_t n) : size_{n}, data_{new std::byte[n]} {}
        ~Buffer() { delete[] data_; }
    private:
        std::size_t  size_;
        std::byte*   data_;
    };

Both members are initialized, in declaration order, before the body — which is empty, because there is nothing left to do. (std::byte is a C++17 type for raw bytes, named here and belonging to Part II; new[]/delete[] are Chapter 10.4's.)

That constructor is also Chapter 12.9's subject in miniature: it acquires in the initializer list and releases in the destructor, and Chapter 12.2 §8 measured why that is safe — if the allocation throws, size_ is destroyed and the object never existed.

9. One Hazard to Know About Here

The initializer list looks like it controls the order members are built in. It does not — declaration order does — and writing the list in a different order is a real bug when one member's initializer reads another.

Chapter 12.8 measures this directly, including the compiler warning that catches it. It is flagged here so that you do not form the wrong model while learning the syntax: the list says what each member is initialized with, not when.

10. What to Actually Do

Initialize every member in the initializer list. Reserve the body for checking and for work that is not initialization.

Give members default member initializers too where a sensible default exists — the list overrides them.

Expect to need the list for const members, reference members, and any member whose type has an invariant.

Use a private static helper or a named factory when a member's value takes several statements to compute.

Do not default-build a member and assign over it. Measured: three operations instead of one, and two were waste.

Write the list in declaration order — Chapter 12.8 is why, and the compiler will tell you.

Repeat nothing: if several constructors need the same initialization, delegate (Chapter 12.6).

And read the three error messages as one message — this member needs a value where values are given.

11. Mistakes Worth Making Once

Assigning to a member in the body. Three operations for one; two wasted.

Assigning to a const member. Two errors: not initialized, and cannot assign.

Assigning to a reference member. must explicitly initialize the reference member — and it would have assigned through the reference anyway.

Holding a member whose type has no default constructor and assigning to it. The commonest of the three, and the sign of a well-designed member type.

Treating the list as a performance micro-optimisation. For a std::string member it is an allocation (Chapter 9.6); for three kinds of member it is the only option.

Assuming the list controls construction order. It does not — Chapter 12.8.

Adding a default constructor to a member type so the body can assign to it. You have weakened that type to simplify this one.

Putting initialization in the body because the computation is long. Use a helper or a factory.

12. Interview Relevance

"Why use a member initializer list instead of assigning in the constructor body?" The complete answer has both halves: it avoids a default construction plus an assignment (measured: three operations versus one), and for const members, reference members, and members without a default constructor the body cannot do it at all. Candidates who give only the performance half have given the weaker answer.

"Which members must be initialized in the list?" const, references, and members whose type has no default constructor — plus base classes, which is Module 16's.

"What does the compiler say if you get it wrong?" constructor for 'C' must explicitly initialize the const member 'width_', and two parallel messages for the other cases. Knowing the shape of the message is a sign of having tried it.

"Does the initializer list control the order members are constructed?" No — declaration order does. A common trap, and Chapter 12.8's measurement.

"How do default member initializers interact with the list?" The list wins; the default applies when no constructor initializes that member.

13. What You Should Be Able to Say Now

  1. The list sits between the signature and the body and initializes members; member(v) or member{v}.
  2. Every member already exists when the body starts (Chapter 12.1 §3), so the body can only assign.
  3. Measured: assigning in the body cost three operations — a discarded default construction, a second construction, and a copy assignment — against one.
  4. The performance argument is the weaker half, because for an int the waste is one removable store.
  5. Case one, const member: must explicitly initialize the const member 'width_' plus cannot assign to … const-qualified type.
  6. Case two, reference member: must explicitly initialize the reference member 'ref_' — and a reference cannot be reseated anyway (Chapter 8.5).
  7. Case three, no default constructor: must explicitly initialize the member 'm_' which does not have a default constructor.
  8. All three compile with 0 errors once moved to the list.
  9. The three cases are the shape of good design, not exotic corners — immutable state, references, and members with invariants.
  10. A default member initializer is overridden by the list.
  11. The list does not control construction order — declaration order does (Chapter 12.8).
  12. Put everything in the list; leave the body for checking.
  13. For a long computation use a private static helper or a named factory, never default-then-fix.

14. Exercises

Exercise 1 — rewrite it. Convert C(int a, int b) { x_ = a; y_ = b; } to use the list, and say how many operations you removed for a std::string member.

Exercise 2 — predict the error. For each, say whether it compiles and give the message: (a) const int c_; assigned in the body (b) int& r_; assigned in the body (c) std::string s_; assigned in the body (d) a member of a type with only explicit T(int), assigned in the body.

Exercise 3 — count the operations. For C() { s_ = std::string("abc"); } with std::string s_, list every operation in order and mark the wasted ones.

Exercise 4 — which must be in the list? Given members const int id_, int count_, Logger& log_, std::string name_, Field f_ (invariant, no default ctor) — say which must be initialized in the list and which merely should.

Exercise 5 — the design connection. Explain why following Chapter 11.5's advice about immutability makes initializer lists mandatory rather than optional.

Exercise 6 — the long computation. A member's value needs five statements to compute. Give two designs that keep it initialized once.

Exercise 7 — both mechanisms. Write a class with a member that has both a default member initializer and an initializer-list entry, and say what the value is.

Exercise 8 — read the message. A colleague shows you must explicitly initialize the member 'm_' which does not have a default constructor and asks whether to add a default constructor to m_'s type. Advise them.

15. Summary

  • The list initializes; the body can only assign, because members already exist (Chapter 12.1 §3).
  • Measured: body assignment cost three operations — discarded default construction, second construction, copy assignment — against one.
  • The performance half is the weaker half; the three impossible cases are the reason this is Critical.
  • const member: two errors — not initialized, and cannot assign.
  • Reference member: must explicitly initialize the reference member; a reference cannot be reseated.
  • Member with no default constructor: must explicitly initialize the member 'm_' which does not have a default constructor.
  • All three: 0 errors once moved to the list.
  • These three are the shape of good design — immutable state, references, and members with invariants.
  • The list overrides default member initializers, and does not control construction order (Chapter 12.8).
  • Put everything in the list; keep the body for checking.

The three cases in this chapter all involved a member that must be given a value, and said nothing about the opposite situation — a type where no constructor is written at all. That case is not the absence of a decision; the compiler makes one on your behalf, and Chapter 12.1 §4 already measured one consequence, an int member reading a leftover 1 with no warning. There are several such compiler-written functions, they appear and disappear according to rules worth knowing rather than discovering, and declaring a single constructor of your own silently removes one of them — which is why Chapter 11.1 §6's Field h; would not compile. The next chapter is which constructors exist, how to ask for one back, and how to make one exist specifically so that calling it is an error.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.