Click here to Skip to main content
Click here to Skip to main content

Changing an Object's Polymorphic Behavior at Runtime with the V-table

, 1 Feb 2010
Rate this:
Please Sign up or sign in to vote.
This technique allows you to change the polymorphic behavior of an object at runtime using the v-table.

Introduction

I recently discovered an interesting C++ technique that I've never read about before, so I thought I'd share it here. It isn't a language feature or anything, but it is still interesting and (in my case at least) useful. The technique allows you to change the polymorphic behavior of an object at runtime.

Background

First, a little back story. I've got a Property class that provides generic access to an object's property value. To provide this, the Property class must know the data type of the property that it encapsulates. So, I've also got a DataType class that encapsulates a data type and provides generic access to values of that type. This DataType class uses standard polymorphic class design such that the abstract base DataType class is implemented for each data type that we need to support (i.e., DataType_int or DataType_MyClass). So, my Property class has a reference (pointer) to a DataType object which provides it with generic access to that type's value. This is also an example of the Strategy pattern, which allows for the Property class to change its behavior (its DataType) at runtime, and an example of design by composition (Property has a DataType) rather than inheritance (Property is subclassed for each DataType it must support). So far, I think that I'm on the right path.

The problem arises when I make a couple of DataType subclasses and begin trying to assign them to Property. Since Property has a reference to a DataType object, that object must exist somewhere. So, I have a couple of options. I can have Singleton instances of each DataType subclass and let Property objects reference those Singletons. Or, I can dynamically allocate an instance of a DataType class and let the Property class manage that object's memory. The latter would result in many small allocations, which would be slow and could fragment the heap. So it isn't desirable. And, I prefer not to keep globals around if at all possible, so the Singleton solution, while not terrible, was not ideal.

I started thinking of using a structure of function pointers to encapsulate the many behaviors required to encapsulate a given type. However, I quickly realized that this would result in huge objects when I really only want a single reference to a class of functionality that the group of functions would define. At this point, I realized (as I'm sure you also have) that what I needed was a class. The class provides each instance of it with a group of functions accessed via a single reference, the v-table. Following this train of thought, I began to think of an object as a reference to a group of functions (methods). If I just copied this reference, then I could change the functionality of my object (exactly the way that my Property class can change its functionality by changing its DataType reference). This is the standard Strategy design pattern.

Using the Code

The solution that I arrived at looks like this (I'll explain below):

 #include <cstring> // for memcpy

// Base DataType class
class DataType {
public:

    // Construction
    DataType() {}
    DataType(const DataType &newType) { setType(newType); }
 
    // Set the polymorphic behavior of this DataType object
    void setType(const DataType &newType) {
        memcpy(this, &newType, sizeof(DataType));
    }
 
    // Polymorphic behavior example
    protected: virtual int _getSizeOfType() const { return -1; }
    public: inline int getSizeOfType() const { return _getSizeOfType(); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return NULL; }
    public: inline const char *getTypeName() const { return _getTypeName(); }
};
 
// Implementation of DataType for 'int'
class DataType_int : public DataType {
public:

    // Construction
    DataType_int() {}
    DataType_int(const DataType &newType) : DataType(newType) {}
 
    // Polymorphic behavior example
    protected:  virtual int _getSizeOfType() const { return sizeof(int); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return "int"; }
};
 
// Implementation of DataType for 'float'
class DataType_float : public DataType {
public:

    // Construction
    DataType_float() {}
    DataType_float(const DataType &newType) : DataType(newType) {}
 
    // Polymorphic behavior example
    protected:  virtual int _getSizeOfType() const { return sizeof(float); }
 
    // Polymorphic behavior example
    protected: virtual const char *_getTypeName() const { return "float"; }
};
 
// Example
DataType myType = DataType_int();
const char *typeName = myType.getTypeName(); // returns "int"
int typeSize = myType.getSizeOfType(); // returns sizeof(int)
 
myType.setType(DataType_float());
typeName = myType.getTypeName(); // returns "float"

As you can see, when we set the type, we are simply using memcpy to make the object's v-table pointer point to the v-table of the object that gets passed in. This changes myType's polymorphic behavior to that of the new type! And, we no longer need pointers or singletons or dynamic memory allocations! We have an object that is the size of a v-table pointer, and that is all! If you prefer a bit of a speedup here, you could just use *((void**)this) = *((void**)&newType; to copy directly, assuming that your DataType class has no members (thanks to Dezhi Zhao for pointing that out in his comments below).

Please keep in mind that this technique is not standards compliant, as the standard doesn't say anything about v-tables or v-ptrs (thank you to all of the commentators below that pointed this out). If a compiler implements virtual methods in such a way that doesn't store lookup information within an object's memory space, this technique will fail completely. However, I have never heard of a C++ compiler that doesn't work this way.

Also, you can see that we can easily change the type of myType at any point during runtime. This allows you the flexibility of having an uninitialized array of DataType objects and initialize them whenever you like later. For the performance minded out there, Dezhi Zhao also pointed out below that this will most likely cause the processor's branch prediction to fail for the getTypeName() call immediately after changing it. This will only happen for the DataType_float version above, however, as the prediction will only fail if the processor has made a prediction already.

One thing that you may have noticed is the use of public proxy methods (getSizeOfType) that call protected virtual methods (_getSizeOfType). We need to do this because the compiler may skip the v-table lookup when it knows the actual type of an object (as opposed to pointers or references, where it doesn't). This is perfectly reasonable, but breaks our setup. Inside the proxies, though, the v-table lookup always happens. And because they are inline, all they really do is make the compiler look up the correct method in the v-table and call that one. Remember, however, that we are not removing the virtual method lookup. This setup will not speed up virtual method calls in any way. In fact, we depend on the compiler looking up our virtual method for this to work.

One important thing to note about this setup is the absence of any member variable in DataType. Since we are doing a memcpy expecting that both objects have the same size (sizeof(DataType)), none of DataType's subclasses may add any member variables. You could add member variables to DataType with no problem, but you are not able to add any member variables to subclasses. Since I didn't need any member variables for DataType, this didn't present a problem for me. However, it is not impossible to add member variables to subclasses. You just need to use memory that was provided in the base class as the memory where your members live. For example:

#include <cstring> // for memcpy

// Base DataType class
class DataType {
public:

    // Construction
    DataType() {}
    DataType(const DataType &newType) { setType(newType); }
 
    // Set the polymorphic behavior of this DataType object
    void setType(const DataType &newType) {
        memcpy(this, &newType, sizeof(DataType));
    }
 
protected:
 
    // Member data
    enum { kMemberDataBufferSize = 256, kMemberDataSize = 0 };
    char memberDataBuffer[kMemberDataBufferSize];
};
 
// My Data Type class
class DataType_MyType : public DataType {
public:

    // My base class
    typedef DataType BASECLASS;
 
    // Construction
    DataType_MyType() {}
    DataType_MyType(const DataType &newType) : DataType(newType) {}
 
    // Access myData
    inline int getExampleMember() const { return _getMemberData().exampleMember; }
    inline void setExampleMember(int newExampleMember) 
        { _getMemberData().exampleMember = newExampleMember; }
 
protected:
 
    // Member Data
    struct SMemberData {
        int exampleMember;
    };
 
    // Amount of member data buffer that we use (this class' member data +
    // all base class' member data)
    enum { kMemberDataSize = sizeof(SMemberData) + BASECLASS::kMemberDataSize };
 
    // Make sure that we don't run out of data buffer
    #define compileTimeAssert(x) typedef char _assert_##__LINE__[ ((x) ? 1 : 0) ];
    compileTimeAssert(kMemberDataSize <= kMemberDataBufferSize);
 
    // Access member data
    inline SMemberData &_getMemberData() {
        return *((SMemberData*) memberDataBuffer);
    }
    inline const SMemberData &_getMemberData() const {
        return *((const SMemberData*) memberDataBuffer);
    }
};

As you can see, the DataType base class simply provides a buffer of data which the subclasses may use to store whatever member data they like. While this setup is a bit messy, it clearly works, and without too many hoops to jump through.

Points of Interest

Finally, an excellent complimentary technique to use with this technique is Type Traits. While implementing my Property class with this new DataType setup, I realized that it was kind of a pain to specify your DataType subclass whenever you register a method or member as a property:

Property propList[] = {
    Property(
        "prop1",
        DataType_Prop1(), &MyClass::getProp1,
        DataType_Prop1(), &MyClass::setProp1
    ),
 
    Property(
        "prop2",
        DataType_Prop2(), &MyClass::getProp2,
        DataType_Prop2(), &MyClass::setProp2
    ),
};

Also, this setup isn't very type-safe, since if I change the return value of MyClass::getProp1, I would get no warnings or errors, and the program would (at best) crash and burn when I use that property. Ideally, you would declare properties like this:

Property propList[] = {
    Property("prop1", &MyClass::getProp1, &MyClass::setProp1),
    Property("prop2", &MyClass::getProp2, &MyClass::setProp2),
};

The data type would be pulled from the method declaration and converted into the appropriate DataType subclass. Luckily for me, my Property constructor already looked like this:

template <class Class, typename AccessorReturnType, typename MutatorArgType>
Property(
    const char *propertyName,
    const DataType &accessorDataType, AccessorReturnType (Class::*accessor)(),
    const DataType &mutatorDataType, void (Class::*mutator)(MutatorArgType)
) {
    set(propertyName, accessorDataType, accessor, mutatorDataType, mutator);
}

So, I already had the data types that I would need: AccessorReturnType and MutatorArgType. All I needed to do was have some mechanism to convert those compile-time C++ types into run-time DataType subclass objects. This is actually easy to do with a template trick called Template Specialization. I won't describe it here, but if you don't already know what it does, feel free to check out the link and come back. It is really powerful.

The basic idea is to have a templated class that is unimplemented, or implemented for the general case. Then, for each special case, we partially or completely specialize our template arguments and implement that as a new class, like this:

// General case is not implemented. 
// If you give this template a type that isn't supported,
// you'll get a compiler error
template <typename CppType> struct MapCppTypeToDataType;
 
// Macro to define a template specialization that maps the given CppType to the given
// DataType. Once mapped, you can access the DataType like so:
//    MapCppTypeToDataType<int>::Type
// This should resolve to whatever type you mapped to int (DataType_int, for example).
#define MAP_DATA_TYPE(CppType, MappedDataType) \
    template <> struct MapCppTypeToDataType<CppType> { \
        typedef MappedDataType Type; \
    }
 
// Function to convert a C++ type to a DataType object
template <typename CppType>
inline DataType GetDataType() {
    return MapCppTypeToDataType<CppType>::Type();
}
 
// Example
MAP_DATA_TYPE(int, DataType_int);
DataType myDataType = GetDataType<int>(); // returns a DataType_int

You can see how powerful this is. Now, we can add a new Property constructor that computes the correct DataType object for you:

template <class Class, typename AccessorReturnType, typename MutatorArgType>
Property(
    const char *propertyName,
    AccessorReturnType (Class::*accessor)(),
    void (Class::*mutator)(MutatorArgType)
) {
    set(
        propertyName,
        GetDataType<AccessorReturnType>(), accessor,
        GetDataType<MutatorArgType>(), mutator
    );
} 

This constructor allows you to declare properties as we would prefer to, like this:

Property propList[] = {
    Property("prop1", &MyClass::getProp1, &MyClass::setProp1),
    Property("prop2", &MyClass::getProp2, &MyClass::setProp2),
}; 

You can easily see how much easier and safer this constructor is than the old one. You no longer have to know the type of the method you are registering. The compiler, which already knows it, can simply do the work for you. And this method is safer, because if you change the return type of prop1 now, the compiler will simply change the DataType that gets used. And, if there isn't a DataType that supports the new return type, your compiler will give you an error, something along the lines of "Type was not declared in class 'MapCppTypeToDataType' with template parameters ...".

I hope that you've enjoyed reading about this technique. If you have any comments or questions, I'd love to hear them. Thanks for reading!

P.S.: I'm not sure that the code snippets above compile. They were meant only for illustration, not for compilation. However, if you find any errors, please let me know and I'll correct them.

History

  • January 24, 2010 - Original article.
  • February 01, 2010 - Fixed a few code errors.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

danielh_code
Software Developer Terminal Reality
United States United States
No Biography provided

Comments and Discussions

 
GeneralYou may want to check ATL out Pinmemberqisamuelzhang19-Feb-10 15:54 
GeneralMy vote of 1 Pinmemberjefito27-Jan-10 4:04 
GeneralRe: My vote of 1 PinmemberDezhi Zhao27-Jan-10 19:17 
GeneralRe: My vote of 1 [modified] Pinmemberdanielh_code29-Jan-10 7:23 
Thanks for your response.
 
With regard to your statement about "mucking about with v-tables", I am not technically. I am merely assuming that given a pointer to an object, the compiler will use something in that object's memory space to look up which version of a method to call. This happens to be the vptr in most cases, but as mentioned before, the standard doesn't mention v-tables or pointers. My implementation only asserts that given a blob of memory, something in that memory must tell the processor what function to call. I'm interested to know which compilers this assertion actually doesn't work on.
 
With regard to your suggestion (?) to use allocated objects, all of my work is done on embedded systems (specifically game consoles) where memory fragmentation is a big issue. So much so that everyone is forbidden to use new unless you REALLY need it, and even then to allocate only large blocks at a time. In the system described above, I would either need a manager to manage a large block of allocated memory for data type objects (which could run out of space) or perform lots of small object allocations (in this case, 4 bytes each), which is begging for fragmentation issues. With the system described above, allocations become unnecessary, which is a big win in my profession.
 
With regard to your opinion of the article, I am very sorry that it wasn't easy to read or understand. It is my first, and I'll try to be more thorough in the future. I hope that you understand (which I could have made more clear) that the tone of the article is more "Here's something that I bet you didn't know you could do in C++!" than "Here's the best way to solve this problem." Which is why I didn't provide test cases or source files (or apparently compilable code, for that matter). It solves my problem better than available alternatives, and is at least a little interesting, which is really the only reason I thought to share it with you guys.
 
modified on Friday, January 29, 2010 5:31 PM

GeneralRe: My vote of 1 Pinmemberjefito19-Feb-10 11:27 
GeneralAn interesting way of cooking vtable ponters; but hardly justifying the effort PinmemberDezhi Zhao25-Jan-10 20:46 
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effort Pinmemberdanielh_code29-Jan-10 6:29 
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effort PinmemberDezhi Zhao29-Jan-10 22:11 
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effort Pinmemberdanielh_code30-Jan-10 17:46 
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effort PinmemberDezhi Zhao31-Jan-10 6:13 
GeneralOne drawback Pinmemberemilio_grv24-Jan-10 20:36 
GeneralRe: One drawback Pinmemberenosch25-Jan-10 5:33 
GeneralRe: One drawback PinmemberOvermindDL125-Jan-10 17:29 
GeneralRe: One drawback Pinmemberwtwhite25-Jan-10 18:35 
GeneralRe: One drawback Pinmemberdanielh_code26-Jan-10 8:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140709.1 | Last Updated 1 Feb 2010
Article Copyright 2010 by danielh_code
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid