C++ vtables - Part 4 - Compiler-Generated Code
(742 words) Tue, Mar 22, 2016So far in this mini-series we learned how the vtables and typeinfo records are placed in our binaries and how the compiler uses them. Now we’ll understand some of the work the compiler does for us automatically.
Constructors
For any class’s constructor the following code is generated:
- Call parent(s) constructors if there are any;
- Set vtable pointer(s) if there are any;
- Initialize members according to initializer list;
- Execute code inside constructor’s brackets.
All of the above can happen without explicit code:
- Parent default constructors happen automatically unless otherwise specified;
- Members are default initialized unless they have a default value or an entry in the initializer list;
- The entire constructor can be marked
= default
. - Only the vtable assignment is always hidden.
Here’s an example:
#include <iostream>
#include <string>
using namespace std;
class Parent {
public:
Parent() { Foo(); }
virtual ~Parent() = default;
virtual void Foo() { cout << "Parent" << endl; }
int i = 0;
};
class Child : public Parent {
public:
Child() : j(1) { Foo(); }
void Foo() override { cout << "Child" << endl; }
int j;
};
class Grandchild : public Child {
public:
Grandchild() { Foo(); s = "hello"; }
void Foo() override { cout << "Grandchild" << endl; }
string s;
};
int main() {
Grandchild g;
}
Let’s write the pseudo-code for each class’s constructor:
Parent | Child | Grandchild |
---|---|---|
1. vtable = Parent’s vtable; | 1. Call Parent’s default c’tor; | 1. Call Child’s default c’tor; |
2. i = 0; | 2. vtable = Child’s vtable; | 2. vtable = Grandchild’s vtable; |
3. Call Foo(); | 3. j = 1; | 3. Call s’s default c’tor; |
4. Call Foo(); | 4. Call Foo(); | |
5. Call operator= on s; |
Given this, it’s no surprise that in the context of a class constructor, the vtable points to that very class’s vtable rather than its concrete class. This means that virtual calls are resolved as if no inheritors are available. Thus the output is:
Parent
Child
Grandchild
What about pure virtual functions? If they are not implemented (yes, you can implement pure virtual functions, but why would you?) you’re probably (and hopefully) going to segfault. Some compilers actually omit an error about this, which is cool.
Destructors
As one might imagine, destructors have the same behavior of constructors, only happen in reverse order.
Here’s a quick thought-exercise: why do destructors change the vtable pointer to point to the their own class’s rather than keep it pointing to the concrete class? Answer: Because by the time the destructor runs, any inheriting class had already been destroyed. Calling such class’s methods is not something you want to do.
Implicit casts
As we saw in Part 2 & Part 3, a pointer to a child is not necessarily equal to the same instance’s parent pointer (like in multiple inheritance).
Yet, there’s no added work for you (the developer) to call a function that receives a parent’s pointer. This is because the compiler implicitly offsets this
when you up-cast pointers and references to parent classes.
Dynamic casts (RTTI)
Dynamic casts use the typeinfo tables we explored in Part 1. They do it in runtime by looking at the typeinfo record that’s 1 pointer before what vtable pointer points to, and use the class there to check whether or not a cast is possible.
This explains the cost of dynamic_cast
when used a lot.
Method pointers
I plan to write a full post about method pointers in the future. Until then I’d like to stress that a method pointer pointing at a virtual function will actually call the overridden method (unlike non-member function pointers).
// TODO: add a link when the post is alive
Test yourself!
You should now be able to explain to yourself why the following piece of code behaves the way it does:
#include <iostream>
using namespace std;
class FooInterface {
public:
virtual ~FooInterface() = default;
virtual void Foo() = 0;
};
class BarInterface {
public:
virtual ~BarInterface() = default;
virtual void Bar() = 0;
};
class Concrete : public FooInterface, public BarInterface {
public:
void Foo() override { cout << "Foo()" << endl; }
void Bar() override { cout << "Bar()" << endl; }
};
int main() {
Concrete c;
c.Foo();
c.Bar();
FooInterface* foo = &c;
foo->Foo();
BarInterface* bar = (BarInterface*)(foo);
bar->Bar(); // Prints "Foo()" - WTF?
}
This concludes my first blog post, which grew to become a 4 piece post. I hope you learned some new things, I know I sure did.