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.


//SoundSystem.h

#ifndef SOUNDSYSTEM_HEADER
#define SOUNDSYSTEM_HEADER

#include "windows.h" #include "mmsystem.h" #include "dsound.h" #include "GameStandardHdr.h" #include "I_Subsystem.h" #include "Game_camera.h" #include "Game_Util.h"

#define MAX_WAVEFILES 5 #define MAX_CHANNELS 8

struct WaveFile { ........ };

struct SoundInfo { ........ };

class CSoundSystem: public I_SubSystem { public: CSoundSystem(); void SetListener(CCamera * pCamera); void PlaySnd(const char * szFileName); const char * GetDriverName() const; bool IsActive() const; private: WaveFile m_wavFiles[MAX_WAVEFILES]; SoundInfo m_currentSounds[MAX_CHANNELS]; CCamera * m_pCamera; };

#endif


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


class CCamera;

class CSoundSystem: public I_SubSystem { ....... };


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.


class CSoundSystem: public I_SubSystem
{
	struct SoundInfo
	{	........
	};
	SoundInfo	m_currentSounds[MAX_CHANNELS];

public: ............. };


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


class CSoundSystem: public I_SubSystem
{
	struct SoundInfo;
	SoundInfo      *	m_currentSounds;
public:
};

And then in the implementation file, we define the struct, and allocate the array

//SoundSystem.cpp #include "SoundSystem.h"

struct CSoundSystem::SoundInfo { ...... };

//Constructor CSoundSystem::CSoundSystem : m_currentSounds(new SoundInfo[MAX_CHANNELS]) { }

//Destructor CSoundSystem::~CSoundSystem { delete [] m_currentSounds; }


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.


//SoundSystem.h

class CSoundSystem
{
	static const int MAX_WAVEFILE = 5;
	static const int MAX_CHANNELS = 5;

SoundInfo * m_currentSounds; WaveFile m_wavFiles[MAX_WAVEFILES]; public: ............ };

/* make sure you declare the static variables as const int CSoundSystem::MAX_WAVEFILE, and const int CSoundSystem::MAX_CHANNELS in the SoundSystem implementation file. */

Or by using enums.

class CSoundSystem { enum { MAX_WAVEFILE = 5, MAX_CHANNELS = 5 }; SoundInfo * m_currentSounds; WaveFile m_wavFiles[MAX_WAVEFILES]; public: ............ };


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


namespace SoundSys
{	struct WaveFile;
}

class CSoundSystem { SoundSys::WaveFile * m_wavFiles; public: }


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.


//SndWaveFile.h

namespace SoundSys {

struct WaveFile { ..... };

}

//SoundSystem.cpp #include "SoundSystem.h" #include "SndWaveFile.h"

using namespace SoundSys;

CSoundSystem::CSoundSystem : m_currentSounds(new SoundInfo[MAX_CHANNELS]) { }


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.


	void SetListener(CCamera * pCamera);
	void PlaySnd(const char * szFileName);
 


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".


//I_SoundSystem.h

class CCamera;

struct I_SoundSystem { virtual void SetListener(CCamera * pCamera)=0; virtual void PlaySnd(const char * szFileName)=0; };


Your CSoundSystem class then becomes something like


#include "I_SoundSystem.h"
#include "I_Subsystem.h"

class CSoundSystem : public I_SubSystem, public I_SoundSystem { public: void SetListener(CCamera * pCamera); void PlaySnd(const char * szFileName); };


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


#include "Sys_main.h"
#include "SoundSystem.h"
#include "Game_main.h"

//CGame is the game manager. Constructor defined as CGame(I_SoundSystem *pSound); .... m_pSoundSystem = new CSoundSystem(); m_pGame = new CGame(m_pSoundSystem); //gets casted down to an I_SoundSystem ....


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


#include "I_SoundSystem.h"
#include "I_Subsystem.h"

class CSoundSystem : public I_SubSystem, public I_SoundSystem { private: CSoundSystem(); public: static CSoundSystem & GetSoundSystem() { static CSoundSystem theSoundSystem; return theSoundSystem; } };


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.


//Sys_hdr.h

#include "windows.h"
#include "string"
#include "vector"
....

struct I_SoundSystem; namespace System { const float & GetTime(); I_SoundSystem * GetSoundSystem(); ...... }


Then in your main System class, you can redefine these functions as


//Sys_main.h

class CSoundSystem;

class CSystem { friend const float& System::GetTime(); friend I_SoundSystem * GetSoundSystem(); CSoundSystem * m_pSound; float m_fcurTime; public: ....... };


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.


//Sys_main.cpp

#include "Sys_main.h"
#include "SoundSystem.h"
#include "Game_main.h"

//Notice the lack of a name after the namespace namespace { CSystem * g_pSystem = 0; ........ }

//Constructor CSystem::CSystem() { g_pSystem = this; //do this first thing m_pSoundSystem = new CSoundSystem(); ....... }

//Friend functions namespace System { const float & GetTime() { return g_pSystem->m_fcurTime; } I_SoundSystem * GetSoundSystem() { return g_pSystem->m_pSoundSystem; } }


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.


//SoundSystem.h

#ifndef SOUNDSYSTEM_HEADER
#define SOUNDSYSTEM_HEADER

#include "I_Subsystem.h" #include "I_SoundSystem.h"

namespace SoundSys { struct WaveFile; ....... };

struct CCamera;

class CSoundSystem : public I_SubSystem, public I_SoundSystem { private: CSoundSystem();

struct SoundInfo; CCamera * m_pCamera; SoundInfo * m_currentSounds;

SoundSys::WaveFile * m_wavFiles;

public: ~CSoundSystem();

void SetListener(CCamera * pCamera); void PlaySnd(const char * szFileName); const char * GetDriverName() const; bool IsActive() const;

static CSoundSystem & GetSoundSystem() { static CSoundSystem theSoundSystem; return theSoundSystem; }

};

#endif


And our SoundSystem implementation file will look like


#include "Sys_hdr.h"
#include "mmsystem.h"
#include "dsound.h"
#include "Snd_hdr.h"	//for module types like WaveFile etc
#include "Game_camera.h"

namespace { static const int MAX_WAVEFILES = 5; static const int MAX_CHANNELS = 5; } ............


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

 

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.