Discussion: Who’s Up First?

Execute the code to understand the output and gain insights into constructors.

Run the code

Now, it’s time to execute the code and observe the output.

Press + to interact
#include <iostream>
struct Resource
{
Resource()
{
std::cout << "Resource constructed\n";
}
};
struct Consumer
{
Consumer(const Resource &resource)
{
std::cout << "Consumer constructed\n";
}
};
struct Job
{
Job() : resource_{}, consumer_{resource_} {}
Consumer consumer_;
Resource resource_;
};
int main()
{
Job job;
}

Understanding the output

The Job class has two members, consumer_ and resource_, declared in that order. However, in the constructor’s member initializer list (the : resource_{}, consumer_{resource_} part), they are initialized in the opposite order. In which order are they actually initialized?

Member initialization in C++

C++ always initializes members in the order they are declared in the class, not in the order they are listed in the constructor’s member initializer list. This ensures that members are always initialized in a deterministic order, even if different constructors happen to list them in different orders in their member initializer lists. So even if the resource seems to be initialized first in the constructor, consumer_ is actually initialized first.

Having members initialized in a deterministic order is essential if one member depends on the other. For instance, in this example, the consumer depends on the resource in its constructor. Everything seems to be okay in the constructor, but since consumer_ is actually initialized before resource_, the Consumer constructor gets passed a Resource that hasn’t been constructed yet. If we tried to use it for anything, we would have undefined behavior.

Member destruction order

The reverse is also true. Members are destroyed in the opposite order of their declarations in the class. Destruction order can be as important as construction order since one member can depend on another in its destructor, or there can be other lifetime dependencies. Imagine, for instance, the Consumer in this puzzle runs a thread that uses Resource, and that Consumer joins the thread in its destructor. In the current version of the code, resource_ would be destroyed while the thread is still running, and we would risk using a destroyed resource_, resulting in undefined behavior. If consumer_ had been declared after resource_, however, the thread in consumer_ would have stopped before resource_ was destroyed.

Listing the members in the wrong order in the member initializer list can be confusing, so it is recommended to always use the same order in the member initializer list and the class declaration. GCC, Clang, and MSVC all have warnings (-Wreorder, -Wreorder-ctor, and C5038, respectively) that warn us if we don’t use a consistent order. These warnings are enabled if we compile with -Wall//Wall, which is always recommended. Different compilers often have different warnings, so compiling with several compilers can be a good idea. Compiling with several compilers can also help us avoid accidentally using non-standard, platform-specific variations of C++, which would make it harder to port our project to other platforms later.

Recommendations

Here are some recommendations to ensure code consistency and portability:

  • Always enable warnings: Enabling compiler warnings helps identify potential issues and maintain code quality.

  • Compile with several compilers: Try compiling your code with several compilers (e.g., GCC, Clang, MSVC) to ensure portability and catch compiler-specific issues.

Level up your interview prep. Join Educative to access 70+ hands-on prep courses.