• Advertisement
  • entries
  • comments
  • views

Generic execution routine for type-based code

Sign in to follow this  


Last entry:

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!
Sign in to follow this  


Recommended Comments

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?

Share this comment

Link to comment

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.

Share this comment

Link to comment

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

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

Create an account with GameDev.net and join the game development conversation on our forums, blogs, articles, and more!

Sign me up!