Hello! Welcome back to our deep dive into C++.
In the last lesson, we explored the lifecycle of an object, focusing on the fundamental roles of constructors and destructors. We traced their execution order and got a first look at member initializer lists, contrasting them with assignment inside the constructor body. We saw a hint that initializer lists are more performant, but today we're going to prove it and, more importantly, discover when they are not just a good idea, but an absolute necessity.
Today's lesson is dedicated to mastering a crucial C++ feature. Our goal is to use initialization lists and explain their performance and correctness benefits over assignment in constructors. By the end of this lesson, you'll understand precisely why and when to use them, moving from "a thing you can do" to a fundamental part of writing correct and efficient C++ code.
The Two Paths to an Initial Value: Initialization vs. Assignment
Let's start by solidifying the mechanical difference between these two approaches. When a constructor executes, all member variables must be created before the constructor's body ({...}) begins. The question is how they are created.
-
Assignment in the Body:
- First, each member object is default-initialized. For a class type like
std::string, this means its default constructor (std::string()) is called. For anint, its value is indeterminate. - Second, the code inside the constructor body runs. The assignment operator (
=) is called to give the member its intended value. - This is a two-step process: default construction + assignment.
- First, each member object is default-initialized. For a class type like
-
Member Initializer List:
- Each member object is directly initialized using the constructor specified in the initializer list. For example, for a
std::stringmembername,name(n)calls thestd::stringcopy constructor directly. - This is a one-step process: direct initialization.
- Each member object is directly initialized using the constructor specified in the initializer list. For example, for a
Conceptually, this is analogous to creating a database record. Using assignment is like running an INSERT statement to create a row with default/null values, and then immediately running an UPDATE statement to set the actual values. An initializer list is like running a single, complete INSERT statement with all the initial values provided at once. The latter is clearly more efficient.
Performance: Avoiding the "Double Initialization"
For simple types like int, a modern compiler will likely optimize away any difference. However, for any non-trivial class type (like std::string, std::vector, or your own classes), the cost of assignment is real. It involves an unnecessary constructor call and an assignment operation, which can mean extra memory allocations and copying.
Let's see this inefficiency in action.
C++ Initializing Larger Data Members
The following video by Jamie King clearly demonstrates this 'double initialization' problem. Watch how using assignment in the constructor body results in an extra, useless constructor call for a member object.
Please watch from 03:16 to 05:56. Pay close attention to the console output. Notice how without an initializer list, a parameter-less constructor is called first, followed by a second constructor call via assignment. The initializer list eliminates the first, redundant call. The explanation using std::string is particularly relevant, as it highlights the cost of unnecessary memory operations.
The key takeaway is that an initializer list doesn't assign; it constructs. This is the most efficient way to bring a member object into existence in its desired initial state. The C++ FAQ puts it best in the resource "Constructors, C++ FAQ" (which you have access to from the previous lesson): "the result of the ... expression is constructed directly inside [the member] — the compiler does not make a separate copy of the object."
Correctness: When Initializer Lists are Mandatory
Beyond performance, there are critical situations where using an initializer list is the only way to correctly write your code. In these cases, assignment is not just inefficient; it's a compile-time error.
These situations arise for any member variable that cannot be assigned to after it has been created.
-
Initializing
constmembers: Aconstvariable must be given its value at the moment of its creation. Once initialized, its value is immutable. You cannot default-construct it and then assign to it later. -
Initializing reference members: A reference (
&) is an alias for another object. It must be bound to that object when it is declared. It cannot be "unbound" or "re-seated" to refer to a different object later. Therefore, it must be initialized in the initializer list. -
Initializing member objects whose class has no default constructor: If a member's class type does not provide a default constructor (one that takes no arguments), the compiler has no way to perform the "default construction" step required for assignment. You must explicitly tell the compiler which constructor to use, and the initializer list is the mechanism for doing so.
-
Initializing a base class: Similarly, if you are inheriting from a base class that requires arguments for its constructor, you must call that specific base class constructor in the derived class's initializer list.
To see each of these cases explained with clear examples, let's turn to another excellent resource.
Member Initializer Lists | C++ Tutorial
The video 'Member Initializer Lists | C++ Tutorial' by Portfolio Courses walks through each of these mandatory use-cases systematically.
Watch the following segments, focusing on why assignment fails in each case: const members (10:19 - 12:31): Observe how trying to assign to a const member in the constructor body results in a compilation error. Members without a default constructor (12:31 - 15:02): See why the compiler insists that the member be 'explicitly initialized' when no default constructor is available. Reference members (15:02 - 18:04): Understand why a reference member, which must be bound at creation, cannot be initialized via assignment. Base class constructors (18:04 - 20:23): Note how the initializer list is used to pass arguments up to the base class constructor.
As a handy reference with concise code snippets for these mandatory cases, you can review the article "When do we use Initializer List in C++?". Sections 1 through 4 cover const members, reference members, members without a default constructor, and base class members, respectively.
A Modern C++ Refinement: Uniform Initialization {}
Since C++11, you have a choice of syntax in your initializer lists: parentheses () or curly braces {}.
class Example {
int x;
std::string s;
public:
// Parentheses (traditional)
Example(int val, const char* str) : x(val), s(str) {}
// Curly Braces (uniform initialization)
Example(int val, const char* str) : x{val}, s{str} {}
};
While they often behave identically, using curly braces provides an important safety feature: it disallows "narrowing conversions." A narrowing conversion is an implicit type conversion that may result in a loss of data, for example, initializing an int with a double, or a char with an int that is too large.
Using () might allow this with a compiler warning, leading to silent data corruption. Using {} will cause a compile-time error, forcing you to address the potential bug. This makes your code more robust and correct by default.
Member Initializer Lists | C++ Tutorial
Let's see a practical demonstration of this safety feature.
Watch the final segment of the 'Portfolio Courses' video from 20:23 to 22:54. Notice how using parentheses () to initialize an int with a long int that's too large compiles but produces incorrect results, whereas using curly braces {} correctly flags this as a build error.
For this reason, it is considered modern C++ best practice to prefer using {} in your initializer lists.
Test your understanding!
You are tasked with creating a LogEntry class that encapsulates information about a system event. It must store:
- A reference to a
Loggerobject that it will use for context. - A constant, unique
entryID. - An
Eventobject, which describes the event. TheEventclass does not have a default constructor; it must be created with astd::stringmessage.
Here is the supporting code:
#include <iostream>
#include <string>
// Cannot be changed
class Logger {
public:
const std::string loggerName = "SystemLogger";
};
// Cannot be changed
class Event {
public:
std::string message;
// Note: No default constructor!
Event(std::string msg) : message(std::move(msg)) {}
};
class LogEntry {
private:
Logger& logger;
const int entryID;
Event event;
public:
// TODO: Implement this constructor correctly.
LogEntry(Logger& log, int id, std::string eventMsg) {
// ... your implementation here ...
}
void print() const {
std::cout << "Logger: " << logger.loggerName
<< ", ID: " << entryID
<< ", Event: " << event.message << std::endl;
}
};
int main() {
Logger mainLogger;
LogEntry entry(mainLogger, 101, "User login failed.");
entry.print();
return 0;
}
Complete the LogEntry constructor. Why is an initializer list the only possible way to implement it?
Show answer
Here is the correct implementation of the constructor:
LogEntry(Logger& log, int id, std::string eventMsg)
: logger{log}, entryID{id}, event{eventMsg}
{
// Body can be empty
}
An initializer list is mandatory for three reasons, one for each member variable:
loggeris a reference member (Logger&). It must be bound to an object at initialization.entryIDis aconstmember. It must be given its value at initialization.eventis an object of a class (Event) that does not have a default constructor. The compiler cannot create a defaultEventobject to be assigned to later; we must call itsEvent(std::string)constructor directly.
All three of these requirements can only be met by using a member initializer list.
Conclusion
You now have a complete picture of member initializer lists, moving beyond simple syntax to a deep understanding of their role in C++ object construction.
Key Takeaways:
- Performance: Initializer lists perform direct initialization, avoiding the "default construct + assign" two-step that is inefficient for class-type members.
- Correctness: They are mandatory for initializing
constmembers, reference members, and any member whose class lacks a default constructor. They are also the mechanism for calling specific base class constructors. - Best Practice: Always prefer initializer lists over assignment in the constructor body. List the members in the same order they are declared in the class to avoid subtle bugs.
- Modern Syntax: Prefer using curly braces
{}for uniform initialization to gain protection against unsafe narrowing conversions.
In our previous lesson, we saw how an object is born. Today, we've perfected that process. In our next lesson, we will confront a new challenge: what happens when an object is created not from raw values, but as a copy of another existing object? This will lead us into the famous "Rule of Three/Five", where we will implement copy constructors, move constructors, and assignment operators to gain full control over our object's lifecycle.