Templatizing Singletons

Now, assuming we get our Singleton working just the way that we want it to, you may wish to create more Singletons in the future. You could create them all from scratch, but a better thing to do is instead create a consistent approach, creating templates and inheritance to create a single implementation that you can use for any class. At the same time, we can also learn about an alternative way of creating a Singleton class, which will look something like the following:

template <typename T> 
class Singleton
{
public:
Singleton()
{
// Set our instance variable when we are created
if (instance == nullptr)
{
instance = static_cast<T*>(this);
}
else
{
// If instance already exists, we have a problem
printf(" Error: Trying to create more than one Singleton");
}
}

// Once destroyed, remove access to instance
virtual ~Singleton()
{
instance = nullptr;
}

// Get a reference to our instance
static T & GetInstance()
{
return *instance;
}

// Creates an instance of our instance
static void CreateInstance()
{
new T();
}

// Deletes the instance, needs to be called or resource leak
static void RemoveInstance()
{
delete instance;
}

private:
// Note, needs to be a declaration
static T * instance;

};

template <typename T> T * Singleton<T>::instance = nullptr;

You'll notice that most of the differences have to do with the class itself. The very first line in our code above uses the template keyword which tells the compiler that we are creating a template, and typename T tells the compiler that, when we create a new object using this, the type T will be replaced with whatever the class we want it to be based on is.

I also want to point out the use of a static cast to convert our Singleton pointer to a T. static_cast is used in code generally when you want to reverse an implicit conversion. It's important to note that static_cast performs no runtime checks for if it's correct or not. This should be used if you know that you refer to an object of a specific type, and thus a check would be unnecessary. In our case, it is safe because we will be casting from a Singleton object to the type that we've derived from it (T).

Of course, it may be useful to see an example of this being used, so let's create an example of a class that we could use as a Singleton, perhaps something to manage the high scores for our game:

class HighScoreManager : public Singleton<HighScoreManager> 
{
public:
void CheckHighScore(int score);

private:
int highScore;
};

Notice here that, when we declare our HighScoreManager class, we say that it's derived from the Singleton class and, in turn, we pass the HighScoreManager class to the Singleton template. This pattern is known as the curiously recurring template pattern.

For more information on the curiously recurring template pattern, check out https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern.

After defining the class, let's go ahead and add in an example implementation for the function we've created for this class:

void HighScoreManager::CheckHighScore(int score) 
{
std::string toDisplay;

if (highScore < score)
{
highScore = score;
toDisplay = " New High Score: " + std::to_string(score);
printf(toDisplay.c_str());
}
else
{
toDisplay = " Current High Score: " + std::to_string(highScore);
printf(toDisplay.c_str());
}
}

By using the templatized version of our class, we don't need to create the same materials as in the preceding class. We can just focus on the stuff that is particular to what this class needs to do. In this case, it's checking our current high score, and setting it to whatever we pass in if we happen to beat it.

Of course, it's great to see our code in action, and in this case I used the SplashStage class, which is located in the Mach5 EngineTest project, under SpaceShooter/Stages/SplashStage.cpp. To do so, I added the following bolded lines to the Init function:

void SplashStage::Init(void) 
{
//This code will only show in the console if it is active and you
//are in debug mode.
M5DEBUG_PRINT("This is a demo of the different things you can do ");
M5DEBUG_PRINT("in the Mach 5 Engine. Play with the demo but you must ");
M5DEBUG_PRINT("also inspect the code and comments. ");
M5DEBUG_PRINT("If you find errors, report to [email protected]");

HighScoreManager::CreateInstance();
HighScoreManager::GetInstance().CheckHighScore(10);

HighScoreManager::GetInstance().CheckHighScore(100);

HighScoreManager::GetInstance().CheckHighScore(50);


//Create ini reader and starting vars
M5IniFile iniFile;

// etc. etc.

In this case, our instance has been created by us creating a new HighScoreManager. If that is not done, then our project could potentially crash when calling GetInstance, so it's very important to call it. Then call our CheckHighScore functions a number of times to verify that the functionality works correctly. Then, in the Shutdown function, add the following bolded line to make sure the Singleton is removed correctly:

void SplashStage::Shutdown(void) 
{
HighScoreManager::RemoveInstance();

M5ObjectManager::DestroyAllObjects();
}

With all of that gone, go ahead, save the file, and run the game. The output will be as follows:

As you can see, our code works correctly!

Note that this has the same disadvantages we discussed with our initial version of the script, with the fact that we have to manually create the object and remove it; but it takes away a lot of the busy work when creating a number of Singletons in your project. If you're going to be creating a number of them in your project, this could be a good method to look into.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.70.21