Some C Initialization Pitfalls


Some C++ Initialization Pitfalls

Before we work through our initialization checklist, let's get some critical initialization pitfalls out of the way. I've heard that power corrupts, and absolute power corrupts absolutely. You might get some disagreement from Electonic Art's executives on this point. I'll prove it to you by showing you some problems with going too far using C++ constructors to perform initialization. It turns out that C++ constructors are horrible at initializing game objects, especially if you declare your C++ objects globally.

Programming in C++ gives you plenty of options for initializing objects and subsystems. Since the constructor runs when an object comes into scope, you might believe that you can write your initialization code like this:

 // Main.cpp - initialization using globals // DataFiles g_DataFiles; AudioSystem g_AudioSystem; VideoSystem g_VideoSystem; int main(void) {    BOOL done = false;    while (! done)    {       // imagine a cool main loop here    }    return 0; } 

The global objects in this source code example are all complicated objects that could encapsulate most game subsystems. The fledgling game programmer might briefly enjoy the elegant look of this code, but that love affair will be quite short lived. When any of these initialization tasks fail, and they will, there's no way to recover.

Global objects under C++ are initialized before the entry point, in this case main (void). One problem with this is ordering; you can't control the order in which global objects are instantiated. Under Visual Studio.NET and other Microsoft compilers, the objects are instantiated in the order of the link, but you can't count on that being the case with all compilers. In other words, don't count on it. What makes this problem worse is that since C++ constructors have no return value, you are forced to do something ugly to find out if anything went wrong. One option, if you can call it that, is to check a member variable of the class to see if the initialization completed properly:

 // Main.cpp - initialization using globals // DataFiles g_DataFiles; AudioSystem g_AudioSystem; VideoSystem g_VideoSystem; int main(void) {    // check all the global objects for initialization failure    if (! g_DataFiles.Initialized() ||        ! g_AudioSystem.Initialized() ||        ! g_VideoSystem.Initialized() )    {       printf("Something went horribly wrong. Please return this game to the "          "store in a fit of anger and write scathing emails to FatBabies "          "about the company that would hire such idiots.");       return (1);    }    BOOL done = false;    while (! done)    {       // imagine a cool main loop here    }    return (0); } 

This code is suddenly looking less elegant. But wait, there's more! The wise programmer will inform their game players about what has gone wrong so they can have some possibility of fixing the problem. The simpler alternative of failing and dropping back to the operating system with some lame error message is sure to provoke a strong reaction.

If you want to inform the player you might want to do it with a simple dialog box. This assumes that you've already initialized the systems that make the dialog box function: video, user interface, data files that contain the button art, font system, and so on. This is certainly not always possible. What if your brilliant game player hacked into the art data files and screwed them up? You won't have any button art to display your nice dialog box telling the hacker they've screwed themselves. You have no choice but to use the system UI, such as the ugly message box under Windows. It's better than nothing.

Best Practice

Initialize your text cache, or whatever you use to store text strings, very early. You can present any errors about initialization failures in te right language. If the initialization of the text cache fails, present an error with a number. It's easier for foreign language speakers almost anywhere in the world to use the number to find a solution from a customer service person or a web site.

There are some good reasons to use global objects. One of the best ones is to trap the general exception handler; your code then has control over how the game will handle failures during initialization. If you write applications in MFC, the application object, CApp(), is global. These global objects have one important thing in common: They cannot fail. Make sure that any global object you create has the same quality: It cannot fail on construction.

Global object pointers are much better than global objects. Singleton objects such as the instantiation of the class that handles the audio system or perhaps your application object are naturally global, and if you're like me you hate passing pointers or references to these objects in every single method call from your entry point to the lowest level code.

Declare global pointers to these objects, initialize them when you're good and ready, and free them under your complete control. Here's an example of a more secure way to initialize:

 // Main.cpp - initialization using pointers to global objects // // A useful macro #define SAFE_DELETE(p)      { if (p) { delete (p); (p)=NULL; } } DataFiles *gp_DataFiles = NULL; AudioSystem *gp_AudioSystem = NULL; VideoSystem *gp_VideoSystem = NULL; int main(void) {    gp_DataFiles = new DataFiles;    if ( (NULL==gp_DataFiles) || (!gp_DataFiles->Initialized() ) )    {       // Please excuse the use of naked text strings! They are better for       // examples, but in practice I'd use a text cache for localization.       // Not everyone speaks English, you know.       printf("The data files are somehow screwed. Try to reinstall before you "          "freak out and return the game.");       return (1);    }    gp_AudioSystem = new AudioSystem;    if ( (NULL==gp_AudioSystem) || (!gp_AudioSystem ->Initialized() ) )    {      printf("The audio system is somehow screwed. Reboot and try running the "      "game again. That almost always works. ");      return (1);    }    gp_VideoSystem = new VideoSystem;    if ( (NULL==gp_VideoSystem) || (!gp_VideoSystem ->Initialized() ) )    {      printf("The video system is somehow screwed. Go get a real video "      "card before you even think of trying to run this game.");      return (1);    }    BOOL done = false;    while (! done)    {       // imagine a cool main loop here    }    SAFE_DELETE(gp_VideoSystem);              // AVOID DEADLOCK!!!    SAFE_DELETE(gp_AudioSystem);    SAFE_DELETE(gp_DataFiles);    return (0); } 

Note that the objects are released in the reverse order in which they are instantiated. This is no mistake, and it is a great practice whenever you need to grab a bunch of resources of different kinds in order to do something. In multithreaded operating systems with limited resources, you can avoid deadlock by allocating and deallocating your resources in this way. Deadlock is a nasty situation whereby two processes are attempting to gain access to the same resources at the same time, and cannot because they each have access to the resource the other process needs to continue. Computers are very patient, and will happily wait until the sun explodes. Get in the habit of programming with that problem in mind, even if your code will never run on an operating system where that will be a problem. It's a great habit and you'll avoid some nasty bugs.

Exception Handling

Sometimes you have no choice but to write code in a C++ constructor that has the possibility of failing. Certainly if you wrap the creation of some DirectX objects in a nice class you'll have plenty of places you'd wish a constructor can return an HRESULT. Instead of rewriting all your code to cripple the constructor and replace it with the ubiquitous Init() method that returns success or failure, use exception handling as shown here.

 // Main.cpp - nitialization using pointers to global objects and exception handling // // A useful macro #define SAFE_DELETE(p)           { if (p) { delete (p); (p)=NULL; } } DataFiles::DataFiles() {    // Imagine some code up here...    {        // blah blah blah    }    if (somethingWentWrong)    {         // You can throw anything you want, I'm throwing a custom class that         // defines errors, so don't go looking in MSDN for the ErrorCode         // class; it's mine!         throw ErrorCode(EC_DATAFILES_PROBLEM);    } } DataFiles *gp_DataFiles = NULL; AudioSystem *gp_AudioSystem = NULL; VideoSystem *gp_VideoSystem = NULL; int main(void) {     BOOL returnCode = 0;     try     {         // initialize everything in this try block         gp_DataFiles = new DataFiles;         gp_AudioSystem = new AudioSystem;         gp_VideoSystem = new VideoSystem;         BOOL done = false;         while (! done)         {             // imagine a cool main loop here         }    }    catch(ErrorCode e)    {       e.InformUser();         // ErrorCode can inform the user itself       returnCode = 1;    }    SAFE_DELETE(gp_VideoSystem);                  // AVOID DEADLOCK!!!    SAFE_DELETE(gp_AudioSystem);    SAFE_DELETE(gp_DataFiles);    return (returnCode); } 

That code is looking much nicer, and it's beginning to appeal to my sense of elegance. Any problem in initialization is going to throw an exception, jumping past the main loop entirely. The ErrorCode class, the design of which I'll leave as an exercise for the reader, simply reports the error back to the user in the best way possible given what systems are up and running. Perhaps the only thing it can do is send a text string out to stdout, or maybe it can bring up a nice dialog box using your game's graphics. After the error is reported to the player a useful macro frees the global objects that have been constructed. Finally, a return code is sent back to the operating system.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net