Generic execution routine for type-based code

Published January 08, 2015
Advertisement
Last entry:
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!
1 likes 2 comments

Comments

Aardvajk
Help me understand here. You have:

-> 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?
January 09, 2015 10:31 AM
Juliean

Yeah, that part is a bit convoluted. Obviously all specializations of the functors "Call" have to have the exact same return value, as it is not known until runtime which one is called. So having different return values depending on the type should generate a compile error anyways. I don't know of any cleaner way to ensafe that, so I just used the "bool" overload to determining the return value, and all others must have the same return value (or a convertible to that, for that matter), or they will throw a generic compile error. Which specialization I take in the decltype is really arbitrary, and if I ever find out a way to make this more explicit, I'd probably change it.

January 09, 2015 11:39 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement