Before we begin...
This article has been reformatted to be more readable on GameDev.net, the original can be found at the following
blog.
Are you sitting comfortably?
C++ supports two powerful abstractions, Object Orientation and Generic Programming. Ask any battle-hardened games industry veterans about the two and you're likely to see an eye twitch with the latter. It's not that Generic Programming is particularly hard but the errors you get out of the language can be particularly verbose without even getting to the private hell of errors relating solely to that usage...
This article provides example issues with template typedefs and the alternatives that modern C++ provides.
Let's make a game!
Let's say you have a simple game where multiple wizards lay the smack down, nerdy-spellcast style! We'll impose some rules:
- Each battle arena contains several magic pools, each imbued with a different spell.
- A wizard casts spells using these pools (maybe their robes soak up the juice?)
- Over time, these pools lose their power. When the power is lost, spells can no longer be cast.
Sounds... fun? Let's get into it.
Modelling Spells
Let's briefly look at two approaches to the modelling of spells. Often a key difference between OO and Generic code is that we may have a reliance on dispatch when identifying "IS-A" relationships with the former, and
Type Traits or
Duck Typing for the latter.
Your OO code may look something like:
class ISpell abstract
With the concrete specification of two spells:
class MagicMissileSpell : public ISpell
class HealSpell : public ISpell
(Actually, if this gets any more complicated it would be a good idea to take a look at prototype and component patterns at
http://gameprogrammingpatterns.com and save yourself a headache).
Your Generic approach on the other hand is likely to be more like:
template
class Spell
{
T mSpell;
}
With the implementation provided by individual classes satisfying whatever functionality the spell requires:
class MagicMissile
class Heal
There are benefits and pitfalls to both approaches and in all honesty the two aren't even mutually exclusive. Let's not dwell on exactly why you would pick one implementation over the other (I didn't) but instead focus on how to make the code work well (I had to).
Spell Ownership
It seems like we need some consideration over ownership in this game:
"Over time, these pools lose their power. When the power is lost, spells can no longer be cast."
Ownership semantics in C++ 11 are supported in one way with smart pointers. We can model this scenario by letting each pool hold a shared pointer to the spell type, with each wizard holding a weak pointer to the same asset as required. As long as we lock that weak pointer whilst we cast, the condition should be fine (we do have a small amount of time where the cast could be using magic no longer in the pool, but we'll pretend Wizards are just down with that).
OO Spell Ownership
This looks pretty easy, we'll set up:
class Pool
{
// ...
private:
std::shared_ptr mSpell;
};
class Wizard
{
// ...
public:
bool cast(std::weak_ptr spell);
};
We could choose to define a type for these pointers, making them easily alterable and reducing the amount of typing:
typedef std::shared_ptr SharedSpellPtr;
typedef std::weak_ptr WeakSpellPtr;
Looks OK, we're actually going to leave the OO approach now as it doesn't suffer from the same plague affecting the Generic approach but feel free to check out
the source for a more in-depth comparison.
Generic Spell Ownership
Let's take a quick step back and look at how our spells are modelled again. The pools in this implementation will want to be imbued in a similar way, so how would that look? As we don't have the common base we will have to bind to a template on the pool:
template
class Pool
{
//... will have mSpell variable, related to T
}
An explicit specialisation of
T can be provided as a constructor argument. For example, a magic missile spell:
std::shared_ptr> mMagicMissileSpell;
In the same manner as the OO aproach, we can probably define this as a custom type:
typedef Spell MagicMissileSpell;
std::shared_ptr magicMissileSpell
Maybe even go further...
typedef std::shared_ptr MagicMissileSpellPtr;
MagicMissileSpellPtr magicMissile;
This is especially useful if we were overriding types with allocators etc as we get to avoid writing an essay every time we use the type (which would also be error prone as hell).
The problem here is that we're going to have to jump through the same hoops to define the weak pointer, and any other structures we wanted further down the line (unique pointers, vectors, maps...). It doesn't scale too well and needs a lot of boilerplate for every spell.
Template Typedefs
Wouldn't it be great if we could define a more abstract template type for the above? We can... eventually. Let's start with a more general shared spell shared pointer:
template
typedef std::shared_ptr> SpellSharedPtr;
This looks innocuous enough... but try to compile and *gasp*
error C2823: a typedef template is illegal
ILLEGAL??? That's not ideal... and sure enough, this is a
well trodden restriction of olden times C++.
The common workaround is to take advantage of the fact that classes can be templated, and can contain
typedef:
template < typename T >
class SpellType
{
public:
typedef std::weak_ptr< T > SpellWeakPtr;
typedef std::shared_ptr< T > SpellSharedPtr;
}
typedef SpellType MagicMissileSpellType;
Which now means that we can refer to the various pointers like so:
MagicMissileSpellType::SpellSharedPtr magicMissileSharedPtr;
MagicMissileSpellType::SpellWeakPtr magicMissileWeakPtr;
This is the point where a lot of literature leaves the subject. Sadly it can still get a little worse. Disappointment comes whenever we want to use that type definition (e.g. if we set up a magic pool like so):
template
class Pool final
{
public:
explicit Pool(SpellType::SpellSharedPtr spellPtr)
: mSpellPtr(std::move(spellPtr))
{
}
private:
SpellType::SpellSharedPtr mSpellPtr;
};
On compilation of the above, we're again greeted with a nice compilation error:
warning C4346: 'SpellType::SpellSharedPtr' : dependent name is not a type. prefix with 'typename' to indicate a type
This one is pretty obviously fixable, we just need to rephrase that declaration
every time we see it:
typename SpellType::SpellSharedPtr
We've got a workable solution, there's one last consideration here though...
What if our spells were referenced in a large amount of places? Maybe we're not so sure whether the pool should be the sole owner anymore, shared ownership might be fine but the model holds well together... for now. Let's define an
alias (remember that name for later). We reserve the right to change type later and it's going to be a single point of change (with some hopefully minor fiddling with locks etc, dependant on functionality):
template
class SpellTypePointer
{
typedef typename SpellType::SpellSharedPtr Type;
};
SpellTypePointer::Type spellPtr;
Notice the typename again, you'll probably forget to type it every time. There was a point where every code review I ever took for this pattern had someone arguing against that keyword too. The technique works well enough but when you've had to defend your code for the fiftieth time, you really wish there was an alternative...
Type Alias, Alias Template
In the C++ 11 standard, type alias and alias templates fill this hole in functionality. For Visual Studio this means upgrading to 2013 but it's worth the wait. Remember when we couldn't even define this type:
template
typedef std::shared_ptr> SpellSharedPtr;
The syntax for Alias Templates make this all possible by propagating the template binding:
template
using SpellSharedPtr = std::shared_ptr>;
This feels much cleaner, the same technique can be applied to all the above examples as well.
So
here are the details:
- A type alias declaration introduces a name which can be used as a synonym. This is essentially the new typedef.
- An alias template is a template which allows substitution of the template arguments from the alias template. The new functionality allowing us to define aliases on templates like we never could before.
Academic challenges are part of the student journey. When I recently needed help with a project, I turned to read more. It was a decision I didn't regret. The expert assigned to my project displayed professionalism and delivered a well-structured and coherent assignment. Their support was invaluable, and I wouldn't hesitate to recommend these professionals to all students who are facing similar challenges.