Whenever possible, we want to reduce redundant code (following the DRY principle -- Don’t Repeat Yourself).
Consider the following functions:
Both functions have a set of statements that do exactly the same thing (task A). In such a case, we can refactor like this:
In that way, we’ve removed redundant code that existed in functions A() and
B(). This makes our code easier to maintain, as changes only need to be made in one place.
When a class contains multiple constructors, it’s extremely common for the code in each constructor to be similar if not identical, with lots of repetition. We’d similarly like to remove constructor redundancy where possible.
Consider the following example:
The body of each constructor has the exact same print statement.
Author’s note
It’s generally not a good idea to have a constructor print something (except for debugging purposes), as this means you can’t create an object using that constructor in cases where you do not want to print something. We’re doing it in this example to help illustrate what’s happening.
Constructors are allowed to call other functions, including other member functions of the class. So we could refactor like this:
While this is better than the prior version (as the redundant statement has been replaced by a redundant
function call), it requires introduction of a new function. And our two constructors are also both
initializing m_name and m_id. Ideally we’d remove this redundancy too.
Can we do better? We can. But this is where many new programmers run into trouble.
Calling a constructor in the body of a function creates a temporary object
Analogous to how we had function B() call function A() in the example above,
the obvious solution seems like it would be to call the Employee(std::string_view, int)
constructor from the body of Employee(std::string_view, int, bool) in order to initialize
m_name, m_id, and print the statement. Here’s what that looks like:
But this doesn’t work correctly, as the program outputs the following:
Even though Employee Dave created printed, after e2 finished construction,
e2.m_name still appears to be set to its initial value of "???". How is that
possible?
We were expecting Employee(name, id) to call the constructor in order to continue
initialization of the current implicit object (e2). But initialization of a class object is
finished once the member initializer list has finished executing. By the time we begin executing the
constructor’s body, it’s too late to do more initialization.
When called from the body of a function, what looks like a function call to a constructor usually creates
and direct-initializes a temporary object (in one other case, you’ll get a compile error instead). In
our example above, Employee(name, id); creates a temporary (unnamed) Employee object. This
temporary object is the one whose m_name is set to Dave, and is the one that
prints Employee Dave created. Then the temporary is destroyed. e2 never has
its m_name or m_id changed from the default values.
Best practice
Constructors should not be called directly from the body of another function. Doing so will either
result in a compilation error, or will direct-initialize a temporary object.
If you do want a
temporary object, prefer list-initialization (which makes it clear you are intending to create an
object).
So if we can’t call a constructor from the body of another constructor, then how do we solve this issue?
Delegating constructors
Constructors are allowed to delegate (transfer responsibility for) initialization to another constructor from the same class type. This process is sometimes called constructor chaining and such constructors are called delegating constructors.
To make one constructor delegate initialization to another constructor, simply call the constructor in the member initializer list:
When e1 { "James" } is initialized, matching constructor
Employee(std::string_view) is called with parameter name set to
"James". The member initializer list of this constructor delegates initialization to other
constructor, so Employee(std::string_view, int) is then called. The value of
name ("James") is passed as the first argument, and literal 0 is
passed as the second argument. The member initializer list of the delegated constructor then initializes
the members. The body of the delegated constructor then runs. Then control returns to the initial
constructor, whose (empty) body runs. Finally, control returns to the caller.
The downside of this method is that it sometimes requires duplication of initialization values. In the
delegation to the Employee(std::string_view, int) constructor, we need an initialization
value for the int parameter. We had to hardcode literal 0, as there is no way
to reference the default member initializer.
A few additional notes about delegating constructors. First, a constructor that delegates to another constructor is not allowed to do any member initialization itself. So your constructors can delegate or initialize, but not both.
As an aside…
Note that we had Employee(std::string_view) (the constructor with less parameters)
delegate to Employee(std::string_view name, int id) (the constructor with more
parameters). It is common to have the constructor with fewer parameters delegate to the constructor
with more parameters.
If we had instead chosen to have Employee(std::string_view name, int id) delegate to
Employee(std::string_view), then that would have left us unable to initialize
m_id using id, as a constructor can only delegate or initialize, not both.
Second, it’s possible for one constructor to delegate to another constructor, which delegates back to the first constructor. This forms an infinite loop, and will cause your program to run out of stack space and crash. You can avoid this by ensuring all of your constructors resolve to a non-delegating constructor.
Best practice
If you have multiple constructors, consider whether you can use delegating constructors to reduce duplicate code.
Reducing constructors using default arguments
Default values can also sometimes be used to reduce multiple constructors into fewer constructors. For
example, by putting a default value on our id parameter, we can create a single
Employee constructor that requires a name argument but will optionally accept an id
argument:
Since default values must be attached to the rightmost parameters in a function call, a good practice when defining classes is to define members for which a user must provide initialization values for first (and then make those the leftmost parameters of the constructor). Members for which the user can optionally provide (because the default values are acceptable) should be defined second (and then make those the rightmost parameters of the constructor).
Best practice
Members for which the user must provide initialization values should be defined first (and as the leftmost parameters of the constructor). Members for which the user can optionally provide initialization values (because the default values are acceptable) should be defined second (and as the rightmost parameters of the constructor).
Note that this method also requires duplication of the default initialization value for m_id
(‘0’): once as a default member initializer, and once as a default argument.
A conundrum: Redundant constructors vs redundant default values
In the above examples, we used delegating constructors and then default arguments to reduce constructor redundancy. But both of these methods required us to duplicate initialization values for our members in various places. Unfortunately, there is currently no way to specify that a delegating constructor or default argument should use the default member initializer value.
There are various opinions about whether it is better to have fewer constructors (with duplication of initialization values) or more constructors (with no duplication of initialization values). Our opinion is that it’s usually more straightforward to have fewer constructors, even if it results in duplication of initialization values.
For advanced readers
When we have an initialization value that is used in multiple places (e.g. as a default member initializer and a default argument for a constructor parameter), we can define a named constant and use that wherever our initialization value is needed. This allows the initialization value to be defined in one place.
Although you could use a constexpr global variable for this, a better option is to use a
static constexpr member inside the class:
Use of the static keyword in this context allows us to have a single
default_id member that is shared by all Employee objects. Without the
static, each Employee object would have its own independent
default_id member (which would work, but be a waste of memory).
The downside of this approach is that each additional named constant adds another name that must be understood, making your class a little more cluttered and complex. Whether this is worth it depends on how many of such constants are required, and in how many places the initialization values are needed.
We cover static data members in lesson 15.6 -- Static member variables.