Sunday, January 22, 2012

C++ class internals - part 2/2

C++ class internals - part 1/2 analyzes the class structure internals, and how member functions and access specifiers work internally. In this part, we'll cover the internals of constructors and destructors in detail.

1. The constructor:

Now that we've covered how a normal C++ member function works, we'll try to dig a bit deeper into the C++ constructor. Let's consider the following code:

#include <iostream>
using namespace std;

class A {
   public:
       A() { a_ = 100; }
       int a_;
       void setA( int a ) { a_ = a; }
       int getA( void ) { return a_; }
};

int main() {
   A a;
   a.setA( 10 );
   cout << a.getA() << endl;

   return 0;
}

Looking into the above piece of code, one question strikes us immediately. If the constructor is similar to a member function, then does it have an extra "this" pointer too ? The answer is "yes". Whenever people encounter a constructor, they feel it's pretty complex and special. It's special yes, because it does a few extra stuff. But, it's not complex. As I've already mentioned, every function in C++ is like a normal function in 'C'. So, with that idea, let's go in.

Using the same technique explained in part 1 ( 3. A C++ member function call ) we can pretty much confirm that "some_value" which is the "this" pointer, gets passed to the constructor too. So, if you write a constructor with no arguments, say A(), then it will have actually have 1 arg A(this), if the constructor is say,  A( int, float ), then it will actually have 3 args A( this, int, float ).

People often say that constructor initializes object. If that's the case how can the "this" pointer be available to the constructor. I mean "this" pointer is valid only, when it denotes a valid object right ? And unless the constructor is fully executed, the object cannot be valid and obviously the "this" pointer cannot be valid. So, what's the truth here ? Let's find out. Consider the following C++ code sample.

class A {
private:
  int a_;
public:
  A() : a_(10) {
    a_ = 20;
  }
};

class B : public A {
private:
  int b_;
public:
  B() : b_(30) {
    b_ = 40;
  }
};

int main() {
  B b;

  return 0;
}

The following is the assembly dump of the main() function:

(gdb) disassemble main
Dump of assembler code for function main:
0x00001f10 <main+00>:    push   %ebp
0x00001f11 <main+01>:    mov    %esp,%ebp
0x00001f13 <main+03>:    sub    $0x18,%esp
0x00001f16 <main+06>:    lea    -0x10(%ebp),%eax
0x00001f19 <main+09>:    mov    %eax,(%esp)
0x00001f1c <main+12>:    call   0x1f90 <dyld_stub__ZN1BC1Ev>
0x00001f21 <main+17>:    movl   $0x0,-0x8(%ebp)
0x00001f28 <main+24>:    mov    -0x8(%ebp),%eax
0x00001f2b <main+27>:    mov    %eax,-0x4(%ebp)
0x00001f2e <main+30>:    mov    -0x4(%ebp),%eax
0x00001f31 <main+33>:    add    $0x18,%esp
0x00001f34 <main+36>:    pop    %ebp
0x00001f35 <main+37>:    ret   
End of assembler dump.


There is one thing to note here. There is just one "call" instruction, which implies a single function call. It should now be easy to figure out which function it is. Yes it is B::B() constructor. This can be seen by following the dyld_stub__ZN1BC1Ev function.

(gdb) disassemble dyld_stub__ZN1BC1Ev
Dump of assembler code for function dyld_stub__ZN1BC1Ev:
0x00001f90 <dyld_stub__ZN1BC1Ev+0>:    jmp    *0x2020
End of assembler dump.

(gdb) disassemble *0x2020
Dump of assembler code for function B::B():
0x00001f5a <B::B()+00>:    push   %ebp
0x00001f5b <B::B()+01>:    mov    %esp,%ebp
0x00001f5d <B::B()+03>:    sub    $0x8,%esp
0x00001f60 <B::B()+06>:    mov    0x8(%ebp),%eax
0x00001f63 <B::B()+09>:    mov    %eax,-0x4(%ebp)
0x00001f66 <B::B()+12>:    mov    -0x4(%ebp),%eax
0x00001f69 <B::B()+15>:    mov    %eax,(%esp)
0x00001f6c <B::B()+18>:    call   0x1f8a <dyld_stub__ZN1AC2Ev>
0x00001f71 <B::B()+23>:    mov    -0x4(%ebp),%eax
0x00001f74 <B::B()+26>:    movl   $0x1e,0x4(%eax)    <=== b_=30
0x00001f7b <B::B()+33>:    mov    -0x4(%ebp),%eax
0x00001f7e <B::B()+36>:    movl   $0x28,0x4(%eax)
    <=== b_=40
0x00001f85 <B::B()+43>:    add    $0x8,%esp
0x00001f88 <B::B()+46>:    pop    %ebp
0x00001f89 <B::B()+47>:    ret   
End of assembler dump.


So, we are now in B's constructor. There are few things worthy to note here. B's constructor calls another function. If we follow dyld_stub__ZN1AC2Ev we see that A's constructor is getting called.

(gdb) disassemble dyld_stub__ZN1AC2Ev
Dump of assembler code for function dyld_stub__ZN1AC2Ev:
0x00001f8a <dyld_stub__ZN1AC2Ev+0>:    jmp    *0x201c
End of assembler dump.

(gdb) disassemble *0x201c
Dump of assembler code for function A::A():
0x00001f36 <A::A()+00>:    push   %ebp
0x00001f37 <A::A()+01>:    mov    %esp,%ebp
0x00001f39 <A::A()+03>:    sub    $0x4,%esp
0x00001f3c <A::A()+06>:    mov    0x8(%ebp),%eax
0x00001f3f <A::A()+09>:    mov    %eax,-0x4(%ebp)
0x00001f42 <A::A()+12>:    mov    -0x4(%ebp),%eax
0x00001f45 <A::A()+15>:    movl   $0xa,(%eax)
    <=== a_=10
0x00001f4b <A::A()+21>:    mov    -0x4(%ebp),%eax
0x00001f4e <A::A()+24>:    movl   $0x14,(%eax)
   <=== a_=20
0x00001f54 <A::A()+30>:    add    $0x4,%esp
0x00001f57 <A::A()+33>:    pop    %ebp
0x00001f58 <A::A()+34>:    ret   
0x00001f59 <A::A()+35>:    nop   
End of assembler dump.

 


Viola !!! Yes, compiler kind of "inserts" these calls in our constructor. When someone say's that a derived class constructor calls the base class constructor, the compiler inserts these direct / explicit function calls under the hood. There is no black magic here. This is one function calling another function. That's it !!

The next thing to note here is that the order in which a_ and b_ are assigned values in our program ( highlighted lines ). The order is 

1. a_ = 10
2. a_ = 20
3. b_ = 30
4. b_ = 40

The order can also be written as:

1. Base class initialization list
2. Base class constructor code within braces {}
3. Derived class initialization list
4. Derived class constructor code within braces {}

This tells us one important conclusion. There is code which gets executed in a constructor, even before the code within it's braces is executed. Yes, the initialization list of the constructor !!! If you're staring at what is the difference between a_(10) and the a_ = 20 in A's constructor, it's huge. It's not very obvious here, since we're dealing with a primitive data type int here. In fact, if a_ is an object of another class say X, then here is the difference

a_(10) calls  X::X(10)            which is a constructor
a_=20  calls X& X::operator=(int) which is assignment operator function.

This practically implies that any code within the constructors' braces {} is likely to call only the assignment operator. I mean whatever you do within the braces might impact the object, but after the object has been created ( much of which happens in the initialization lists ). So, it's pretty safe to use the "this" pointer within the constructor's braces {}. That's a 100% guarantee :-)

This gives a high level picture of how a constructor works. Next, we'll explore a bit more on the default constructor.

2. The default constructor:

There is a common belief that the constructor which is automatically generated by the compiler is the default constructor. That's not exactly true. The accurate answer is: A constructor which can be called with no arguments is the default constructor.  The compiler generates a default constructor with no arguments, if the class doesn't have a constructor. Consider the following class:

class A {
  // Implicit Default constructor
private:
  int a_;
};

class B : public A {
public:
  // Explicit Default constructor
  B() {
    b_ = 10;
  }
private:
  int b_;
};

class C : public B {
public:
  // Explicit Default constructor with default arguments.
  C( int c = 10 ) {
    c_ = c;
  }
private:
  int c_;
};

class D : public C {
public:
  // Non default constructor
  D( int d ) {
    d_ = d;
  }
private:
  int d_;
};

int main() {
  A a;
  B b;
  C c;
  D d(10);

  return 0;
}


As noted in the above comments, classes A, B and C have default constructors, but not class D. In short, if you can construct an object with just a plain declaration like "A a;" then "class A" has a default constructor. If an explicit argument is a minimum requirement during an object declaration as in "D d(10)", then "Class D", doesn't have a default constructor. "Class D" is said to have a non-default constructor.

Okay .. that should have been in a lot of text books. But, why all this fuzz about default and non-default constructor ? I mean, they are both constructors right ? Why bother if they have 1 argument or no argument ? What difference does it make ?

We can pretty much say, that "default constructors" are compiler's favorite, because compiler's can do a lot of things automatically with default constructors without expecting much from the user. And a class can have only 1 default constructor + any number of non-default constructors. So, when there is only 1 default constructor, there is no confusion. It's all simple for the compiler. Compiler will do a lot of jobs while simultaneously sipping a cup of green tea :-) !!

So, what's the advantage for a programmer ? It saves some effort and time. Consider the following code sample:


class RandomClass {
private:
  int a[100];
};


class A {
  // Implicit Default constructor
private:
  RandomClass r_;
  int a_;
};

class B : public A {
public:
  // Explicit Default constructor
  B() {
    b_ = 10;
  }
private:
  RandomClass r_;
  int b_;
};

class C : public RandomClass {
public:
  // Explicit Default constructor with default arguments.
  C( int c = 10 ) {
    c_ = c;
  }
private:
  int c_;
};

int main() {
  A a;
  B b;
  C c;

  return 0;
}


Here, we've added a new class RandomClass. This class doesn't have any default constructor. The compiler has generated one.  When objects "a" and "b" are created, their members "r_" which is an object of RandomClass is being constructed automatically. The compiler calls the default constructor of RandomClass to initialize it. Class C inherits from RandomClass. So, when object "c" is created, the base class object is constructed automatically. The compiler calls the default constructor of a base class ( RandomClass ) object to initialize it.

If you noticed the previous paragraph, one thing is pretty obvious: Compiler does things automatically when there is a default constructor.  Compiler has it's base on one principle. Every object however big, is always constructed from simple primitive data types. Since compiler knows how to handle primitive data types, it can pretty much handle any object type. That is why it can always  ... I mean always come up with a default constructor ... now matter how complex your class is.

One frequent question here is, since compiler knows how to operate on primitive data type, does it imply that it initializes them to their default values ( 0 for int, 0.0 for double etc ). I mean it feels really good and clean having an integer variable initialized to "0", rather than having some garbage like "-2394959595". Well .. the compiler can do that .. Nothing stops it from doing it ... But, there is one big problem here. When talking about initialization, we should be aware that most initializations happen at run-time. What that implies is that we're not talking about writing "0"'s to binary executable while compiling. Initialization at run-time means we need assembly instructions ( like mov 0, <some_register> or  mov 0, <some_memory_location> ) sprinkled all over the code, which need to be executed while the program executes. Now, do you see the problem. That's a huge run-time overhead. So, the compiler only makes sure that it sets aside memory for the primitive types. Nothing more !

We just covered on what is a default constructor and how it can help the compiler do stuff automatically. Next, we'll explore non-default constructors. 
  
3. Initialization lists:

Default constructors should serve the purpose for most cases. But in cases, where we might want to explicitly specify the arguments while creating the object, we can go for non-default constructors. As mentioned in the last section, the compiler can do a lot of things for you, if there is a default constructor, which implies that it can't do a lot of things when there are no default constructors.

Imagine this simple use case. There is 1 base class ( B ), and 10 derived classes ( D1 to D10 ). So "D1 inherits from B", "D2 inherits from D1" and "D10 inherits from D9" and so on. We've already said, that a class can have any number of non-default constructors. So, let's say that all classes ( B, D1, D2 ... D10 ) have 5 non-default constructors each ( say ranging from 1 integer arg to 5 integer args ). For eg. let's consider the following for class D10.

D10( int a ) {}
D10( int a, int b ) {}
D10( int a, int b, int c ) {}
D10( int a, int b, int c, int d ) {}
D10( int a, int b, int c, int d, int e ) {}

Now imagine what happens when the compiler encounters the following line ?


D10 d10;

The compiler is a bit tensed now. The green tea can wait !!

The compiler will probably spit out a lot of error messages. The first problem here should be obvious. d10 should be called with some arguments, so that it matches one of the above constructors. But, that will only take us successfully to the appropriate constructor for D10. The compiler doesn't know which "D9" constructor to call !!

Initialization lists comes to our rescue. In initialization lists, we can specify which base class constructor to call. Simple. Which leads to one simple truth. Wherever you have only non-default constructors, it's up to the user's responsibility to call them. The compiler will bail out otherwise. So, the following will make the compiler happy.

...
D10( int a, int b ) : D9( a, b ) {}
D10 d10( 10, 20 )

We're not done yet. Only 1 half of the problem is solved. Consider the case, when class D10 has a member object, say D11 d11_; which doesn't have a default constructor either ? The answer is the same. Specify them explicitly in the initialization lists. That's it. Initialization lists are there to serve this purpose only. Always remember initialization lists and constructors go hand in hand.

Q:   ok ... still one fundamental question is still unanswered. Why do we need special syntax and concept of initialization lists ?

A: As already mentioned, pretty much whatever you try to do within the constructor's braces will lead you only to the assignment operator, not the constructor. So, we need a "different place" to initialize things and it should not be within the braces of a constructor. Clearly, this "different place" cannot be at the class level, since each constructor has it's own freedom of how to initialize it's member variables and base objects. So, this "different place" has to be at the constructor level. So, even a syntax like this should be fine:


class C : public A {
public:
  C( int a, int b ) {
    // Initialization
    [
     A(a);   // base class initialization
     B_(b);  // member variable initialization
    ]

    // Assignment
    b_ = b;
  }
private:
  B b_;
};

This is not valid C++ syntax. But, it achieves what we want to do. Some syntax to differentiate between initialization ( within square brackets[] ) and assignment. That's the key here. The original C++ syntax for this is:


class C : public A {
public:
  C( int a, int b ) : A(a), B_(b) {
    b_ = b;
  }
private:
  B b_;
};


Never mind the syntax. Syntax are for parsers. Conceptually both are fine. That's all to say about constructors. We'll move to destructors now.


4. The destructor:


This is pretty simple than the constructor. Building an object might take a lot of effort, but destroying always follows a single rule.  

You just destroy whatever you built. There is nothing to think !

For this reason, they are small and cute in the context of a class. There are a few small points to observe here, though. Consider the following class:


class A {
private:
  int a_;
public:
  A() {  a_ = 0xa;  }
  ~A() { a_ = 0xaa; }
};

class B : public A {
private:
  int b_;
public:
  B() {  b_ = 0xb;  }
  ~B() { b_ = 0xbb; }
};

int main() {
  B b;

  return 0;
}


The thing to watch out for is the order of destruction. The destructor of the derived class is called before the base class destructor. Now, that we're experts, this should be pretty easy to decode in the assembly code. Look for the bold lines alone:


(gdb) disassemble main
Dump of assembler code for function main:
0x00001ec0 <main+00>:    push   %ebp
0x00001ec1 <main+01>:    mov    %esp,%ebp
0x00001ec3 <main+03>:    sub    $0x18,%esp
0x00001ec6 <main+06>:    lea    -0x10(%ebp),%eax
0x00001ec9 <main+09>:    mov    %eax,(%esp)
0x00001ecc <main+12>:    mov    %eax,-0x14(%ebp)
0x00001ecf <main+15>:    call   0x1f80 <dyld_stub__ZN1BC1Ev>
0x00001ed4 <main+20>:    movl   $0x0,-0x8(%ebp)
0x00001edb <main+27>:    mov    -0x14(%ebp),%eax
0x00001ede <main+30>:    mov    %eax,(%esp)
0x00001ee1 <main+33>:    call   0x1f86 <dyld_stub__ZN1BD1Ev>
0x00001ee6 <main+38>:    mov    -0x8(%ebp),%eax
0x00001ee9 <main+41>:    mov    %eax,-0x4(%ebp)
0x00001eec <main+44>:    mov    -0x4(%ebp),%eax
0x00001eef <main+47>:    add    $0x18,%esp
0x00001ef2 <main+50>:    pop    %ebp
0x00001ef3 <main+51>:    ret   
End of assembler dump.
 

(gdb) disassemble 0x1f86
Dump of assembler code for function dyld_stub__ZN1BD1Ev:
0x00001f86 <dyld_stub__ZN1BD1Ev+0>:    jmp    *0x2028
End of assembler dump.


(gdb) disassemble *0x2028
Dump of assembler code for function B::~B():
0x00001f4e <B::~B()+00>:    push   %ebp
0x00001f4f <B::~B()+01>:    mov    %esp,%ebp
0x00001f51 <B::~B()+03>:    sub    $0x8,%esp
0x00001f54 <B::~B()+06>:    mov    0x8(%ebp),%eax
0x00001f57 <B::~B()+09>:    mov    %eax,-0x4(%ebp)
0x00001f5a <B::~B()+12>:    mov    -0x4(%ebp),%eax
0x00001f5d <B::~B()+15>:    movl   $0xbb,0x4(%eax)
0x00001f64 <B::~B()+22>:    mov    -0x4(%ebp),%eax
0x00001f67 <B::~B()+25>:    mov    %eax,(%esp)
0x00001f6a <B::~B()+28>:    call   0x1f7a <dyld_stub__ZN1AD2Ev>
0x00001f6f <B::~B()+33>:    add    $0x8,%esp
0x00001f72 <B::~B()+36>:    pop    %ebp
0x00001f73 <B::~B()+37>:    ret  
End of assembler dump.
 

(gdb) disassemble 0x1f7a
Dump of assembler code for function dyld_stub__ZN1AD2Ev:
0x00001f7a <dyld_stub__ZN1AD2Ev+0>:    jmp    *0x2020
End of assembler dump.
 

(gdb) disassemble *0x2020
Dump of assembler code for function A::~A():
0x00001f0e <A::~A()+00>:    push   %ebp
0x00001f0f <A::~A()+01>:    mov    %esp,%ebp
0x00001f11 <A::~A()+03>:    sub    $0x4,%esp
0x00001f14 <A::~A()+06>:    mov    0x8(%ebp),%eax
0x00001f17 <A::~A()+09>:    mov    %eax,-0x4(%ebp)
0x00001f1a <A::~A()+12>:    mov    -0x4(%ebp),%eax
0x00001f1d <A::~A()+15>:    movl   $0xaa,(%eax)
0x00001f23 <A::~A()+21>:    add    $0x4,%esp
0x00001f26 <A::~A()+24>:    pop    %ebp
0x00001f27 <A::~A()+25>:    ret  
End of assembler dump.


Here, if you trace the code sequence, b_ = 0xbb is executed before a_ = 0xaa. So, the ordering depends upon where the compiler places the destructor function calls. Pretty similar to a constructor, except the compiler inverts the order.

Destructors gain significance in the context of exceptions. When exceptions are thrown, all function stacks unwind until the stack which has a 'catch' block to handle the exception. So, whenever stacks unwind, the objects in it go out of scope, which triggers a destructor call. Hence, objects in the intermediate stack of an exception are automatically destroyed "cleanly", when an exception is thrown. ( Actually, the exception handling mechanism is a bit more tricky that this. This is covered in the C++ exception internals post. )

Destructors should not throw an exception in case of a failure. Reason being the stack unwind process explained above. Exception triggers more destructors to be called in the unwinding stacks, and destructors might in-turn throw exceptions. The system can only process 1 exception at a time. So, due to this technical limitation, it's not recommended to throw an exception in a destructor. Logging is a recommended solution.

Destructors cannot be overloaded. The point behind that is you don't need to decorate your demolition bombs. Different types of overloaded constructors can be used to construct a C++ object, but finally what you have is a single C++ object, and a single destructor is sufficient to destroy a single object.

Destructors can be manually invoked using the "->" operator, if you have the object address ( as in, aPtr->~A() ). This is mostly useful in placement new case. If you're using placement new, the recommended way of destroying the object is to call the destructor explicitly. This will be covered in "C++ memory internals" post. Point is, it is possible to invoke a destructor, but this is automatically done by the compiler in most cases, and by the "delete" keyword in the rest. So, you won't need it mostly.

Destructors should be declared virtual, if you're class is a base class to other classes. This is to make sure that when you delete a pointer to a base class ( as in delete b ),  it forces the entire destructor chain in the inheritance hierarchy to be called. You never know, who'll inherit from you. So, unless you're sure that no one is going to inherit from you, or no one is allowed to inherit from you, it's safe to mark the destructor virtual.

1 comment:

  1. Dude... Great Job... Really appreciate ... This is exactly what I was looking for....

    Many thanks and keep coming the good explanations like this :)

    ReplyDelete