|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article describes an easy method to reduce compilation dependencies and build times. This method would not be interesting enough to warrant an article on it, except that it is the key to an interesting method of managing project-wide objects, and I have not seen this method mentioned anywhere. BackgroundWhile watching another religious war starting to erupt over the use of Singletons, I realized that I had not seen the method I use discussed anywhere else, including CP. It has some advantages to it, and, as I haven't seen it mentioned, I figured I'd post it. (Of course, having said that, I'll probably come across an article on the exact same thing tomorrow.) The methodThe method itself, as I said, is not very interesting. Simply use smart pointers to hold all non-POD objects stored in your class interface. Well, there is one interesting thing about it - you cannot use The reason that you can't use an (If you are unsure whether a smart pointer can be used for this purpose without creating a destructor in the holding class' .cpp file, look in the smart pointer's documentation for a statement saying something to the effect that it can hold and correctly destroy an incomplete type. If it says that, you can safely use it without having to remember to supply a destructor in the .cpp file.) As a quick example of the pattern I am talking about, here is a theoretical implementation of my wallet: //Header file, include guards not shown #include "arg.h" //Only uses forward declarations: class CreditCard; class BusinessCard; class DollarBill; class Wallet { private: //Make it simple - just one of each arg::grin_ptr<CreditCard> masterCardC; arg::grin_ptr<BusinessCard> businessCardC; arg::grin_ptr<DollarBill> dollarBillC; //anything else, but if they are classes, //wrap them in pointers as above. public: Wallet(); BusinessCard & businessCard() { return *businessCardC.get(); } //I really don't want to //expose the following two, //but this is simply an example... CreditCard & masterCard() { return *masterCardC.get(); } DollarBill & dollarBill() { return *dollarBillC.get(); } //anything else... }; //Implementation file #include "Wallet.h" #include "CreditCard.h" #include "BusinessCard.h" #include "DollarBill.h" Wallet::Wallet() : masterCardC(new MasterCard(/*any args*/)), businessCardC(new BusinessCard(/*any args*/)), dollarBillC(new DollarBill()) { } //And anything else... (Feel free to 'new' me some more dollar bills. :) ) Anyway, as you can see, nothing to get excited over, until you think about it for a second. We have just entirely eliminated all external dependencies except the 'arg.h' file from the The savings with the above method is not as good as a full pimpl implementation, as a full pimpl implementation enables you to recompile The method I have just outlined is simpler than the pimpl pattern, as it does not require you to create an intermediary ' The 'interesting use'The above method can be used to easily manage project-wide objects. This method, when used in a global, can quite often be used as a quasi-Singleton manager. Doing so will often simplify some aspects of your overall design, and it will do so without increasing compilation dependencies. Please do not take this to mean that I don't like Singletons. The pattern I am about to show you does not replace Singletons. There is nothing in this pattern to keep you from instantiating multiple copies of the objects held in this container. This pattern simply makes it very easy to manage and use project-wide objects, and it may be an appealing alternative if you do not really care to mess with figuring your way around Singleton instantiation order dependencies. Also, do not take this to mean that I am a huge proponent of globals. This pattern allows me to minimize the use of globals to two or three for my entire project, and I am happy with that. I do store quite a few variables within the global objects, though, and as these variables are defined within the header file of the global, whenever their interface changes, or I add another item to the global, every unit that Let me give a simple example of this 'interesting use'. The two changes to the previous example that are needed are to change it so that it holds things commonly held in Singletons, and make the class into a global. The example I gave while the religious war was raging was the following: //Header file (minus include guards again) #include "arg.h" class TextureManager; class LoggingSystem; class ObjectManager; //... class Globals { private: arg::grin_ptr<TextureManager> textureManagerC; arg::grin_ptr<LoggingSystem> loggerC; arg::grin_ptr<ObjectManager> objectManagerC; //... public: Globals(); TextureManager & textureManager() { return *textureManagerC.get(); } LoggingSystem & logger() { return *loggerC.get(); } ObjectManager & objectManager() { return *objectManagerC.get(); } //... }; //Implementation file: #include "TextureManager.h" #include "LoggingSystem.h" #include "ObjectManager.h" Globals::Globals() : textureManagerC(new TextureManager()), loggerC(new LoggingSystem()), objectManagerC(new ObjectManager()) /* and any other stuff */ { } //Here is a sample of a 'main' file: #include "Globals.h" Globals gGlobals; //The following #include is only //so we can access 'doSomething'. //We don't need it for the global creation. #include "TextureManager.h" int main() { gGlobals.textureManager().doSomething(); //... return 0; } And that is it, although if you need to pass in initialization parameters, in order to pass them to one of your classes, you will need to implement Globals * gGlobals; int main() { //Do whatever in order to get your 'args' //... //... and finally std::auto_ptr<Globals> tGlobals(new Globals(/*args*/)); gGlobals = tGlobals.get(); //... Using the method outlined above, you can explicitly control your object creation order, and very easily overcome the issues that arise when trying to control multiple Singletons with inter-singleton creation order dependencies. In addition, this method has a simpler syntax than Singletons. Singletons require something like But the truly interesting part is that using this technique, if you only modify the TextureManager.cpp file, it will be the only file recompiled at recompilation. If you modify the TextureManager.h file, only units that explicitly It is worth reading the last paragraph again, and looking at the code, until you understand that this system is not exposing any of the other objects being managed by the The secrets to this whole technique: using only forward declarations and a capable smart pointer. I hope that you find this technique useful, and wish you happy coding. AddendumIf you do atomize your globals, and do not wish to use Singletons, you can modify the previous method to instantiate your globals within Globals * gGlobals; TextureManager * gTextureMan; //... int main() { std::auto_ptr<Globals> tGlobals(new Globals(/*args*/)); gGlobals = tGlobals.get(); std::auto_ptr<TextureManager> tTextureMan(new TextureManager()); gTextureMan = tTextureMan.get(); //... //And, if you want, you can even //destroy them in any order. //Just manually call 'release' on //the pointers in the order you want //at the end of 'main', rather than //relying upon the auto_ptr's destructors. You could even create a class to manage these atomized globals. Doing so would overcome the previous objection to long build times. I envision something of the following nature: //GlobalManager.h w/o include guards class TextureManager; class OtherGlobals; class GlobalManager { private: arg::grin_ptr<TextureManager> textureManagerC; arg::grin_ptr<OtherGlobals> otherGlobalsC; //... }; //GlobalManager.cpp #include "TextureManager.h" TextureManager * gTextureMan; #include "OtherGlobals.h" OtherGlobals * gOtherGlobals; GlobalManager::GlobalManager() { textureManagerC.reset(new TextureManager()); gTextureMan = textureManagerC.get(); otherGlobalsC.reset(new OtherGlobals()); gOtherGlobals = otherGlobalsC.get(); //... } //Main unit: #include "GlobalManager.h" int main() { //Automatically instantiate all //globals in one fell swoop: std::auto_ptr<GlobalManager> globals(new GlobalManager()); //... Using this method, all of your globals will automatically be instantiated for you in a manner that only forces your main unit to recompile if you add more globals. You no longer have the 'hiding' that took place in the earlier pattern I discussed, but you have a simple method of controlling your global class instantiation order. You can even explicitly control the destruction order, if you desire, by creating a destructor for the global organizer class, and calling 'release' upon the smart pointers in the order you want the objects to be released. Hopefully, the above discussion has given you more options when it comes to implementing global objects. As always, use what works for you, and keep the religious wars to a minimum :) History
| ||||||||||||||||||||