关于编译器生成默认构造函数的一些误区

When a class we write does not explicitly provide a constructor but the compiler needs a constructor (be sure to note this phrase), the compiler will generate one for us. However, the default constructor generated by the compiler does not meet our expectations of what it can accomplish.

Let’s analyze this in detail.

1
2
3
4
5
6
7
8
9
10
class testClass{
public:
int x;
double y;
};
// Using testClass
testClass x;
cout<<x.x<<"\t"<<x.y<<endl;
testClass y;
cout<<y.x<<"\t"<<y.y<<endl;

The output:

In the code above, we did not explicitly define a default constructor. We expect the compiler to generate a default constructor for us; however, the compiler does not always synthesize a default constructor for such needs. Clearly, the above code does not generate a default constructor for us.

This can be seen through gprof:

So when does the compiler generate a default constructor for us? When the compiler needs it!

In the code for the testClass that we defined above, the actions we expect the compiler to generate (to initialize the two member variables) are not what the compiler requires, but rather what the program requires. Therefore, we cannot rely on the compiler to implement the operations needed by the program; the operations required by the program should be implemented by the programmer rather than relying on the compiler.

So, under what circumstances does the compiler need to generate a default constructor?

There are four situations:

  • Member class objects with a Default Constructor
  • Base Class with a Default Constructor
  • Class with a virtual Function
  • Class with a virtual Base Class

First, let’s test one by one:

Member class objects with a default constructor

We define a string class and add a string member in testClass.

1
2
3
4
5
6
7
8
9
10
11
12
class testClass{
public:
int x;
double y;
string input;
};
class string{
public:
string():p(nullptr),x(0){}
char *p;
int x;
};

Then we compile and run the code we created and output all members, and use gprof to analyze whether the compiler generated a default constructor for us:

We can see that the compiler has automatically generated a default constructor for us, but for the other members of the class, they are still not initialized. The values of the two members in testClass we read remain undefined:

Base class with a default constructor

If a class that has no constructors is derived from a Base Class that has a Default Constructor, the compiler will generate a default constructor for it.

The generated derived class default constructor will call the base class’s default constructor to initialize the members derived from the base class.

1
2
3
4
5
6
7
8
9
10
class Base{
public:
Base():x(0),y(0.0){};
int x;
double y;
};
class Derived:public Base{
public:
int z;
};

Let’s write some code to test whether the compiler will generate a default constructor for Derived:

1
2
3
4
int main(int argc,char* argv[]){
Derived d;
return 0;
}

We compile and use gprof to analyze:

If a class provides a constructor but does not provide a default constructor, the compiler will not generate a default constructor for it, but it will extend each constructor to insert code “to call the default constructor”.

1
2
3
4
5
6
7
8
9
10
11
12
class Base{
public:
Base():x(0),y(0.0){};
int x;
double y;
};
class Derived:public Base{
public:
// Derived has a constructor, but not a default constructor
Derived(int ival):z(ival){}
int z;
};

In this case, the compiler will not generate a default constructor for Derived, but will automatically insert the Base constructor into the Derived constructor to correctly initialize the two members x and y inherited from the Base class.

1
2
3
4
int main(int argc,char* argv[]){
Derived d(10);
return 0;
}

Next, we’ll verify it again through compilation and gprof:

As shown, the Base’s default constructor will still be executed. Our previous prediction was correct.

Classes with Virtual Functions

In the following two situations about classes with Virtual Functions, the compiler will synthesize a default constructor:

  • Class declares (or inherits) a Virtual Function.
  • Class is derived from an inheritance chain that contains one or more Virtual Base Classes.

Regardless of which case, in the absence of user-defined constructors, the compiler will document the necessary information to synthesize a default constructor.

The code is as follows:

1
2
3
4
5
6
7
8
9
10
class Base {
public:
virtual void func() { cout << "A::func()" << endl; }
int x;
};
class Derived :public Base {
public:
virtual void func() { cout << "B::func()" << endl; }
int z;
};

Two expansion behaviors will occur during compilation:

  1. A virtual function table will be generated by the compiler, which holds the addresses of the Class’s virtual functions (function pointer addresses).
  2. Each Class Object will have an additional Pointer Member (the vptr) generated by the compiler, which holds a pointer to the virtual function table.

Using Visual Studio for analysis makes it more intuitive:

We can see that the Derived class object b contains all members (the class does not define static members and non-virtual functions, so we will not discuss static and non-virtual function items here).

The object b contains:

  • A complete object of Base: the data member x and the Base’s virtual function table, which contains a pointer to Derived::func().

  • A Data Member (data member) z defined in b.

If we use the b object to call func:

1
b.func();

The execution of this code correctly relies on the compiler setting the initial value for b’s vptr, placing the appropriate virtual Table address. For every class’s constructors defined, the compiler inserts some necessary code to achieve this behavior.

For those classes that declare no constructors, the compiler will synthesize a default constructor for them to properly initialize the vptr of each class object.

It should be noted that the C++ standard does not require the virtual table to be singular. If we declare another virtual function in Derived and set Base as virtual inheritance, Visual Studio will produce multiple Virtual Function Tables in the inheritance hierarchy; however, other compilers may not do this. Some compilers may merge multiple virtual tables into one when implementing the Virtual Function Table, which must be noted.

Classes with a virtual Base Class

There is considerable variation in the implementation methods of the Virtual Base Table across different compilers. However, a common point of every implementation is that the position of the Virtual base class within each derived class object must be prepared adequately at runtime.

1
2
3
4
5
6
7
8
9
10
11
12
class X{public:int i;};
class A:public virtual X{public: int j;};
class B:public virtual X{public:double d;};
class C:public A,public B{public: int k;};

void foo(A* const pa){pa->include=1024;}

int main(void){
foo(new A);
foo(new C);
//...
}

In the above code, the compiler cannot fix the actual offset of “accessing X::i via pa” within foo() because the true type of pa can change (polymorphism). The compiler must modify the code “to perform the access operation” so that X::i can be resolved at runtime.

The compiler accomplishes this by inserting a pointer into each of the virtual base classes of the Derived class object. Through this pointer, all access operations to a virtual base class via a reference or pointer can be performed through a related pointer.

For example, the above foo() code may be rewritten at runtime as follows:

1
2
3
4
void foo(const A* pa){
// Access class members through an intermediate pointer
pa->_vbcX->i=1024;
}

Inserting pointers into class objects or other tasks performed by the compiler is achieved during the class object construction phase.

For every constructor defined in the class, the compiler will insert code that “allows for virtual base class access operations during execution.” If the class declares no constructors, the compiler must synthesize a default constructor for it.

In the default constructor synthesized by the compiler, only Base class subobjects and Member class objects will be initialized. All other nonstatic data members (like integers, pointers, arrays, etc.) will not be initialized. These initializations are necessary for the program but not required by the compiler; thus, the default constructor generated by the compiler will not initialize them. If the program requires initialization of a nonstatic built-in type data member, the responsibility for providing this operation should lie with the class implementers (programmers) and should not rely on the compiler.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:关于编译器生成默认构造函数的一些误区
Author:LIPENGZHA
Publish Date:2016/05/18 19:38
World Count:6k Words
Link:https://en.imzlp.com/posts/7666/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!