14.10 — Constructor member initializer lists

This lesson continues our introduction of constructors from lesson 14.9 -- Introduction to constructors.

Member initialization via a member initialization list

To have a constructor initialize members, we do so using a member initializer list (often called a “member initialization list”). Do not confuse this with the similarly named “initializer list” that is used to initialize aggregates with a list of values.

Member initialization lists are something that is best learned by example. In the following example, our Foo(int, int) constructor has been updated to use a member initializer list to initialize m_x, and m_y:

#include <iostream>

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y)
        : m_x { x }, m_y { y } // here's our member initialization list
    {
        std::cout << "Foo(" << x << ", " << y << ") constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

The member initializer list is defined after the constructor parameters. It begins with a colon (:), and then lists each member to initialize along with the initialization value for that variable, separated by a comma. You must use a direct form of initialization here (preferably using braces, but parentheses works as well) -- using copy initialization (with an equals) does not work here. Also note that the member initializer list does not end in a semicolon.

This program produces the following output:

Foo(6, 7) constructed
Foo(6, 7)

When foo is instantiated, the members in the initialization list are initialized with the specified initialization values. In this case, the member initializer list initializes m_x to the value of x (which is 6), and m_y to the value of y (which is 7). Then the body of the constructor runs.

When the print() member function is called, you can see that m_x still has value 6 and m_y still has value 7.

Member initializer list formatting

C++ provides a lot of freedom to format your member initializer lists as you prefer, as it doesn’t care where you put your colon, commas, or whitespace.

The following styles are all valid (and you’re likely to see all three in practice):

    Foo(int x, int y) : m_x { x }, m_y { y }
    {
    }
    Foo(int x, int y) :
        m_x { x },
        m_y { y }
    {
    }
    Foo(int x, int y)
        : m_x { x }
        , m_y { y }
    {
    }

Our recommendation is to use the third style above:

  • Put the colon on the line after the constructor name, as this cleanly separates the member initializer list from the function prototype.
  • Indent your member initializer list, to make it easier to see the function names.

If the member initialization list is short/trivial, all initializers can go on one line:

    Foo(int x, int y)
        : m_x { x }, m_y { y }
    {
    }

Otherwise (or if you prefer), each member and initializer pair can be placed on a separate line (starting with a comma to maintain alignment):

    Foo(int x, int y)
        : m_x { x }
        , m_y { y }
    {
    }

Member initialization order

Because the C++ standard says so, the members in a member initializer list are always initialized in the order in which they are defined inside the class (not in the order they are defined in the member initializer list).

In the above example, because m_x is defined before m_y in the class definition, m_x will be initialized first (even if it is not listed first in the member initializer list).

Because we intuitively expect variables to be initialized left to right, this can cause subtle errors to occur. Consider the following example:

#include <algorithm> // for std::max
#include <iostream>

class Foo
{
private:
    int m_x{};
    int m_y{};

public:
    Foo(int x, int y)
        : m_y{ std::max(x, y) }, m_x{ m_y } // issue on this line
    {
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

In the above example, our intent is to calculate the larger of the initialization values passed in (via std::max(x, y) and then use this value to initialize both m_x and m_y. However, on the author’s machine, the following result is printed:

Foo(-858993460, 7)

What happened? Even though m_y is listed first in the member initialization list, because m_x is defined first in the class, m_x gets initialized first. And m_x gets initialized to the value of m_y, which hasn’t been initialized yet. Finally, m_y gets initialized to the greater of the initialization values.

To help prevent such errors, members in the member initializer list should be listed in the order in which they are defined in the class. Some compilers will issue a warning if members are initialized out of order.

Best practice

Member variables in a member initializer list should be listed in order that they are defined in the class.

It’s also a good idea to avoid initializing members using the value of other members (if possible). That way, even if you do make a mistake in the initialization order, it shouldn’t matter because there are no dependencies between initialization values.

Member initializer list vs default member initializers

Members can be initialized in a few different ways:

  • If a member is listed in the member initializer list, that initialization value is used
  • Otherwise, if the member has a default member initializer, that initialization value is used
  • Otherwise, the member is default initialized.

This means that if a member has both a default member initializer and is listed in the member initializer list for the constructor, the member initializer list value takes precedence.

Here’s an example showing all three initialization methods:

#include <iostream>

class Foo
{
private:
    int m_x{};    // default member initializer (will be ignored)
    int m_y{ 2 }; // default member initializer (will be used)
    int m_z;      // no initializer

public:
    Foo(int x)
        : m_x{ x } // member initializer list
    {
        std::cout << "Foo constructed\n";
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ", " << m_z << ")\n";
    }
};

int main()
{
    Foo foo{ 6 };
    foo.print();

    return 0;
}

On the author’s machine, this output:

Foo constructed
Foo(6, 2, -858993460)

Here’s what’s happening. When foo is constructed, only m_x appears in the member initializer list, so m_x is first initialized to 6. m_y is not in the member initialization list, but it does have a default member initializer, so it is initialized to 2. m_z is neither in the member initialization list, nor does it have a default member initializer, so it is default initialized (which for fundamental types, means it is left uninitialized). Thus, when we print the value of m_z, we get undefined behavior.

Constructor function bodies

The bodies of constructors functions are most often left empty. This is because we primarily use constructor for initialization, which is done via the member initializer list. If that is all we need to do, then we don’t need any statements in the body of the constructor.

However, because the statements in the body of the constructor execute after the member initializer list has executed, we can add statements to do any other setup tasks required. In the above examples, we print something to the console to show that the constructor executed, but we could do other things like open a file or database, allocate memory, etc…

New programmers sometimes use the body of the constructor to assign values to members:

#include <iostream>

class Foo
{
private:
    int m_x{ 0 };
    int m_y{ 1 };

public:
    Foo(int x, int y)
    {
        m_x = x; // incorrect: this is an assignment, not an initialization
        m_y = y; // incorrect: this is an assignment, not an initialization
    }

    void print() const
    {
        std::cout << "Foo(" << m_x << ", " << m_y << ")\n";
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}

Although in this simple case this will produce the expected result, in case where members are required to be initialized (such as for data members that are const or references) assignment will not work.

Best practice

Prefer using the member initializer list to initialize your members over assigning values in the body of the constructor.

Detecting and handling invalid arguments to constructors

Consider the following Fraction class:

class Fraction
{
private:
    int m_numerator {};
    int m_denominator {};

public:
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator }
    {
    }
};

Because a Fraction is a numerator divided by a denominator, the denominator of a fraction cannot be zero (otherwise we get a divide by zero, which is mathematically undefined). In other words, this class has an invariant that m_denominator cannot be 0.

Related content

We discussed class invariants in lesson 14.2 -- Introduction to classes.

So what do we do when the user tries to create a Fraction with a zero denominator (e.g. Fraction f { 1, 0 };)?

Inside a member initializer list, our tools for detecting and handling errors are quite limited. We can use the conditional operator to detect an error, but then what?

class Fraction
{
private:
    int m_numerator {};
    int m_denominator {};

public:
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator != 0.0 ? denominator : ??? } // what do we do here?
    {
    }
};

We could change the denominator to a valid value, but then the user is going to get a Fraction that doesn’t contain the values they asked for, and we don’t have any way to notify them that we did something unexpected. Thus, we typically won’t try to do any kind of validation in the member initializer list -- we’ll just initialize the members with the values passed in, and then try to deal with the situation.

Inside the body of the constructor, we can use statements, so we have more options for detecting and handling errors. This is a good place to assert or static_assert that the arguments passed in are semantically valid, but that doesn’t actually handle runtime errors in a production build.

When a constructor cannot construct a semantically valid object, we say it has failed.

When constructors fail (a prelude)

In lesson 9.4 -- Detecting and handling errors, we introduced the topic of error handling, and discussed some options for handling cases where a function cannot proceed due to an error occurring. Since constructors are functions, they are susceptible to the same issues.

In that lesson, we suggested 4 strategies for dealing with such errors:

  • Resolve the error within the function.
  • Pass the error back to the caller to deal with.
  • Halt the program.
  • Throw an exception.

In most cases, we don’t have enough information to resolve such issues entirely within the constructor. So fixing the issue is generally not an option.

With non-member and non-special member functions, we can pass an error back to the caller to deal with. But constructors have no return value, so we don’t have a good way to do that. In some cases, we can add an isValid() member function (or an overloaded conversion to bool) that returns whether the object is currently in a valid state or not. For example, an isValid() function for Fraction would return true when m_denominator != 0.0. But this means the caller has to remember to actually call the function any time a new Fraction is created. And having semantically invalid objects that are accessible is likely to lead to bugs. So while this is better than nothing, it’s not that great of an option.

In certain types of programs, we can just halt the entire program and let the user rerun the program with the proper inputs… but in most cases, that’s just not acceptable. So probably not.

And that leaves throwing an exception. Exceptions abort the construction process entirely, which means the user never gets access to a semantically invalid object. So in most cases, throwing an exception is the best thing to do in these situations.

Key insight

Throwing an exception is usually the best thing to do when a constructor fails (and cannot recover). We discuss this further in lessons 27.5 -- Exceptions, classes, and inheritance and 27.7 -- Function try blocks.

Author’s note

For now, we’ll generally assume that construction of our class object succeeds in creating a semantically valid object.

For advanced readers

If exceptions aren’t possible or desired (either because you’ve decided not to use them or because you haven’t learned about them yet), there is one other reasonable option. Instead of letting the user create the class directly, provide a function that either returns an instance of the class or something that indicates failure.

In the following example, our createFraction() function returns a std::optional<Fraction> that optionally contains a valid Fraction. If it does, then we can use that Fraction. If not, then the caller can detect that and deal with it. We cover std::optional in lesson 12.15 -- std::optional and friend functions in lesson 15.8 -- Friend non-member functions.

#include <iostream>
#include <optional>

class Fraction
{
private:
    int m_numerator { 0 };
    int m_denominator { 1 };

    // private constructor can't be called by public
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator }
    {
    }

public:
    // Allow this function to access private members
    friend std::optional<Fraction> createFraction(int numerator, int denominator);
};

std::optional<Fraction> createFraction(int numerator, int denominator)
{
    if (denominator == 0.0)
        return {};
    
    return Fraction{numerator, denominator};
}

int main()
{
    auto f1 { createFraction(0, 1) };
    if (f1)
    {
        std::cout << "Fraction created\n";
    }

    auto f2 { createFraction(0, 0) };
    if (!f2)
    {
        std::cout << "Bad fraction\n";
    }   
}

Quiz time

Question #1

Write a class named Ball. Ball should have two private member variables, one to hold a color, and one to hold a radius. Also write a function to print out the color and radius of the ball.

The following sample program should compile:

int main()
{
	Ball blue{ "blue", 10.0 };
	print(blue);

	Ball red{ "red", 12.0 };
	print(red);

	return 0;
}

and produce the result:

Ball(blue, 10)
Ball(red, 12)

Show Solution

Question #2

Why did we make print() a non-member function instead of a member function?

Show Solution

Question #3

Why did we make m_color a std::string instead of a std::string_view?

Show Solution

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
438 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments