devxlogo

Mastering Class Member Initialization

Mastering Class Member Initialization

n a previous column I explained the rules of POD initialization. The rules of class member initialization are radically different, due to the fact that, in certain contexts, member initialization is mandatory. In addition, class constants have a wide range of initialization forms, depending on their type. The following sections shed some light on the intricate rules of class member initialization and show how to avoid common bugs and inefficiencies.


How do you initialize class members that require explicit initialization? How do you define class constants?


Distinguish between assignment and initialization. Use the appropriate initialization form according to the members type.

Distinguishing Between Assignment and Initialization
Some programmers believe that the proper place for initializing data members is inside a constructor. For example:

class Task{private: int pid; string name;public: Task(int num, const string & n)  {  pid=num;  name=n;   }};

This is wrong. The two statements in the constructor assign, rather than initialize, the members pid and name. When dealing with objects, the performance overhead can be noticeable. The object is first constructed and only then is it assigned. To see the difference, replace the type of the member name. Instead of a string, it will be an object of the following class. By the way, notice how the static data member is initialized:

class Test{public: Test() {  ctor_count++;  cout

ctor_count is a static member thats incremented every time its constructor, copy-constructor, or assignment operator is called. This way, you can track the phases of its construction. Notice that only the definition of a static data member may initialize it (I will discuss the initialization of const static members shortly). Class Task now looks as follows:

class Task{private: int pid; Test name; // type changed from string to Testpublic: Task (int num, const Test & n) {pid=num; name=n;}};

Test It
Use the following program to test the code:

int main(){ Test t; Task task(1, t);}

The output from this program (shown in Figure 1) shows that the assignment inside Task's constructor takes place after t's construction:

 
Figure 1: Here, the assignment inside Task's constructor takes place after t's construction.

The program constructs the object t which is used as an argument for Task's constructor (line #1). Next, it constructs the sub-object name using its default constructor (line #2). Finally, Task's constructor invokes Test's assignment operator to assign n to name (line #3). The last two phases can be combined into one.

Proper Member Initialization
A real initialization of a data member uses a special syntactic construct called a member initialization list, which looks like this:

class Task{//..public: Task(int num, const Test & n) : pid (num), name (n) {}};

Using Task's modified constructor, the same program now produces the output shown in Figure 2.

 
Figure 2: This is the output using Task's modified constructor.

Mandatory Initialization
const members, references, and sub-objects (i.e., embedded objects and base classes) whose constructors take arguments necessitate a member initialization list. For example, a class that has a data member of type std::vector can pass an argument to its constructor like this:

class Thing{private: int & ref; // reference member const int MAX; // const member  vector arr;public: Thing(int& r) : ref(r), MAX(100), arr (MAX) {}};

Notice that the member MAX serves as arr's initializer. This is perfectly legal. C++ ensures that members are initialized in the order of their declaration in the class. If you change the member initialization list to:

Thing(int& r) : arr(MAX), MAX(100), ref(r) {}

The members will still be initialized in the following order: ref, MAX, and arr. However, pay attention to the members' order inside the class. Doing something like this causes unpredictable results at runtime:

class BadThing{private: int & ref; Array arr; //wrong, should appear after MAX const int MAX;};

The problem is that MAX hasnt been initialized when its value is passed to arr's constructor.

Defining Class Constants
Sometimes, you need an internal constant that all instances of the same class share. In earlier stages of C++, an anonymous enum was used for this purpose:

class Allocator{ enum { PAGE_SIZE=1024 };//};

Although this form is still in use, standard-compliant compilers offer a preferable method of defining class constants:

class Allocator{ static const int PAGE_SIZE=1024;public: Allocator(int pages){ p=new char[pages*PAGE_SIZE];}//};const int Allocator::PAGE_SIZE; //no initializer here

This kind of initialization is permitted only with const static members of integral types. For any other type, use a member initialization list:

class Math{ const static double PI;//};const double Math::PI= 3.14159265358979;

Knowing When and How
The primary form of initializing members is a member initialization list. For const data, references and subobjects whose constructors take arguments, the use of a member initialization list is mandatory. As a special case, const static members of an integral type are initialized inside the class body. Other static data members are initialized when defined, outside the class body.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist