Page 1 of 1

Behind the scenes - How C++ Classes Work : Part III Rate Topic: -----

#1 Martyn.Rae  Icon User is offline

  • The programming dinosaur
  • member icon

Reputation: 540
  • View blog
  • Posts: 1,406
  • Joined: 22-August 09

Posted 27 March 2011 - 09:14 AM

*
POPULAR

Behind the scenes - How C++ Classes Work : Part III

Introduction

In the previous two tutorials, we have concerned ourselves with the implementation of C++ classes and inheritance. This tutorial progresses onto how the C++ compiler handles classes with pure virtual functions. Again, we will be looking at this by using pure C code to achieve the equivalent results.

Virtual and Pure Virtual methods

We shall begin by modifying the C++ example from the previous tutorial by introducing three functions, one normal method one virtual and one pure virtual method to the base class foo. Also, notice that the foo destructor declaration has been changed to being a virtual destructor. The reason for this will be discussed later.

#include <Windows.h>

class foo {
    public:
        int    m_counter;
        float  m_value;
        char * m_buffer;
    public:
	foo(int counter, float value, int length); 
	virtual ~foo();
        void set_counter(int counter);
        virtual void set_value(float value);
        virtual void set_buffer(char * buffer) = 0;
};

foo::foo(int counter, float value, int length) 
        : m_counter(counter), m_value(value), m_buffer(new char[length]) { 
};

foo::~foo() {
        delete m_buffer;
}

void foo::set_counter(int counter) {
	m_counter = counter;
}

void foo::set_value(float value) {
	m_value = value;
}

class fooed : public foo {
    public:
        double m_double;
    public:
        fooed(int counter, float value, int length, double big_value);
        ~fooed();
        void set_counter(int counter);
        void set_value(float value);
        void set_buffer(char * buffer);
};

fooed::fooed(int counter, float value, int length, double big_value) 
      : foo(counter, value, length), m_double(big_value) {
}

fooed::~fooed() {
}

void fooed::set_counter(int counter) {
	m_counter = counter + 10;
}

void fooed::set_value(float value) {
	m_value = value + 10.0f;
}

void fooed::set_buffer(char * buffer) {
	strcpy(m_buffer, buffer);
}

int main(int argc, char **argv) {
	foo * a = new fooed(10, 3.1f, 256, 20.0);
	a->set_counter(1);
	dynamic_cast<fooed *>(a)->set_counter(20);
	a->set_value(100.0f);
	a->set_buffer("Hello");
	delete a;
}



Referring to the main function, we create a new fooed object. This line of code (as we saw in the previous tutorial) allocates the twenty bytes needed by the class for its data members. This is the eight bytes for the fooed member m_double, and the twelve bytes for the derived foo data members. The fooed constructor is then called which in turn calls the foo constructor.

Notice that we have allocated the pointer to the new fooed object to a foo object - it's base class. This has been done to show off the virtual method set_value.

The next line calls the set_counter method of foo. Now, even though the fooed object also has a set_value method and it was an object of the fooed class that we created, the compiler generates code based on the current pointer type which is foo. We would need to dynamically upcast this pointer to a type fooed before we would call the fooed class set_value method. (This has been included as the next line of the main code).

The next line calls the set_value method. Now, here we have a different kettle of fish altogether. This method was defined in the foo class declaration as being a virtual function. This means that even though we are using a pointer to the base class object foo, it is the fooed class set_value method that is called. We will look at this in a little more detail in a moment.

The next line calls the set_buffer function which is defined as a pure virtual function in the foo class. This tells the compiler that the base class does not implement this method but any derived class MUST supply the method.

Finally, a call is made to the delete operator to delete the fooed object. Because the object is a pointer to the base class, not a fooed object pointer, if the destructor was not defined as virtual, the fooed destructor would never be called. In our example, this would not matter but if fooed had dynamically allocated space then that would cause a memory leak.

The vtable (__vfptr) pointer

Any method defined in a class as being virtual (it does not have to be pure virtual), is added to a table held by the compiler for the class. An invisible pointer to this table is then added as the first data member variable, so in our example above, the size of foo is not twelve, but sixteen bytes in length, and fooed is twenty-four bytes in length. Our foo class would have three entries in this vtable. The first entry would be a pointer to the destructor for this class, the second entry would be a pointer to the set_value function and the third pointer would be a pointer to the set_buffer method.

The compiler sets the vtable pointer based on the object being created through the new operator. This is performed immediately after the memory for the data members has been allocated and before the call to the constructor is called.

Here is the equivalent pure C code.

#include <Windows.h>

typedef void (* DESTRUCTOR)(void * this);
typedef void (* SET_VALUE)(void * this, float value);
typedef void (* SET_BUFFER)(void * this, char *buffer);

typedef struct {
	DESTRUCTOR destructor;
	SET_VALUE set_value;
	SET_BUFFER set_buffer;
} VTABLE;

typedef struct {
        VTABLE * m_vfptr;
        int      m_counter;
        float    m_value;
        char *   m_buffer;
} foo;

void foo_constructor(foo * this, int counter, float value, int length) {
	this->m_counter = counter;
	this->m_value = value;
	this->m_buffer = (char *)malloc(length);

}

void foo_destructor(foo * this) {
	free(this->m_buffer);
}

void foo_set_counter(foo * this, int counter) {
	this->m_counter = counter;
}

typedef struct {
        foo      m_foopart;
        double   m_double;
} fooed;

void fooed_constructor(fooed * this, int counter, float value, int length, double big_value) {
	foo_constructor((foo *)this, counter, value, length);
        this->m_double = big_value;
}

void fooed_destructor(fooed * this) {
	foo_destructor((foo *)this);
}

void fooed_set_counter(fooed * this, int counter) {
	((foo *)this)->m_counter = counter + 10;
}

void fooed_set_value(fooed * this, float value) {
	((foo *)this)->m_value = value + 10.0f;
}

void fooed_set_buffer(fooed * this, char * buffer) {
	strcpy(((foo *)this)->m_buffer, buffer);
}

VTABLE fooed_vtable = { fooed_destructor, fooed_set_value, fooed_set_buffer };

int main(int argc, char **argv) {
	foo * a = (foo *)malloc(sizeof(fooed));
	a->m_vfptr = &fooed_vtable;
	fooed_constructor((fooed *)a, 10, 3.1f, 256, 20.0);
	foo_set_counter(a, 1);
	fooed_set_counter((fooed *)a, 20);
	a->m_vfptr->set_value((fooed *)a, 100.0f);
	a->m_vfptr->set_buffer((fooed *)a, "Hello");
	a->m_vfptr->destructor(a);
	free(a);
}



Conclusion

As you can see from the pure C sample above, the implementation of a simple derived class with virtual functions is messy to say the least. The C++ compiler removes a considerable amount of this complexity and thus reduces the possibility of potential errors.

Hopefully, you have understood these tutorials and now have a better insight into what goes on behind the scenes when programming C++ classes in the future.

FOOTNOTE: You need to be very careful when upcasting C++ object pointers, because it is very easy to upcast the object of a class to some derived object class when the actual object created was not of that derived type. Let's say we have a base object A, an object B that derives from A, and an object C that derives from A. Given an object pointer of class A created as an object B, it would be wrong to assume that you can upcast that pointer to a class C object pointer.

Is This A Good Question/Topic? 12
  • +

Page 1 of 1