Member initializer lists
The part of a constructor between the colon and the opening brace is the member initialiser list, and it is where members are actually created. The body runs afterwards, when every member already exists.
Lesson 025 wrote the first lists. This lesson shows the order they run in, why some members can only be initialised there, and what a constructor that assigns in the body costs compared with one that uses the list.
The list runs before the body
Members are constructed in the list, before the first statement of the body. The program prints one line from a member's constructor and one from the body, and the member comes first.
The list is the only place where a member gets its initial value without a temporary. A member written in the list is constructed with the given arguments; a member left out is default-constructed, and for an int that means no value at all.
The order of the list does not decide the order of construction; the declaration order does. The next section makes that visible.
#include <iostream>
class tracer
{
public:
tracer()
{
std::cout << "member constructed\n";
}
};
class widget
{
public:
widget(int size) : size_(size), log_()
{
std::cout << "body, size " << size_ << '\n';
}
int size() const
{
return size_;
}
private:
int size_;
tracer log_;
};
int main()
{
widget w{5};
std::cout << w.size() << '\n';
}
g++ -std=c++23 -Wall -Wextra list.cpp -o list_demo
./list_demo
member constructed
body, size 5
5
Declaration order, not list order
Members are initialised in the order they are declared in the class, whatever order the list uses. The program below declares first and then second, initialises first from the argument and second from first, and the output is 10 and 11.
Because the list is written in the same order as the declarations, the reader and the compiler agree. That is a style rule worth keeping even when the compiler does not complain: a list in declaration order is the only one that cannot surprise you.
The next section shows what happens when the two orders disagree, and the warning GCC prints about it.
#include <iostream>
class record
{
public:
record(int value) : first(value), second(first + 1) {}
int first;
int second;
};
int main()
{
record r{10};
std::cout << r.first << ' ' << r.second << '\n';
}
g++ -std=c++23 -Wall -Wextra order.cpp -o order_demo
./order_demo
10 11
When the list disagrees
The same class with the list written backwards compiles, warns, and does the wrong thing. GCC reports that second will be initialised after first, and then that first is computed from an uninitialised member.
Both warnings come from -Wall. The program is not run here: first would be built from whatever the stack held, which is undefined behaviour, and the answer changes between builds.
The fix is to write the list in declaration order. A constructor that needs a value from another member should compute it in the body, or take an extra parameter, rather than depend on an initialisation order that the reader has to reconstruct.
#include <iostream>
class record
{
public:
record(int value) : second(value), first(second + 1) {}
int first;
int second;
};
int main()
{
record r{10};
std::cout << r.first << ' ' << r.second << '\n';
}
g++ -std=c++23 -Wall -Wextra reorder.cpp -o reorder_demo
reorder.cpp: In constructor 'record::record(int)':
reorder.cpp:9:9: warning: 'record::second' will be initialized after [-Wreorder]
9 | int second;
| ^~~~~~
reorder.cpp:8:9: warning: 'int record::first' [-Wreorder]
8 | int first;
| ^~~~~
reorder.cpp:6:5: warning: when initialized here [-Wreorder]
6 | record(int value) : second(value), first(second + 1) {}
| ^~~~~~
reorder.cpp:6:46: warning: member 'record::second' is used uninitialized [-Wuninitialized]
6 | record(int value) : second(value), first(second + 1) {}
| ^~~~~~
reorder.cpp: In constructor 'record::record(int)':
reorder.cpp:6:46: warning: '*this.record::second' is used uninitialized [-Wuninitialized]
6 | record(int value) : second(value), first(second + 1) {}
| ^~~~~~
Some members can only be initialised here
A member of a type without a default constructor has to be given its arguments in the list. The program below declares a ratio class whose only constructor takes two ints, and a member of that type in another class: the list is the only place that can pass them.
The same is true for const members and for references. A const int cannot be assigned in the body, and a reference cannot be reseated after it is bound, so both must be created in the list with their value.
The program prints a ratio and a doubled ratio. The second object is built through a delegating constructor, which calls the first one and then does a little more work.
#include <iostream>
class ratio
{
public:
ratio(int numerator, int denominator) : numerator_(numerator), denominator_(denominator) {}
double value() const
{
return static_cast<double>(numerator_) / denominator_;
}
private:
int numerator_;
int denominator_;
};
class scaled
{
public:
scaled(int numerator, int denominator) : ratio_(numerator, denominator) {}
scaled(int whole) : scaled(whole, 1) {}
const ratio& value() const
{
return ratio_;
}
private:
ratio ratio_;
};
int main()
{
scaled half{1, 2};
scaled whole{3};
std::cout << half.value().value() << ' ' << whole.value().value() << '\n';
}
g++ -std=c++23 -Wall -Wextra required.cpp -o required
./required
0.5 3
Delegating constructors
A constructor can call another constructor of the same class in its list. The target constructor runs first and the delegating body runs afterwards, so the shared work is written once.
The program below has a counter with two constructors: one that starts from zero and one that takes a value, where the first delegates to the second. Both produce a valid object and the printed totals are the same as the arguments.
Delegation is not the same as calling a member function in the body: the delegated constructor initialises the whole object, and the body of the delegating one runs on a complete object.
#include <iostream>
class counter
{
public:
counter() : counter(0) {}
explicit counter(int start) : total_(start) {}
int total() const
{
return total_;
}
private:
int total_;
};
int main()
{
counter zero;
counter five{5};
std::cout << zero.total() << ' ' << five.total() << '\n';
}
g++ -std=c++23 -Wall -Wextra delegating.cpp -o delegating
./delegating
0 5
A reference member left out of the list
A reference member that is not in the list has no value to bind to, and the constructor cannot assign one in the body: the reference exists from the moment the object is created and cannot be reseated.
GCC reports the uninitialised reference member and points at the declaration that should have been in the list. The fix is to add ref_(target) to the list of the constructor.
The same message appears for a const member, which cannot be assigned after construction either. Both cases are the reason the list exists in the first place.
class holder
{
public:
holder(int& target)
{
ref_ = target;
}
private:
int& ref_;
};
int main()
{
int value = 1;
holder h{value};
}
g++ -std=c++23 -Wall -Wextra reference_member.cpp -o reference_member
reference_member.cpp: In constructor 'holder::holder(int&)':
reference_member.cpp:4:5: error: uninitialized reference member in 'int&' [-fpermissive]
4 | holder(int& target)
| ^~~~~~
reference_member.cpp:10:10: note: 'int& holder::ref_' should be initialized
10 | int& ref_;
| ^~~~
Exercises
- Add a const int member to widget and initialise it from the constructor argument. Then try to assign it in the body and read the error.
- Write a class with two members of a type that has no default constructor and initialise both in the list.
- Give counter a third constructor that takes a counter and delegates to the int one. Which constructor runs first?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
When are the members of an object created?
Answer
In the member initialiser list, before the constructor body
The list is the creation of the members, and the body runs when they already exist.
-
In what order are members initialised?
Answer
The declaration order of the members
The list is a set of initialisers, and the compiler applies them in declaration order.
-
Which member can only be initialised in the list?
Answer
A reference member
A reference has no default state, so it must be bound when the member is created.
-
What does a delegating constructor do?
Answer
It calls another constructor of the same class
The delegation puts the shared work in one constructor and reuses it.
-
What does this program print?
#include <iostream> struct member { member() { std::cout << "member constructed\n"; } }; class holder { public: explicit holder(int size) : data_(size) { std::cout << "body, size " << data_ << '\n'; } int get() const { return data_; } private: member m_; int data_; }; int main() { holder h{5}; std::cout << h.get(); }Answer
member constructed / body, size 5 / 5
The member is built first, then the body runs and prints the size it received.
-
What does this program print?
#include <iostream> class record { public: record(int value) : first_(value), second_(first_ + 1) {} int first() const { return first_; } int second() const { return second_; } private: int first_; int second_; }; int main() { record r{10}; std::cout << r.first() << ' ' << r.second(); }Answer
10 11
The first member is initialised before the second, so the sum uses 10 and gives 11.
-
What does this program print?
#include <iostream> class holder { public: holder(int& target, int fixed) : ref_(target), fixed_(fixed) {} void show() const { std::cout << ref_ * 0.5 << ' ' << fixed_; } private: int& ref_; const int fixed_; }; int main() { int value = 1; holder h{value, 3}; h.show(); }Answer
0.5 3
The reference and the const member are bound in the list, and the program reads them back.
-
What does this program print?
#include <iostream> class box { public: box() : box(0) {} explicit box(int value) : value_(value) {} int get() const { return value_; } private: int value_; }; int main() { box a; box b{5}; std::cout << a.get() << ' ' << b.get(); }Answer
0 5
The default constructor delegates to the other one with zero, and the two objects print their values.
-
What does the compiler warn about when the list order differs from the declaration order?
Answer
'record::second' will be initialized after 'int record::first'
-Wreorder names the members and the constructor, because the written order is not the real one.
-
What does leaving a reference member out of every list produce?
Answer
uninitialized reference member in 'int&'
A reference cannot be left unbound, so the constructor has to initialise it.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev