C++ is one of the most popular programming languages which is being used in multiple domains. Probably there are billions of lines of code written in C++ controlling different aspects of our human civilization. Its diverse use means it supports many programming paradigms. One of the most important features of C++ is the support of Object-Oriented Programming, also known as OOP. Most of the textbooks available in C++ do not go into detail about how C++ objects are being managed internally. This has created many misconceptions about many features of C++. One of the most popular misconceptions is about the default constructors. Programmers new to C++ often have two common misunderstandings:
1. That a default constructor is synthesized for every class that does not define one.
2. That the compiler-synthesized default constructor provides explicit default initializers for each data a member declared within the class.
While the above assertions are not accurate, let's try to have a look at what C++ Annotated Reference Manual(ARM) tells us.
default constructors…are generated (by the compiler) where needed...
The crucial word here is the 'needed' and what it could mean? Consider below code snippet where function foo_bar() expects class Foo object bar to be zeroed out before performing some action.
// example 1
class Foo
{
public:
int val;
Foo *pnext;
};
void foo_bar()
{
// program needs bar's members zeroed out
Foo bar;
if ( bar.val || bar.pnext )
; // ... perform some action
// ... some other statements
}
In the above example, class Foo does not have any user-defined constructor and in order to foo_bar() to work correctly, it expects a default constructor which will zero out Foo's member variables. If we try to run the above program then it comes out Foo's object is not zeroed out[1]. Clearly above program does not full fil ARM's requirement for generating default constructor. From the above code snippet, we need to understand the difference between the needs of the program and the needs of the implementation.
In the above example, the program's need for a default constructor is the responsibility of the programmer i.e. designer of class Foo. A default constructor is not synthesized for the above code fragment. So when is a default constructor is synthesized? It comes out only when implementation needs it. Moreover, the synthesized constructor performs only those activities which are required by the implementation. That is, even if there were a need to synthesize the default constructor for class Foo, that constructor would not include code to zero out two data members int val and Foo* pnext. The standard has refined the discussion in ARM, although the behavior remains the same in practice. The standard states the following:
If there is no user-declared constructor for class X, a default constructor is implicitly declared… A constructor is trivial if it is an implicitly declared default constructor...
The standard goes on further to state the conditions under which the implicit default constructor is considered trivial. A nontrivial default constructor is one that ARM's terminology is needed by the implementation and if necessary, is automatically synthesized by the compiler. Let's try to understand more about this with the help of some examples.
Member Class Object with Default Constructor
If a class is without any constructor and it contains a member object of a class with the default constructor, the implicit default constructor of the class is considered as non-trivial and in such case, a default constructor is synthesized by the compiler for containing class. The condition for synthesizing the constructor only takes place if the constructor actually needs to be invoked. Let's take a look at the below example.
// example 2
class Foo
{
public:
Foo();
Foo( int )
//...
};
class Bar
{
public:
Foo foo;
char *str;
};
void foo_bar()
{
Bar bar; // Bar::foo must be initialized here
if ( str )
{
; // some operation
}
// some operation
}
The synthesized default constructor contains the code necessary to invoke class Foo's default constructor on the member object Bar::foo. But it does not generate any code to initialize Bar::str. Hence initialization of Bar::foo becomes the responsibility of the compiler but initialization of Bar::str is still with the programer.
The default constructor synthesized by the compiler might look something like this.
// Pseudo C++ Code
// possible synthesis of Bar default constructor
inline Bar::Bar()
{
foo.Foo::Foo();
}
Note that the synthesized constructor of Bar::Bar() does not initialize Bar::str i.e. synthesized constructor meets only implementation needs and not programmers needs. In order to run example 2 correctly. character pointer Bar::str should be initialized by the programmer.
Now let's consider the scenario, whereas a programmer we've defined the default constructor for the class Bar as below.
// programmer defined default constructor
Bar::Bar()
{
str = 0;
}
From the above example, we could see that programmer's needs are fulfilled by the user-defined default constructor but implementation needs are not. In such a scenario, the compiler will augment the use defined default constructor such that implementation needs are satisfied.
// Augmented default constructor
// Pseudo C++ Code
Bar::Bar()
{
foo.Foo::Foo(); // augmented compiler code
str = 0; // explicit user code
}
In cases of multiple class member objects requiring constructor initialization, the language requires that the constructors be invoked in order of their member declaration within the class. Similar to the above example, the compiler inserts the appropriate code within each constructor, invoking the associated default constructor of each member in their order to member declaration, prior to user-supplied code.
Base Class with Default Constructor
If a class is derived from a base class that has a default constructor but the derived class does not have a default constructor then the derived class is considered non-trivial and the compiler synthesizes the default constructor. In such cases synthesized default constructor of the derived class invokes the default constructor of the immediate base class, int the order of their declaration.
In the case where the derived class does not have a default constructor but has multiple user-defined constructors, then in such cases, user-defined constructors are augmented with code to invoke all required default constructors of the base class by the compiler. However, the compiler will not synthesize the default constructor in the derived class due to the presence of a user-defined constructor.
Summary
From the above simple examples, we've tried to understand when the compiler synthesized the default constructor for a given class. We've not explored all the possible cases when the compiler will synthesis the default constructor. But I hope this article will help to remove two misconceptions that I've mentioned at the start of the article and avoid the pitfalls of such wrong assumptions in production code.
[1] Global objects are guaranteed to have their associated memory "zeroed out" at program start-up. Local objects allocated on the program stack and heap objects allocated on the free-store do not have their associated memory zeroed out; rather, the memory retains the arbitrary bit pattern of its previous use.
Comments