Non-static data members are supposed to be initialized in the constructor's initializer list as shown in the following example:
struct Point
{
double X, Y;
Point(double const x = 0.0, double const y = 0.0) : X(x), Y(y) {}
};
Many developers, however, do not use the initializer list, but prefer assignments in the constructor's body, or even mix assignments and the initializer list. That could be for several reasons--for larger classes with many members, the constructor assignments may look easier to read than long initializer lists, perhaps split on many lines, or it could be because they are familiar with other programming languages that don't have an initializer list or because, unfortunately, for various reasons they don't even know about it.
It is important to note that the order in which non-static data members are initialized is the order in which they were declared in the class definition, and not the order of their initialization in a constructor initializer list. On the other hand, the order in which non-static data members are destroyed is the reversed order of construction.
Using assignments in the constructor is not efficient, as this can create temporary objects that are later discarded. If not initialized in the initializer list, non-static members are initialized via their default constructor and then, when assigned a value in the constructor's body, the assignment operator is invoked. This can lead to inefficient work if the default constructor allocates a resource (such as memory or a file) and that has to be deallocated and reallocated in the assignment operator:
struct foo
{
foo()
{ std::cout << "default constructor" << std::endl; }
foo(std::string const & text)
{ std::cout << "constructor '" << text << "'" << std::endl; }
foo(foo const & other)
{ std::cout << "copy constructor" << std::endl; }
foo(foo&& other)
{ std::cout << "move constructor" << std::endl; };
foo& operator=(foo const & other)
{ std::cout << "assignment" << std::endl; return *this; }
foo& operator=(foo&& other)
{ std::cout << "move assignment" << std::endl; return *this;}
~foo()
{ std::cout << "destructor" << std::endl; }
};
struct bar
{
foo f;
bar(foo const & value)
{
f = value;
}
};
foo f;
bar b(f);
The preceding code produces the following output showing how data member f is first default initialized and then assigned a new value:
default constructor
default constructor
assignment
destructor
destructor
Changing the initialization from the assignment in the constructor body to the initializer list replaces the calls to the default constructor plus assignment operator with a call to the copy constructor:
bar(foo const & value) : f(value) { }
Adding the preceding line of code produces the following output:
default constructor
copy constructor
destructor
destructor
For those reasons, at least for other types than the built-in types (such as bool, char, int, float, double or pointers), you should prefer the constructor initializer list. However, to be consistent with your initialization style, you should always prefer the constructor initializer list when that is possible. There are several situations when using the initializer list is not possible; these include the following cases (but the list could be expanded with other cases):
- If a member has to be initialized with a pointer or reference to the object that contains it, using the this pointer in the initialization list may trigger a warning with some compilers that it is used before the object is constructed.
- If you have two data members that must contain references to each other.
- If you want to test an input parameter and throw an exception before initializing a non-static data member with the value of the parameter.
Starting with C++11, non-static data members can be initialized when declared in the class. This is called default member initialization because it is supposed to represent initialization with default values. Default member initialization is intended for constants and for members that are not initialized based on constructor parameters (in other words members whose value does not depend on the way the object is constructed):
enum class TextFlow { LeftToRight, RightToLeft };
struct Control
{
const int DefaultHeight = 20;
const int DefaultWidth = 100;
TextFlow textFlow = TextFlow::LeftToRight;
std::string text;
Control(std::string t) : text(t)
{}
};
In the preceding example, DefaultHeight and DefaultWidth are both constants; therefore, the values do not depend on the way the object is constructed, so they are initialized when declared. The textFlow object is a non-constant non-static data member whose value also does not depend on the way the object is initialized (it could be changed via another member function), therefore, it is also initialized using default member initialization when it is declared. text, on the other hand, is also a non-constant non-static data member, but its initial value depends on the way the object is constructed and therefore it is initialized in the constructor's initializer list using a value passed as an argument to the constructor.
If a data member is initialized both with the default member initialization and constructor initializer list, the latter takes precedence and the default value is discarded. To exemplify this, let's again consider the foo class earlier and the following bar class that uses it:
struct bar
{
foo f{"default value"};
bar() : f{"constructor initializer"}
{
}
};
bar b;
The output differs, in this case, as follows, because the value from the default initializer list is discarded, and the object is not initialized twice:
constructor
constructor initializer
destructor
Using the appropriate initialization method for each member leads not only to more efficient code but also to better organized and more readable code.