https://www.gamedev.net/blog/1930/entry-2260667-a-simple-and-fast-dynamic-type-system/
Last time, I've introduced my runtime type system. So this time around, I'll talk about how one can actually run code based on the type system.
Remember that there is a static type, which divides into multiple PODs, Objects, etc... ? This allows us to check on a type-id and execute code based okn what it is:
if(typeId.GetType() == core::VariableType::INT){ // type can be statically casted to int}else if(typeId.GetType() == core::VariableType::OBJECT){ // type is stored in "Object"-wrapper. While we can't access the actual type just based on that // this wrapper-class has a few methods to help us with that.}
Now in practice, sometimes one might have to query for a specific static type. Usually, you are doing that using the specific TypeID generated by core::generateTypeId().
If you require only the static type, most of the time there is a block of code that performs a different routine for each specific type. This looks like so:
switch(type){case VariableType::BOOL: break;case VariableType::FLOAT: break;case VariableType::INT: break;case VariableType::STRING: break;case VariableType::OBJECT: break;case VariableType::ENUM: break;case VariableType::STRUCT: break;default: ACL_ASSERT(false);}
Normally you would also have to check if the type is an array, and make a separate switch block for that. Yikes! Now the switch-statement is actually okay, in the current design there is really no good way around
that for what we want to do with it. But I still want to encapsulate it, so that a user of the type-system doesn't have to write an actual switch-statement for executing type-based code. Here comes template magic again.
We make a callByType<>()-function, that takes a functor, and an unlimited amount of parameters (C++11 variadic templates, yay):
templateauto callByTypeSingle(VariableType type, Args&&... args) -> decltype(Functor::Call(args...)){ switch(type) { case VariableType::BOOL: return Functor::Call(args...); case VariableType::FLOAT: return Functor::Call(args...); case VariableType::INT: return Functor::Call(args...); case VariableType::STRING: return Functor::Call(args...); case VariableType::OBJECT: return Functor::Call(args...); case VariableType::ENUM: return Functor::Call(args...); case VariableType::STRUCT: return Functor::Call(args...); default: ACL_ASSERT(false); }}
So this hides the ugly switch statement, by calling "Call" on the functor, with the type passed as template argument to this Call-method. Note that I use the decltype-mechanic for determining the return type of the Functor::Call<>().
Sounds complicated at first, but its really not hard to use. For example, this is how you perform a type-to-string-conversion:
struct toString{ template static std::wstring Call(const Variable& variable) { return conv::ToString(variable.GetValue()); } template<> static std::wstring Call(const Variable& variable) { return variable.GetValue(); } template<> static std::wstring Call(const Variable& variable) { auto& object = variable.GetValue(); return objectToString(object); } template<> static std::wstring Call(const Variable& variable) { auto& en = variable.GetValue(); return enumToString(en); } template<> static std::wstring Call(const Variable& variable) { auto& s = variable.GetValue(); return structToString(s); }};std::wstring valueToString(const Variable& variable){ ACL_ASSERT(!variable.IsArray()); return variable.CallByTypeSingle(variable);}
The toString-Functor has multiple overloads of a static templated Call-method. This is due to the fact that objects, enum, and structs have custom toString-methods, and all POD-types can be converted using the conv::ToString-function.
It is also possible to optimize certain types, like directly returning the value of the variable in case it is already a string, instead of having to pass it to the conv::ToString-method.
So there you go. This is how you develop functionality for the type-system. The syntax is a little bit verbose, but you can always hide it behind a helper-function like I did it here. As you have seen in the code example above,
I've used a "Variable" called class, which acts as a variant. This is what makes the type-system usable. I'll explain it next time. Thanks for reading!
-> decltype(Functor::Call<bool>(args...))
But then you return, for example:
case VariableType::FLOAT:
return Functor::Call<float>(args...);
How does that work? I'm not familiar with the decltype -> syntax but aren't you returning different types from the method here depending on the type?