Things you should know: vtables

Dec 23, 2012

Virtual method tables (vtables) are used by some languages (like C++) to support run-time method binding. They often show up with inherited classes, when the virtual method to be called cannot be resolved at compile time. (Note: compilers are free to implement this however they wish, but these examples will focus on gcc).

Imagine the following classes:

class Test {
 public:
  virtual void f1() { cout << "Test::f1" << endl; }
  virtual void f2() { cout << "Test::f2" << endl; }
};

class Test2 : public Test {
 public:
  virtual void f1() { cout << "Test2::f1" << endl; }
  // Don't override Test::f2.
};

class Test3 : public Test {
 public:
  // Don't override Test::f1.
  virtual void f2() { cout << "Test3::f2" << endl; }
};

Somewhere in our code we may do the following:

Test* obj = GetObject();
obj->f1();

At compile time, there’s often no way to determine which f1 (Test::f1 or Test2::f1) should be called. To solve this, obj has a (hidden) data member which is a pointer to a vtable (_vptr in g++). The vtable contains the addresses of the object’s virtual methods in the order they were declared. The vtable is constructed once per class since each instance of that class will have the same vtable.

A more complete example is below.

// A simple program to play with the vtable.

#include <algorithm>
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;


class Test {
 public:
  virtual void f1() { cout << "Test::f1" << endl; }
  virtual void f2() { cout << "Test::f2" << endl; }
};


class Test2 : public Test {
 public:
  virtual void f1() { cout << "Test2::f1" << endl; }
  // Don't override Test::f2.
};


class Test3 : public Test {
 public:
  // Don't override Test::f1.
  virtual void f2() { cout << "Test3::f2" << endl; }
};


// Prints some information about obj and its vtable to stdout.
void PrintVtable(Test* obj, const string& class_name) {
  cout << endl;
  cout << "Class:\t\t"    << class_name << endl;
  cout << "size:\t\t"  << sizeof(*obj) << endl;
  cout << "address:\t" << obj << endl;
  cout << "vptr:\t\t"  << obj->_vptr << endl;
  cout << "f1 addr:\t" << (int*)obj->_vptr[0] << endl;
  cout << "f2 addr:\t" << (int*)obj->_vptr[1] << endl;
  // Calls f1.
  cout << "obj.f1():\t";
  ((void (*)()) obj->_vptr[0])();
  // Calls f2.
  cout << "obj.f2():\t";
  ((void (*)()) obj->_vptr[1])();
}

int main(int argc, char** argv) {
  Test obj;
  PrintVtable(&obj, "Test");

  Test2 obj2;
  PrintVtable(&obj2, "Test2");

  Test3 obj3;
  PrintVtable(&obj3, "Test3");

  // Another instance of Test.
  // This should point ot the same vtable as the first Test obj.
  Test another_obj;
  PrintVtable(&another_obj, "Test");

  return 0;
}

Sample output of the program may look like this:

$ g++ vtable.cc -o vtable && ./vtable

Class:    Test
size:     8
address:  0x7fff58ae47e8
vptr:     0x10711d130
f1 addr:  0x10711c844
f2 addr:  0x10711c888
obj.f1(): Test::f1
obj.f2(): Test::f2

Class:    Test2
size:     8
address:  0x7fff58ae47e0
vptr:     0x10711d170
f1 addr:  0x10711c930
f2 addr:  0x10711c888
obj.f1(): Test2::f1
obj.f2(): Test::f2

Class:    Test3
size:     8
address:  0x7fff58ae47d8
vptr:     0x10711d1b0
f1 addr:  0x10711c844
f2 addr:  0x10711c9b0
obj.f1(): Test::f1
obj.f2(): Test3::f2

Class:    Test
size:     8
address:  0x7fff58ae47d0
vptr:     0x10711d130
f1 addr:  0x10711c844
f2 addr:  0x10711c888
obj.f1(): Test::f1
obj.f2(): Test::f2

Some important things to note:

  • The addresses of the two instances of Test are different, but the _vtpr values are the same (since they share the same vtable).
  • Test2’s vtable entry for f2 and Test3’s vtable entry for f1 point to the same values as the respective entries in Test’s vtable, since those methods are not overridden.

A quick note on efficiency: since calling f1 and f2 in the above example require an additional lookup, calling virtual methods is often inherently slower than calling non-virtual methods.

Again, remember that the C++ standard does not specify a standard method of implementing a vtable so the above code is certainly not portable, though it’s an easy way to play with the internals.

Resources:

Minimal command line argument processing→