Friday, August 10, 2012

Object Oriented C: Virtual Table (vtable)

I wrote a previous post on Class-Like Structures for usage in C, in order to create objects that would allow for both inheritance and polymorphism. However, it's annoying to keep a pointer to each type of required function for each type of object, and often times you'll need fancy typecasting in multiple locations in order to avoid compiler warnings.

In C++ each class that contains methods also has a virtual table (vtable). A vtable is simply a pointer to a collection of function pointers. In C++ member functions pointers (pointers to member functions, or methods) aren't actually the exact same as function pointers, but the concept of the vtable in C++ is the same as in C; the vtable keeps track of what functions are available for use by the object.

Luckily the murkiness of member function pointers are completely avoided when working with C and pure function pointers. A vtable in C can consist of a structure with each data member as a function pointer, or an array of function pointers. Whichever style chosen should be chosen based on preference as either method of implementation can be fine. I personally like using a structure.

Lets take a look at the structure defined in my older post:

typedef struct _GameObj
{
  int HP;
  Vector2D_ position;
  GAME_OBJ_TYPE ID;
  Image_ *image;
  void (*Construct)( GameObj_ *self ); 
  void (*Init)( GameObj_ *self );  
  void (*Update)( GameObj_ *self );  
  void (*Draw)( GameObj_ *self );  
  void (*Destroy)( GameObj_ *self );
} GameObj_;

This structure requires memory space for each location. This is unnecessary and a bit annoying to handle during initialization, and can lead to annoying typecasting in various locations. Here's what the new object should look like if we introduce a vtable:

typedef struct _GameObj
{
  int HP;
  Vector2D_ position;
  GAME_OBJ_TYPE ID;
  Image_ *image;
  const _GAMEOBJECT_VTABLE *vtable;
} GameObj_;

This cuts the size of memory required for the virtual functions for this particular object down by a factor of 5. Here is what the definition of the vtable can look like:

typedef struct _GAMEOBJECT_VTABLE
{
  void (*Construct)( struct _GAMEOBJECT *self );
  void (*Init)     ( struct _GAMEOBJECT *self );
  void (*set)      ( struct _GAMEOBJECT *self );
  void (*Update)   ( struct _GAMEOBJECT *self );
  void (*Draw)     ( struct _GAMEOBJECT *self );
  void (*Destroy)  ( struct _GAMEOBJECT *self );
} _GAMEOBJECT_VTABLE;

Since a vtable is in use in the game object, typecasting may be necessary if the parameters of the various functions of the vtable differ from object to object. However a single typecast is all that is necessary during initialization. Typecasting the vtable itself to change the parameters of the functions within the vtable doesn't actually modify any memory. It's important to understand that when you typecast a pointer (and consequently a vtable pointer) no data is modified within the pointer; how the data the pointer points to is interpreted is changed.

So what about the code for this typecasting? Here's a small example of some initialization code for initializing the vtable data member:

const _SOME_OTHER_VTABLE SOME_OTHER_VTABLE = {
  SomeOtherConstruct,
  SomeOtherInit,
  SomeOtherSet,
  SomeOtherUpdate,
  SomeOtherDraw,
  SomeOtherDestroy
};

((GAMEOBJECT *)object)->vtable_ =
  (const _GAMEOBJECT_VTABLE *)(&SOME_OTHER_VTABLE);

// Example invocation of an object's vtable's function
object->vtable->Destroy( object );
((SOME_OTHER_VTABLE *)object->vtable)->SomeOtherUpdate( other params );

It's important to note that some extra parentheses are required to force typecasting to occur to avoid confusing operator precedence issues. The vtable should be initialized directly after allocation of the object. This ensures that the vtable pointer will never be accessed without being initialized.

The definitions of the SomeOther functions can have any type of parameters to your heart's desire!

It may be a bit confusing having a Construct function within the vtable when allocation of the object and initialization of the vtable data member happen outside of the Construct function. This is because you cannot actually call the Construct function from the vtable until the vtable is allocated and initialized. Handling of creation of objects would be best done with the Factory Pattern.

I'll likely write a post in the near future on the factory pattern :)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.