• Advertisement
  • entries
  • comments
  • views

A custom variant class

Sign in to follow this  


Last entry:


So this time, as promised, I'm going to describe how the heart of the type-system, the variant class, is being designed/used. It uses both the TypeId and the generic execution routine, plus some additional template magic. This class is called "Variable".

The data:

Looking just at the private data this class stores, this is what we have:
class ACCLIMATE_API Variable{private: void* m_pData; TypeId m_type; bool m_isArray;};
That means there is a 16 byte overhead associated with every variant class, plus whichever is needed with the object/enum/struct wrapper. This seems a lot, but appears to be neglectible in practice - benchmarks show that using ie. this variable class vs. a pure float for generic calculations takes up only roughly twice the time. I'm going to show these benchmarks in more detail at another time though.


So one thing that we want to be able to do is create an instance of the variable from any given type, like this:const float value = 20.0f;const core::Variable variable(value);
For that reason, the variable-class has two templated constructors (something that I really haven't seen used so often.
templateVariable::Variable(Type& value) : m_type(generateTypeId(value)), m_isArray(isArray::value){ m_pData = initData::value>::Call(value);}templateVariable::Variable(const Type& value) : m_type(generateTypeId(value)), m_isArray(isArray::value){ m_pData = initData::value>::Call(value);}
At first I used to have many explicit ctor overload, but using the type-routines presented before, it is easy to generate the type-id dynamically, and determine whether the type is an array-type statically via templates. So we only need one const& and one non-const& ctor. The stick with templated constructors is,
that you can only have the template be resolved implicitly via an argument, which in our case is actually just what we want.
The initData-template takes care of generating the correct memory allocation operation. It basically resolved to only the allocation itself at compile time, which is pretty neat.
On the other hand, we have a constructor that takes only the TypeId:Variable::Variable(TypeId type, bool isArray) : m_type(type), m_isArray(isArray){ if(m_type != invalidTypeId) m_pData = CallByType(m_type.GetId()); else m_pData = nullptr;}
This constructor now uses the dynamic execution routine, to perform the very same dynamic allocation.


Since we have a dynamic allocation in the constructor, we somehow need to clean that up in the destructor. Now in the constructor, we don't have the type-information available in form of a templated type. Again, we can make use of the dynamic execution:
Variable::~Variable(void){ DeleteData();}struct deleteData{ template static void Call(void* pData) { delete (Type*)pData; }};void Variable::DeleteData(void){ if(m_pData) { CallByType(m_pData); m_pData = nullptr; }}
In this case, the deleter-struct is really simple, since the type to be deleted is already passed in directly.

Access to the data:

The last part to discuss, is how to access the data of this class. At some point, the value that a Variable-class is storing has to be accessed directly. For that manner, there is a SetValue/GetValue function pair:templatevoid Variable::SetValue(typename returnSemantics::value value){ ACL_ASSERT(m_type.GetType() == getVariableType::type); ACL_ASSERT(m_isArray == isArray::value); setObjectData::value>::SetData(m_pData, value, m_type.GetId());}templatetypename returnSemantics::value Variable::GetValue(void) const{ ACL_ASSERT(m_type.GetType() == getVariableType::type); ACL_ASSERT(m_isArray == isArray::value); return getObjectData::value>::GetData(m_pData, m_type.GetId());}
The returnSemantics is used again to quarantee correct semantics (value, reference, pointer) for whichever type is being used. Two assertions check whether variable-type matches the required type (those two methods are not there for altering the internal type). And then we have two more magic templates, which
basically resolve to a simple static_cast at compile time. Now we can make use of the class like that:void function(core::Variable& variable){ int value = variable.GetValue(); // return value templates are a thing, you just have to always specify the template per hand value += 25.0f; value *= 2.0f; variable.SetValue(value);}int someValue = 50.0f;core::Variable variable(someValue);function(variable);sys::log->Out(variable.GetValue());
Which internally resolved to the following code (without the type safety checks):void function(void* pVariable){ int value = *(int*)pVariable; value += 25.0f; value *= 2.0f; *(int*)pVariable = value;}int someValue = 50.0f;void* pVariable = new int(someValue);function(pVariable);sys::log->Out(*(int*)pVariable));delete pVariable;
There is really nothing more to it, so the only additional cost for using the variable-class lies in the additional data overhead, and the memory allocation/fragmented memory access.

Eliminating the costs:

While both these costs appear to be neglectable in practice due to my benchmarks, there are still a few options to making the cost even more insignificant. This is just some stuff I've come up with, but didn't apply yet:

- Store the value of POD-types inside the pointer. Obviously, beginning with string and ending with structs, the more complex types require some additional information which does not fit into the 4 byte of the void pointer (I'm going to talk about those in more detail in another entry).
But for int, float and bool, we could do some seriously evil memory hacking and just misuse the void-pointer to store the value directly, instead of allocating a seperate memory area. There are a few concerns here, mostly regarding the size of the pointer/type itself, but I think those could
be very easily hidden inside the class. I really do think it is worth it in the end though, since it saves us 4 bytes in total space of the class, an allocation/deallocation, and a pointer access/dereferencation.

- Pack the type-information to reduce the class size. Obviously, the TypeId has not been written with compacity in mind. We require a total of 12 bytes to store all the type information, 4 each for the static and dynamic type, as well as the isArray-information. The static type only has a very limited number of bits.
As for the dynamic type, I also don't regard 4 billion possible types as necessary for my needs. So I could really just compact the type-id from 12 into 4 bytes, having the array take up 1 bit, static type the next 3, and the other 28 bits (we don't need unsigned) be reserved for the dynamic type, still leaving
place for 268 million different object/enum/struct types. I would still have to check whether gain of only having to access half the memory really outweights the cost of having to bitshift each individual type information out of the packed structure, but I'm pretty sure it does, specially since the new type size (8)
would still be byte-aligned.

Practical usage:

So at the end of this article, some quick examples of what you can really use this class for. As for now, I've only shown code where the variable class replaces a direct pass of an int to a function, which is not really practical. So here is what you can do:

- Settings: I use the variable-class in the settings-system. This allows me to store all known types as settings - be it simple PODs, up to structs (for screen-size), enums (for graphical settings), and objects (the windowskin-texture for a 2d rpg). This all happens without manual parsing, so you can just say:void BaseEngine::OnSettingsChanged(const game::SettingModule& settings){ auto pScreen = settings.GetSetting(L"screen"); auto vScreenSize = pScreen->GetValue(); //...}
- (Visual) scripting: I'm also using this class for my visual-scripting system, in fact this is the instance for which I came up with it. It is used to represent paramters/return values of functions, etc... I'm using it for the editor/GUI as well as for the runtime, which again is quite fast enough for now (though
I still could do some very serious optimization on that). But just judging how you can use that, its really again very easy:void WrapperForSomeScriptFunction::Execute(double dt, Stack& stack){ auto speed = stack.GetAttribute(0); auto vDirection = stack.GetAttribute(1); auto* pEntity = stack.GetAttribute(2); auto pPosition = pEntity->GetComponent(); const auto vTargetPosition = pPosition->vPosition + vDirection * speed; stack.ReturnValue(0, vTargetPosition)}
- Serialization: The Variable-class obviously fits very well for making the loading of type-information uniformely. There is ie. a VariableLoder::FromNode-method which takes an XML-node (which has type-information and the value) stored, and returns a variable-instance. There is obviously an VariableSaver::ToNode-equivalent.
Those are both used not only for serialization of the scripting, but also the entity-system (which was the original use-case I presented a while ago), and for game-state-serialization.

Wrapping it up:

So thats it for this time. I hope I managed to get across how useful such a variant-class is, especially since I've tailored mine exactly to the needs of the system. Next time, I'll cover how objects/enums/structs are handled in more detail. Thanks for reading!
Sign in to follow this  


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Advertisement