Let's briefly discuss how C++ implements late binding to
support the existence of virtual functions.
Virtual functions introduce a small overhead in the size of your class,
which could be important if your project relies on using them.
I took a few steps back and returned the definition of the play()
function from the base class before we made it pure.
I think that this will make it easier to explain how virtual functions work.
When you mark some function as virtual,
program will create a virtual table in memory for the
class that contains that virtual function.
This virtual table is nothing more than a simple array of pointers,
specifically, pointers to functions.
Each position inside of that array is dedicated to a virtual function,
but since we only have one, this table is small,
it only has one element.
If we had more virtual functions,
each one of them would have a special position in this table.
Since the right classes could override this virtual
function from the base class, they also get their own virtual table.
We know that each of these classes have their own
version of the play() function,
and the instructions for executing these functions are
also stored in a special part of memory.
These function pointers from the virtual table should point
to the appropriate function in memory.
Since every class in this example has its own function override,
these pointers point to the function defined inside of that class.
So let's say that I want to call the play()
function from the Instrument pointer, which actually points to the Guitar object.
Before executing the function, C++ will check the virtual table of the object,
which in this case, is the object of the guitar class.
Compiler knows that the position of this function pointer is 0,
so it will use this function pointer to find the correct function in memory.
Once it finds the correct function,
it will bind it to the current statement and call this function from memory.
This is why we call it late binding because the function
is bound at the time of the execution.
But what if I decide to remove this overridden function from the
Synth derived class. The virtual table from this class will see that
we don't have the appropriate override, so by default,
this function pointer will point to the function defined in the base class,
the Instrument class.
And now when we try to call this play() function on the
Synth object through the Instrument pointer,
the virtual table will be checked again,
and this pointer will tell the program to execute the
function from the Instrument class.
So these virtual tables will help the program to find the
correct virtual function in memory.
And we don't have to manage these tables,
they are managed by the program itself.
The overhead that I was talking about is the existence of the virtual pointer.
Whenever you define a virtual function,
that base class and all of its derived classes need
to store another hidden member, which is known as the virtual pointer.
We don't manage this pointer,
but we have to be aware that it increases the size of the overall class.
The only purpose of this hidden pointer is to point to the
virtual table that belongs to that class.
This is how a program gets access to the correct virtual table.
And this pointer is inherited just like any other member of the base class,
so all of the derived classes also have it.
So let's say again that I'm calling the play()
function from the Instrument pointer, which points to the Guitar object.
This pointer is only aware of the inherited part of this Guitar object.
As far as this pointer is concerned,
this object is actually an instrument and it only has these two members,
the Boolean value and the virtual pointer,
because the number of strings is not inherited.
But that's great because the Instrument pointer is still
aware of this virtual pointer member,
and since this pointer belongs to the Guitar object,
that means that it is pointing to the Guitar's virtual table.
And since the Guitar's virtual table has the correct function pointer,
the overridden function will be used.
That's a good idea, right?
But as I already mentioned,
the usage of virtual functions will cause an overhead in size because we
need to store this virtual pointer in every object.
To show you this, I will print out the size of every class.
Before virtual functions, the Instrument needed only one byte of data.
And now you can see that all of these objects suddenly need 16 bytes of memory.
Since the virtual pointer is just a pointer,
each class would need an additional 8 bytes of memory,
at least on my 64‑bit computer.
So if the Instrument class only required 1 byte,
shouldn't the size now be 9 bytes?
Yes,
but the reason for this big increase is the occurrence of
something known as structure padding.
As I mentioned,
all of the object members have to be stored next to
each other in one block of memory.
But the total size of this block will depend on the alignment of data.
In essence, CPU works by processing data in cycles,
and a so‑called word length determines the maximum amount of
bytes that it can process in each cycle.
On 32‑bit machines, this word length is 4 bytes,
and on 64‑bit machines, like mine, it's 8 bytes.
This makes sense because if it was only 1 byte,
then we would need 4 cycles to process 1 integer,
but now we can process 2 of them in only 1 cycle.
To achieve better performance,
the data from an object is aligned by the computer.
If we just put members next to each other,
Instrument class would only need 9 bytes,
but this virtual pointer would be split in two parts.
CPU prefers to read whole values in one cycle.
To make this happen,
the program will move this pointer into the next block of 8 bytes
and add 7 padding bytes next to the Boolean.
Now we can read the Boolean in one cycle and the whole pointer in the next one.
And the same goes for the derived classes.
Their virtual pointer is also moved to the next cycle,
but the Boolean and the integer member are both available in the first cycle.
If you remove the virtual functions and check the
size of the Guitar or Synth class,
you might be surprised that it's 8 bytes instead of 5.
This is because the processor also likes to have data aligned in a way that
every data type starts at an increment of its own size.
In this case,
an integer of 4 bytes should either start at the beginning of an
8‑byte sequence or at least at the middle.
So the computer adds 3 padding bytes to make this happen.
I should note that this is not a language feature.
Padding is implemented by the compiler,
and just because it works like this on my computer,
that doesn't mean that yours will do the same.
The point of this little digression was to show you how this virtual
pointer can possibly increase the size of your objects,
in this case, significantly.
So don't use virtual functions just because the cool kids
are doing it; approach them wisely.