Reducing Dependencies In C++ by (01 August 2000) |
Return to The Archives |
Introduction
|
This article attempts to go over some guidelines which aim to help reduce
compile times and code dependencies in C and C++. A worthwhile goal in any
software project is to have your code depend on minimum external definitions and
declarations so that changing one thing in one place doesn't require a
recompilation or worse, rewrite of the code in another. This article tries to
come up with some general guidelines to promote code encapsulation. Since a
discussion of using encapsulation in general code "design" is a fairly big
topic, this article will only limit itself to easily applicable coding
techniques. Or you can ignore all the "encapsulation" talk, and just take these
as tips to keep your header files as clean and independent since that ends up
accomplishing the same goal to a large extent. Lets start with an example of a header file which is fairly dependent on other headers and see what we can do about it. The header file below contains a typical SoundSystem class and some other definitions.
Don't include a header if it’s only used to define a type for a pointer. The CSoundSystem class contains a CCamera pointer which is defined in "Game_camera.h". But by including "Game_camera.h" in our header file we are making all code which needs to be aware of CSoundSystem dependent on "Game_camera.h" and its definitions as well. For example, we might have a CSoundBuffer class which has nothing to do with the code in "Game_camera.h" but needs to talk to CSoundSystem. But by including "Game_camera.h" in the "SoundSystem.h", we end up making CSoundBuffer dependent on something it has absolutely no concern for. Remember that the ONLY time, your code needs a definition of a given type is when its actually using it, or creating it either explicitly, or implicitly as temporary variables. The best idea around this would be to just "forward-declare" the CCamera type, remove the "Game_camera.h" from our header, and only include it where its actually needed. Forward-declaring is simply accomplished by naming the CCamera class before the CSoundSystem definition. i.e
Taking this a step further, we can create a module-wide header which will only contain definitions and declarations needed by the module. A module like the SoundSystem is bound to make use of new types and constants it uses internally. All of these, and any other commonly used external definitions can be placed in a new header file. If a given type is only privately used by a single class, then consider redefining it as a nested type. The SoundInfo type in the CSoundSystem class is only internally used by the CSoundSystem class. The rest of the code doesn't need to be aware of it, so we can simply change it to be a nested class privately defined within CSoundSystem as follows.
Using the idea from the first guideline, we can redefine the array "m_currentSounds" as pointer to SoundInfo, and then dynamically allocate the array in the CSoundSystem constructor. This allows us to completely remove the definition of SoundInfo from the header and place it in the CSoundSystem implementation file, or a private header file only included by the CSoundSystem member functions. The header changes to
Use consts and enums instead of #defines in your headers. Consts and enumerations are often ignored in C++ code but these really are much better than using #define macros in nearly every way. The are a big help in debugging since you can actually see the const variables or enumerations with their values instead of mysterious numbers or strings which might have been #defined anywhere. Plus these are real "named" variables and types as opposed to preprocessor overrides like the #defines so you can have more than one const variable or enumeration with the same name in different types or namespaces. I would really recommend that you look into using them. The only downside is that static const variables will slightly increase the memory footprint of your application. However this should not be an issue unless you are developing for a very small platform. In our SoundSystem code, instead of #defining MAX_WAVEFILES and MAX_CHANNELS, we can define them as static const integers in our class. Or if using a compiler like VisualC++ which doesn't seem to support them, we can use enums. The class definitions then looks like either of the following two.
Use namespaces when you are unable to hide the definition of types or constants. In our example, the WaveFile array can be converted into a pointer using the above technique, but we can't hide it as a nested class because other Sound system related classes use the WaveFile type as well. Instead of simply forward declaring it globally, a better idea would be to create a SoundSystem namespace and define it in that to reduce the change of any name collisions. The header then changes to
We can now create a new header called "SndWaveFile.h" which defines the WaveFile type within the SoundSys namespace. Then we can the dynamically allocate the m_wavFiles array in the constructor like in the above example. Make sure you resolve the namespace either by a "using" directive at the beginning of the file, or by using the scope operator.
Of course, we can extend this concept to "module-wide" constants as well. Consider using an interface to decouple the client code from the class definition. The sound system will most likely be used all over the place in your game code. But if you spend some time looking over it you will find that disregarding a few management functions like initialization and shutdown etc, most of your game code only needs access to a few functions exported by the soundsystem. So you might want to use a general sound system interface within your game code and directly manipulate the CSoundSystem class only when you have to. Lets say that the only two functions in CSoundSystem which you need accessible by the game code are as follows.
Then you can just create an interface by using an abstract base class and then pass that interface around instead of having all the game code dependent on the CSoundSystem. This interface can be placed in a public header like "I_SoundSystem.h".
Your CSoundSystem class then becomes something like
The game now only needs to include "I_SoundSystem.h". So in your core system code, just downcast the CSoundSystem object to I_SoundSystem when you pass it to the game code. You might have code that looks like the following
Consider defining friend functions to access data and services exported by singleton objects. Your game code will typically consist of many classes which could be classified as "singleton" classes. That is, classes which never have more than one instance at a given time. Setting up singleton classes is simple. We can change our original SoundSystem class to the following
We make the CSoundSystem() constructor private, so that no one except itself can create it, then we export a static GetSoundSystem() function which will instantiate a static CSoundSystem object the first time its called, and always return a reference to it. Since you are guaranteed that there can only be one CSoundSystem object at a given time, you can treat its data as such. Now lets take this idea to our main System class which is responsible for creating all the other subsystems and basically just initializing, running and shutting down the system. The main System will also obviously a singleton object. A lot of times though, other subsytems will want to access common services, like getting time, getting an interface to another subystem etc. Having them directly depend on the main System class doesn't seem like a good idea, so what we do is define globally accessible functions to access those services and data. And then define those functions as friends to the System class. Lets consider that Sys_hdr.h contains all your global includes and functions. A header like this will typically be included in all the source files before anything else.
Then in your main System class, you can redefine these functions as
And finally in your System implementation file, first create a private CSystem pointer so that the friend functions can access the object. Private variables in file scope can be created by using an anonymous namespace. Anonymous namespaces guarantee that the code in them will only be limited to the current file.
And there we have it. GetTime() and GetSoundSystem() are globally accessible now, and yet, the client code doesnt need to be aware of the CSystem class. Bringing it all together Finally lets look at our new CSoundSystem header to see how it looks.
And our SoundSystem implementation file will look like
Well thats all I can think of for today. Most of this is just based on experience while working on a game project I am collaborating on. You might not need any of this when dealing with small projects only you are ever going to use, but they will hopefully come in handy when working on something bigger. Feel free to send any feedback. Happy coding. Gaz Iqbal http://www.thepeel.com/void |
|