C++ vtables - Part 4 - Compiler-Generated Code

(742 words)

So 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:

All of the above can happen without explicit code:

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.


Comments