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

By , 1 Feb 2010
 

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
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralAn interesting way of cooking vtable ponters; but hardly justifying the effortmemberDezhi Zhao25 Jan '10 - 20:46 
I viewed the optimized asm output from msvc with your first example (in the first half part, with some modifications) and found it still takes 2 indirections to get a member function address, the same as derivations from an abstract base class. So you will not gain any performence by maniuplating the vtables this way. In fact, you will lose some performence from memcpy operations.
And, vtable is already tough for branch predictions. Your cook book makes an indirect jump even harder to predict. So, I believe your approach of changing vtable pointers performs worse than tranditional abstract class derivations even without the need to benchmark them.
 
I understand your point trying to achieve a uniform base class. However, you sacrifice by:
      1. limiting the derived classes to the same size as base.
      2. losing performence by copying operations.
      3. complier implememtation dependent to some extent as others already pointed out
  
Your idea and article are very interesting. However, I can hardly see this approach can justify the down side.
 
Here is the code I use to test your idea. Please note I replace the memcpy by a pointer casting to simplify the code and level the play ground a little bit.
 

#include "stdafx.h"
#include <basetsd.h>
class DataType
{
public:
DataType() {};
DataType(DataType& newType) { *((UINT_PTR*) this) = *((UINT_PTR*) &newType); }
inline int getSizeOfType() { return _getSizeOfType(); }

protected:
virtual int _getSizeOfType() { return -1; }
};
 
class DataType_int : public DataType
{
public:
DataType_int() {};
protected: virtual int _getSizeOfType() { return sizeof(int); }
};
 
class DataType_char : public DataType
{
public:
DataType_char() {};
protected: virtual int _getSizeOfType() { return sizeof(char); }
};
 
int _tmain(int argc, _TCHAR* argv[])
{
 
DataType data[2] = {DataType(DataType_int()), DataType(DataType_char())};
int i;
 
i &= 1;
return data[i].getSizeOfType() + data[i ^ 1].getSizeOfType();
}
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effortmemberdanielh_code29 Jan '10 - 6:29 
Thank you very much for your feedback and suggestions.
 
As you pointed out, doing a uint copy is much faster than using memcpy to set the v-table pointer. I used the memcpy because there could have been members in the base class (as I described above). I agree, in the original implementation where there was only the v-table, your method is much faster. But I didn't need speed in the constructor, and the memcpy is more general. In your suggestion above, if another coder added a member to the base class, your copy method wouldn't copy the member data from the copied object. And while you could say that if another coder added member data, he should be responsible for changing the copy operation, you can't always depend on that. And since I didn't care about the speed anyways, I opted for the memcpy. Thanks for pointing it out, though.
 
And just to clarify, when you say "it still takes 2 indirections to get a member function address", you are talking about the standard v-table, right? That is the whole point of the design, to force the v-table lookup. So it should perform exactly the same as a pointer to a derived class. And how would the processor perform more poorly at predicting branches than regular pointers to objects? The ASM is the same. I ran it in VS release mode with the following code:
 
DataType dataType = DataType_int();
DataType *dataType_int = new DataType_int();
 
int main() {
	dataType.getSizeOfType();
	dataType_int->getSizeOfType();
}
and got the following ASM:
int main() {
	dataType.getSizeOfType();
00401010  mov         eax,dword ptr [dataType (403064h)] 
00401015  mov         edx,dword ptr [eax] 
00401017  mov         ecx,offset dataType (403064h) 
0040101C  call        edx  
 
	dataType_int->getSizeOfType();
0040101E  mov         ecx,dword ptr [dataType_int (4033C0h)] 
00401024  mov         eax,dword ptr [ecx] 
00401026  mov         edx,dword ptr [eax] 
00401028  call        edx  
}
 
As you can see, they are both exactly the same. How would the first call get worse performance than the second? I'm not saying that you're incorrect, I'm just interested in your train of thought.
 
With regards to standards compliance, I agree that this implementation doesn't comply. It isn't based on a particular compiler's idiosyncrasies, however, but on a common (and intuitive) implementation. Do you know of any particular compilers that would break the code? If so, I'd be interested to know if they support the following code:
 
struct Virtual {
	virtual ~Virtual() {}
	int member;
};
 
struct Class {
	int a;
	Virtual v;
	int b;
};
 
int main() {
 
	// Placement new object (heap corruption?)
	void *memory = malloc(sizeof(Virtual));
	Virtual *v = new (memory) Virtual();
 
	// Placement new member (object corruption?)
	Class *test = (Class*)malloc(sizeof(Test));
	new (&test->v) Virtual();
}
 
If the the vptr is not stored within &v + sizeof(Virtual), it should either corrupt the heap here or corrupt test->b. If this isn't the case, please let me know. I don't have a compiler that does vptr placement differently to test with.
 

With regards to the downsides you listed:
1. limiting the derived classes to the same size as base.
- I agree that it is a poor constraint that you must modify the base class to add members to a derived class. But, at least in my case, it is unnecessary anyways.
 
2. losing performance by copying operations.
- Performance isn't an issue. And in the worst case, you are copying four bytes, which isn't really that slow. Most memcpy implementations are optimized.
 
3. compiler implementation dependent to some extent as others already pointed out
- I will grant you this. But as I described above, compilers that this doesn't work on should also break normal language features, like placement new.
 

Again, thank you for your feedback and for posting compilable code for other readers.
 
- Daniel
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effortmemberDezhi Zhao29 Jan '10 - 22:11 
In this post, let me only address the performence hit besides the copy operation and my train of thought as you put it that way.Smile | :) to keep the discussion a little bit simple.
 
I must admit I assumed you know what you should compare to and know pretty well how a modern cpu works in my earlier posts.
 
As I said before to achieve polymorphism, we usually derive classes from an abstract interface class. Let's call it the tranditionnal way. Your way is to hijack the vtable pointer of a base class object. Let call it the cooked way.
 
Here is the simplest examples of the 2 apporaches:
 

The Traditional Way
#include "stdafx.h"
 
class DataType
{
public:
virtual int getSizeOfType() = 0;
};
 
class DataType_int : public DataType
{
public:
virtual int getSizeOfType() { return sizeof(int); }
};
 
class DataType_char : public DataType
{
public:
virtual int getSizeOfType() { return sizeof(char); }
};
 
int _tmain(int argc, _TCHAR* argv[])
{
 
DataType_int obj_i;
DataType_char obj_c;
 
DataType* data[2] = {&obj_i, &obj_c};
int i;
 
i &= 1;
return data[i]->getSizeOfType() + data[i ^ 1]->getSizeOfType();
}

 

The Cooked Way
#include "stdafx.h"
#include <basetsd.h>
class DataType
{
public:
DataType() {};
DataType(DataType& newType) { *((UINT_PTR*) this) = *((UINT_PTR*) &newType); }
inline int getSizeOfType() { return _getSizeOfType(); }

protected:
virtual int _getSizeOfType() { return -1; }
};
 
class DataType_int : public DataType
{
public:
DataType_int() {};
protected: virtual int _getSizeOfType() { return sizeof(int); }
};
 
class DataType_char : public DataType
{
public:
DataType_char() {};
protected: virtual int _getSizeOfType() { return sizeof(char); }
};
 
int _tmain(int argc, _TCHAR* argv[])
{
 
DataType data[2] = {DataType(DataType_int()), DataType(DataType_char())};
int i;
 
i &= 1;
return data[i].getSizeOfType() + data[i ^ 1].getSizeOfType();
}

 
Compile those 2 and check out the asm outputs, you'll see both take 2 indirections (to read/load 2 memory addresses) to reach the member function from the data list. I had wished the cooked way could have saved one before I saw its asm as I'm more intersted in the performence in this kind of topic.
 
Now the subtle part of the cooked way is that you could really change vtable pointer of a base class object at any time besides initialization time as in the article. A cpu predicts an indirect call/jump address by history. The simplest prediction is the last jump address, PIII does this for example. Newer cpus do more than last address but still rely on history. The vtable pointer of an given base object in the trditional way always points to the same member function adddress. In your cooked way, the vtable pointer can point to anything at any time in theory. You may say you don't do that. However, as you see, you've already done it once at inialization. Even you don't change the pointer anytime else beyong initialization, it is still an act of pollution to the prediction mechanism that bassically relies on history buffers. The same thing is true for the one time extra copy operation.
 
You may say the one time cost is not a big deal for even a time critical application to talk about. I probably agree most of the time. But I don't encourage this nowadays. Everbody is already feeling the pain to wait a program to load itsef up everyday. I'm not the only one wishing windows can be instant on Laugh | :laugh:    On the other hand, you maight have already asked youself why you want to hack the the pointer without performence gain in return. The only real return is an abstract base class that can be instantiated as I commented in other posts. I do appreciate the uniformity and apprent simplicity the cooked stuff offers. However, these factors are often a less concern for brave low level programmers who still care about C++ or/and asm. I have to mention the other extreme to illustrate my point here. As you know, there are dirty simple VB that even provides Variant service. Well, I know you don't like that for 101 reasons.
 
Have fun!
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effortmemberdanielh_code30 Jan '10 - 17:46 
Thank you again for your patience and good nature in your replies.
 
I was aware of the ability to change types at runtime beyond just the constructor. That ability is part of the design, albeit one that I didn't use here. The idea is that you can create an array of DataType objects, then initialize them individually later.
 
Thanks for explaining the branch prediction problem. I agree with your assessment that it probably would fail branch prediction. However, it should only fail for the first of subsequent calls after you change types.
 
I can't really see any reason why one would switch types, call a method, then switch back. But I see your point. The performance hit is there if one were to use the system that way. Thank you again for pointing it out.
 
- Daniel
GeneralRe: An interesting way of cooking vtable ponters; but hardly justifying the effortmemberDezhi Zhao31 Jan '10 - 6:13 
Thank you again for sharing the idea.
 
We all want the best of the two worlds if possible, performence and simplicity. C/C++ is a more reality oriented tool which lets programmers make the trade-off decisions. When perforemence is a concern, one has to consider the platform his program is running. In such case, there is really no virtual machine as in Java or .NET perfect world. You are forced to consider the limitations of the machine or cpu.
 
I forgot to mention about another minor thing: the virtual member function in the base class is generated by the compiler in the code. However, such a code snip will never be called by design. A few bytes of dead code itself is not a big deal. But, it is surely be arranged in memory to be close to other frequently used code by any compiler. This is a pollution to (or waste of) the code cache, that could be used by other useful code.
 
Of course, you can specify the code arrangement so that the dead code is, say, all way in the bottom of the memory if you use a sophisticated complier, ms or intel compliers for example. But, having to keep your own function map simply for the sake of dead code is not fun at all. Another option to avoid the dead code is to force the base do something real. You can put all the DataType_int member functions into the base class and get rid of DataType_int class. However, this is not so conceptually elegant when you see you actually derive DataType_char from DataType_int.

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

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130523.1 | Last Updated 1 Feb 2010
Article Copyright 2010 by danielh_code
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid