dcsimg
December 6, 2016
Hot Topics:

Coding Tidbits and Style That Saved My Butt

  • March 23, 2005
  • By Mike McShaffry
  • Send Email »
  • More Articles »

Virtual Functions Gone Bad

Virtual functions are powerful creatures that are often abused. Programmers often create virtual functions when they don't need them or they create long chains of overloaded virtual functions that make it difficult to maintain base classes. I did this for a while when I first learned how to program with C++.

Take a look at MFC's class hierarchy. Most of the classes in the hierarchy contain virtual functions which are overloaded by inherited classes, or by new classes created by application programmers. Imagine for a moment the massive effort involved if some assumptions at the top of the hierarchy were changed. This isn't a problem for MFC because it's a stable code base, but your game code isn't a stable code base. Not yet.

An insidious bug is often one that is created innocently by a programmer mucking around in a base class. A seemingly benign change to a virtual function can have unexpected results. Some programmers might count on the oddities of the behavior of the base class that, if they were fixed, will actually break any child classes. Maybe one of these days someone will write an IDE that graphically shows the code that will be affected by any change to a virtual function. Without this aid, any programmer changing a base class must learn (the hard way) for themselves what hell they are about to unleash. One of the best examples of this is changing the parameter list of a virtual function. If you're unlucky enough to change only an inherited class and not the base class, the compiler won't bother to warn you at all; it will simply break the virtual chain and you'll have a brand new virtual function. It won't ever be called by anything, of course.

Best Practice

If you ever change the nature of anything that is currently in wide use, virtual functions included, I suggest you actually change its name. The compiler will find each and every use of the code and you'll be forced to look at how the original was put to use. It's up to you if you want to keep the new name. I suggest you do, even if it means changing every source file.

From one point of view, a programmer overloads a virtual function because the child class has more processing to accomplish in the same "chain of thought." This concept is incredibly useful and I've used it for nearly ten years. It's funny that I never thought how wrong it can be.

An overloaded virtual function changes the behavior of an object, and gains control over whether to invoke the original behavior. If the new object doesn't invoke the original function at all, the object is essentially different from the original. What makes this problem even worse it that everything about the object screams to programmers that it is just an extension of the original. If you have a different object, make a different object. Consider containing the original class instead of inheriting from it. It's much clearer in the code when you explicitly refer to a method attached to a contained object rather than calling a virtual function.

What happens to code reuse? Yes, have some. I hate duplicating code; I'm a lazy typist and I'm very unlucky when it comes to cutting and pasting code. It also offends me.

Try to look at classes and their relationships like appliances and electrical cords. Always seek to minimize the length of the extension cords, minimize the appliances that plug into one another, and don't make a nasty tangle that you have to figure out every time you want to turn something on. This metaphor is put into practice with a flat class hierarchy—one where you don't have to open twelve source files to see all of the code for a particular class.

Use Interface Classes

Interface classes are those that contain nothing but pure virtual functions. They form the top level in any class hierarchy. Here's an example:

class IAnimation
{
public:
   virtual void VAdvance(const int deltaMilliseconds) = 0;
   virtual bool const VAtEnd()const = 0;
   virtual int const VGetPosition()const = 0;
};
typedef std::list<IAnimation > AnimationList;

This sample interface class defines simple behavior common for a timed animation. We could add other methods such as one to tell how long the animation will run or whether the animation loops; that's purely up to you. The point is that any system that contains a list of objects inheriting and implementing the'IAnimation interface can animate them with a few lines of code:

for(AnimationList::iterator itr = animList.begin();itr
                                != animList.end(); ++itr)
{
   (*itr).VAdvance(d elta );
}

Interface classes are a great way to enforce design standards. A programmer writing engine code can create systems that expect a certain interface. Any programmer creating objects that inherit from and implement the interface can be confidant that object will work with the engine code.

Consider Using Factories

Games tend to build screens and other complex objects constructing groups of objects, such as controls or sprites, and storing them in lists or other collections. A common way to do this is to have the constructor of one object, say a certain implementation of a screen class, "new up" all the sprites and controls. In many cases, many types of screens are used in a game, all having different objects inheriting from the same parents.

In the book, Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et. al., one of the object creation patterns is called a factory. An abstract factory can define the interface for creating objects. Different implementations of the abstract factory carry out the concrete tasks of constructing objects with multiple parts. Think of it this way: a constructor creates a single object and a factory creates and assembles these objects into a working mechanism of some sort.

Imagine an abstract factory that builds screens. The fictional game engine in this example could define screens as components that have screen elements, a background, and a logic class that accepts control messages. Here's an example:

class SaveGameScreenFactory : public IScreenFactory
{
public:
   SaveGameScreenFactory();

   virtual IScreenElements * const BuildScreenElements()const;
   virtual ScreenBackgroundSprite * const
           BuildScreenBackgroundSprite()const;
virtual IScreenLogic * const BuildScreenLogic()const;
};

The code that builds screens will call the methods of the IScreenFactory interface, each one returning the different objects that make the screen including screen elements such as buttons and sprites, a background, or the logic that runs the screen. As all interface classes tend to enforce design standards, factories tend to enforce orderly construction of complicated objects. Factories are great for screens, animations, AI, or any nontrivial game object.

What's more, factories can help you construct these mechanisms at the right time. One of the neatest things about the factory design pattern is a delayed instantiation feature. You could create factory objects, push them into a queue, and delay calling the "BuildXYZ" methods until you were ready. In the screen example, you might not have enough memory to instantiate a screen object until the active one is destroyed. The factory object is tiny, perhaps a few tens of bytes, and can easily exist in memory until you are ready to fire it.

Use Streams to Initialize Objects

Any persistent object in your game should implement an overloaded constructor that takes a stream object as a parameter. If the game is loaded from a file, objects can use the stream as a source of parameters. Here's an example to consider:

class AnimationPath
{
public:
   //...A better idea! Use a default constructor and an Init method ...
   AnimationPath();
   Initialize (InputStream & stream);
   Initialize (std::vector<AnimationPathPoint> const &srcPath);

   //Of course, lots more code follows.
};

This class has a default constructor, and two ways to initialize it. The first is through a classic parameter list, in this case a list of AnimationPathPoints. The second initializes the class through a stream object. This is cool because you can initialize objects from disk, a memory stream, or even the network. If you want to load game objects from disk, as you would in a saved game, this is exactly how you do it.

If you read the first edition of this book, perhaps you remember that this section suggested you use input streams in the constructor of an object, like this:

AnimationPath (InputStream &stream);

Boy that was a horrible idea, and I'm not too big to admit it either. The kind "corrections" posted on the web help me catch this one. The unkind ones I'll happily forget! Here's why It is a bad idea: a bad stream will cause your constructor to fail. You can never trust the content of a stream; it could be coming from a bad disk file, or even from hacked network packets. Ergo, construct objects with a default constructor you can rely on, and create initialization methods for streams.

Best Practice

Test your stream constructors by loading and saving your game automatically in the DEBUG build at regular intervals. It will have the added side effect of making sure programmers keep the load/save code pretty fast.





Page 2 of 4



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date
Rocket Fuel