A class declaring or inheriting at least one virtual function contains a virtual function table (or vtable, for short). Such a class is said to be a polymorphic class. An object of a polymorphic class type contains a special data member (a "vtable pointer") which points to the vtable of this class. This pointer is an implementation detail and cannot be accessed directly by the programmer (at least not without resorting to some low-level trick). In this post, I will assume the reader is familiar with vtables on at least a basic level (for the uninitiated, here is a good place to learn about this topic).
I hope you learned that when you wish to make use of polymorphism, you need to access objects of derived types through pointers or references to a base type. For example, consider the code below:
#include <iostream> struct Fruit { virtual const char* name() const { return "Fruit"; } }; struct Apple: public Fruit { virtual const char* name() const override { return "Apple"; } }; struct Banana: public Fruit { virtual const char* name() const override { return "Banana"; } }; void analyze_fruit(const Fruit& f) { std::cout << f.name() << "\n"; } int main() { Apple a; Banana b; analyze_fruit(a); /* prints "Apple" */ analyze_fruit(b); /* prints "Banana" */ return 0; }
So far, no surprises here. But what will happen if instead of taking a reference to a Fruit object on analyze_fruit, we take a Fruit object by value?
Any experienced C++ developer will immediately see the word "slicing" written in front of their eyes. Indeed, taking a Fruit object by value means that inside analyze_fruit, the object f is truly a Fruit, and never an Apple, a Banana or any other derived type:
/* same code as before... */ void analyze_fruit(Fruit f) { std::cout << f.name() << "\n"; } int main() { Apple a; Banana b; analyze_fruit(a); /* prints "Fruit" */ analyze_fruit(b); /* prints "Fruit" */ return 0; }
This situation is worth analyzing in further detail, even if it seems trivial at first. On the calls to analyze_fruit, we pass objects of type Apple and Banana as arguments which are used to initialize its parameter f (of type Fruit). This is a copy initialization, i.e., the initialization of f in both of these cases is no different from the way f is initialized on the code fragment below:
Apple a; Fruit f(a);
Even though Fruit does not define a copy constructor, one is provided by the compiler. This default copy constructor merely copies each data member of the source Fruit object into the corresponding data member of the Fruit object being created. In our case, Fruit has no data members, but it still has a vtable pointer. How is this pointer initialized? Is it copied directly from the input Fruit object? Before we answer these questions, let us look at what the compiler-generated copy constructor of Fruit looks like:
struct Fruit { /* compiler-generated copy constructor */ Fruit(const Fruit& sf): vptr(/* what goes in here? */) { /* nothing happens here */ } virtual const char* name() const { return "Fruit"; } };
The signature of the Fruit copy constructor shows that is takes a reference to a source Fruit object, which means if we pass an Apple object to the copy constructor of Fruit, the vtable pointer of sf (for "source fruit"), will really point to the vtable of an Apple object. In other words, if this vtable pointer is directly copied into the vtable pointer of the Fruit object being constructed (represented under the name vptr on the code above), this object will behave like an Apple whenever any of its virtual functions are called!
But as we mentioned on the second code example above (the one in which analyze_fruit takes a Fruit object by value), the Fruit parameter f always behaves as a Fruit, and never as an Apple or as a Banana.
This brings us to the main lesson of this post: vtable pointers are not common data members which are directly copied or moved by copy and move constructors respectively. Instead, they are always initialized by any constructor used to build an object of a polymorphic class type T with the address of the vtable for the T class. Also, assignment operators will never touch the values stored by vtable pointers. In the context of our classes, the vtable pointer of a Fruit object will be initialized by any constructor of Fruit with the address of the vtable for the Fruit class and will retain this value throughout the entire lifetime of the object.
Comments
No comments posted yet.