C++ / CLASSES AND OBJECT LIFECYCLE
Constructors and member initialiser lists
Write constructors that initialize every member in the initializer list, including const and reference members, and predict the true initialization order.
What you will learn
- Initialize members in the constructor's list instead of assigning in the body
- Handle const members, reference members, and types with no default constructor
- Order the list to match declaration order, since that is the real construction order
- Read a constructor knowing every member is alive before the first body statement
Understanding Constructors and member initialiser lists
A constructor's contract is to hand back an object whose every member already holds a sensible value. The member initializer list, the comma-separated clause between the colon and the opening brace, is the only place where members are genuinely initialized; anything inside the braces already operates on members that exist. So Sensor(int id) : id_(id) {} builds id_ from id exactly once, while Sensor(int id) { id_ = id; } builds id_ with whatever default it has and then overwrites it.
The mental model is a checklist the compiler walks one member at a time, in the order the members are declared in the class. For each member it looks for a matching entry in your list; if there is none it falls back to the in-class default member initializer, then to the member's default constructor, and for a plain int or pointer with neither of those, to nothing at all, leaving an indeterminate value. Because the walk follows declaration order, rearranging the list changes nothing about execution; it only makes the code lie about what happens, which is why compilers offer -Wreorder.
For some members the list is not an optimisation but the only legal route. A const member and a reference member each get exactly one chance to acquire a value, and a member whose type has no default constructor has no empty state to sit in while the body runs, so assigning any of those later is a compile error. For everything else the list still wins on cost: a std::string or std::vector member assigned in the body is built empty, then discarded, sometimes with an allocation in each direction.
<iostream>
<string>
<utility>
class Tag {
public:
Tag() : text_("<none>") { std::cout << " Tag() default\n"; }
explicit Tag(std::string t) : text_(std::move(t)) {
std::cout << " Tag(" << text_ << ")\n";
}
Tag& operator=(const Tag& other) {
text_ = other.text_;
std::cout << " Tag= " << text_ << "\n";
return *this;
}
private:
std::string text_;
};
class AssignInBody {
public:
explicit AssignInBody(const std::string& t) {
tag_ = Tag(t); // tag_ was already default-built before we got here
}
private:
Tag tag_;
};
class InitInList {
public:
explicit InitInList(const std::string& t) : tag_(t) {}
private:
Tag tag_;
};
int main() {
std::cout << "AssignInBody:\n";
AssignInBody slow("slow");
std::cout << "InitInList:\n";
InitInList fast("fast");
}
Members are initialized by the initializer list in declaration order, before the constructor body starts running.
Worked examples
Declaration order decides, not list order
The members are constructed in the order they are declared even though the list names them in reverse.
<iostream>
struct Part {
explicit Part(const char* name) { std::cout << "init " << name << "\n"; }
};
class Machine {
public:
Machine() : third_("third"), first_("first"), second_("second") {
std::cout << "body\n";
}
private:
Part first_;
Part second_;
Part third_;
};
int main() {
Machine m;
}
Example explained
Line 1Part has no default constructor, so every member has to appear somewhere in the list.
Line 2The list is written third_, first_, second_ but the printed order is first_, second_, third_.
Line 3The compiler matches list entries to members while walking the class declaration top to bottom; g++ and clang++ report this mismatch as -Wreorder.
Line 4"body" prints last because the constructor body starts only after the final member is constructed.
Members that have no other option
A const member, a reference member, and a member without a default constructor all get their values from the list.
<iostream>
<string>
class Port {
public:
explicit Port(int number) : number_(number) {}
int number() const { return number_; }
private:
int number_;
};
class Connection {
public:
Connection(std::string& log, int id, int portNumber)
: id_(id), log_(log), port_(portNumber) {
log_ += "opened id=" + std::to_string(id_)
+ " port=" + std::to_string(port_.number());
}
void describe() const {
std::cout << "connection " << id_ << " on port " << port_.number() << "\n";
}
private:
const int id_;
std::string& log_;
Port port_;
};
int main() {
std::string log;
Connection c(log, 7, 8080);
c.describe();
std::cout << "log: " << log << "\n";
}
Example explained
Line 1id_ is const, so id_(id) in the list is its only chance to get a value; id_ = id; in the body would not compile.
Line 2log_ is a reference bound to the caller's string here and can never be re-seated afterwards.
Line 3Port declares only Port(int), so port_(portNumber) calls that constructor directly instead of default-building a Port that does not exist.
Line 4The body can append to log_ safely because all three members finished initializing before the first statement ran.
Default member initializers fill the gaps
Members the list does not name keep their in-class default initializers.
<iostream>
class Retry {
public:
Retry() {}
explicit Retry(int attempts) : attempts_(attempts) {}
void print() const {
std::cout << "attempts=" << attempts_ << " delayMs=" << delayMs_ << "\n";
}
private:
int attempts_ = 3;
int delayMs_ = 100;
};
int main() {
Retry fallback;
Retry aggressive(5);
fallback.print();
aggressive.print();
}
Example explained
Line 1attempts_ = 3 and delayMs_ = 100 are default member initializers, used for any member a constructor's list does not mention.
Line 2Retry(int) names attempts_, so 5 is used instead of 3; the 3 is not stored and then overwritten.
Line 3delayMs_ becomes 100 in both objects because neither constructor mentions it.
Line 4Retry() has an empty list and an empty body, yet both members are still initialized, because the class-level initializers do that work.
Important notes
Parentheses and braces are not interchangeable in the list: n_{3.7} on an int is a narrowing error while n_(3.7) truncates to 3, and for std::vector<int> v_, v_{3} means one element equal to 3 while v_(3) means three zeros.
The same list also initializes base classes, and base subobjects are always constructed before any data member no matter where you write them.
Common mistakes
Assigning in the body, as in Buffer(int n) { size_ = n; }: for a const or reference member this does not compile at all, and for a std::string or std::vector member it silently builds an empty one first and then replaces it.
Assuming the list executes top to bottom: : total_(rows_ * cols_), rows_(r), cols_(c) with total_ declared first reads rows_ and cols_ before they are initialized, so total_ holds garbage and the program is undefined with no error message.
Naming the parameter exactly like the member and writing x = x; in the body: that assigns the parameter to itself and leaves the member uninitialized, whereas : x(x) works because the name before the parentheses is looked up as a member.
Try it yourself
Change, predict, then run
Write a class Interval whose members are declared const int lo_; const int hi_; int span_; initialize all three in a single two-argument constructor's list and print span_ for Interval(3, 11). Then add lo_ = 0; to the constructor body and read the error the compiler gives you.
Open the C++ workspaceCheck your understanding
A class declares int len_; first and std::string name_; second. Its constructor is Widget(std::string s) : name_(std::move(s)), len_(name_.size()) {}. What actually happens when a Widget is created?
- The list runs left to right, so name_ is in place before len_ is computed and len_ ends up correct.
- The program is rejected, because one member initializer may not refer to another member.
- len_ is initialized first, from a name_ whose constructor has not run yet, so its value is meaningless.
- len_ is zero-initialized and then updated once name_ has been constructed.
Show answer
Initialization order comes from the order the members are declared in the class, not from the order written in the list, so len_ is built first and calling name_.size() on a std::string that has not been constructed is undefined behaviour. The first option is tempting because the list reads like a sequence of statements, but the compiler treats it as a set of initializers it consults while walking the members in declaration order, and warns about the mismatch with -Wreorder. Referring to another member is legal in itself; it is only safe when that member is declared earlier.