Click here to Skip to main content
15,886,919 members
Articles / Programming Languages / C++

Two-thirds of a pimpl and a grin

Rate me:
Please Sign up or sign in to vote.
4.74/5 (14 votes)
5 Jun 2014CPOL9 min read 90.3K   124   31   30
An article on a quasi-pimpl, as well as a neat global object manager.

Introduction

This article describes an easy method to reduce compilation dependencies and build times. This method would not be interesting enough to warrant an article on, except it is the key to an interesting method of managing project-wide objects, and I have not seen this method mentioned anywhere.

History

  • Oct 11th, 2005 - Added Addendum #1 outlining another global organizer method.
  • Jun 3rd, 2013 - Added Addendum #2 showing comparison of grin_ptr and shared_ptr.

Background

While watching another religious war starting to erupt over the use of Singletons, I realized 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 method

The 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 auto_ptr's or many other smart pointers in order to store your objects if you don't include at least an empty destructor inside the unit's .cpp file. That is the reason the following uses Alan Griffith's grin_ptr. You could also use boost::shared_ptr, and probably some others I am unaware of. (See Addendum #2 for more.)

The reason you can't use an auto_ptr for this purpose is simply that auto_ptr must have a complete definition of the forward declared class at the point of destruction, and if you rely upon the default destructor, the forward declaration of the class is the only thing the auto_ptr has, so it will generate a 'do-nothing' default destructor for the class being held. This is rarely, if ever, what you want.

(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 Wallet header. If the implementation to CreditCard, BusinessCard, or DollarBill changes, the only units that need to be recompiled in the project are the Wallet unit and the unit you changed. This is a big savings over having 'hard objects' in the class' header file. In that case, every unit that #included Wallet.h would be recompiled anytime the implementation to CreditCard, BusinessCard, or DollarBill changed.

The savings with the above method is not as good as a full pimpl implementation, as a full pimpl implementation enables you to recompile CreditCard, BusinessCard, or DollarBill, without the Wallet unit or any other unit in the project needing to be recompiled. (Of course, changing the interface to a pimpl can be a PITA (Pain In The A__), and will require more to be recompiled at that time.)

The just-outlined method is simpler than the pimpl pattern, as it does not require you to create an intermediary 'PimplHolder' class. You do, however, have to use '->' notation to access all of the smart-pointer-held objects from within the class they are held in, unless you create a local reference to them within the function using them.

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 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 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, which I am happy with. 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 which #includes Globals.h will be recompiled at that time. If your project takes considerable time for a rebuild of such a nature, you will want to carefully atomize your globals, maybe even to the point of making each object (like 'TextureManager' in the following code) into its own global item. I outline a wrapper that will simplify this for you in the first addendum at the end of this article.

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 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 gGlobals as a pointer, and initialize it after whatever parameter it is dependant upon is obtained. The best option is to implement it through the use of an auto_ptr (in which case auto_ptr has no problems):

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 SingletonManager::getInstance().textureManager().doSomething() in order to use them. The above method boils down to gGlobals.textureManager().doSomething().

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 #include "TextureManager.h" will be recompiled. This will include the Globals unit, but will not include every file that #includes "Globals.h".

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 Globals unit to any unit that is not #includeing the sub-unit you wish access to. You can #include "Globals.h" in every .cpp file in your program, but they won't link to TextureManager until you explicitly #include "TextureManager.h" as well as Globals.h in the unit you want to access the TextureManager from. There are no other compilation dependencies to be aware of, and the Globals unit does not impose any more overhead than a few forward declarations, the class declaration of Globals itself, and a few bits for the grin_ptr's internals.

The secrets to this whole technique: using only forward declarations and a capable smart pointer.

I hope you find this method useful, and wish you happy coding!

Addendum #1 - Global Instantiation

If you do atomize your globals, and do not wish to use Singletons, you can modify the previous method to instantiate your globals within main, and completely control your instantiation and destruction order:

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 :)

Addendum #2 - Incomplete Type Holders

While reworking some code I wondered if C++11 had a better smart pointer available than Allan Griffith's grin_ptr. According to Howard Hinnant's wonderful overview, the shared_ptr seems to be the best one to use. It predated C++11, so my investigation is a little slow, but my excuse is busyness!

Anyway, curiosity causes work, so I slapped the attached Visual Studio solution together to find out. If you back port it to an earlier environment than 2013 you will probably need to comment out the unique_ptr segment, but I believe that is the only C++11 part that will cause hiccups.

The bottom line was the grin_ptr was about 1.27 times slower than the shared_ptr on my system.

According to Hinnant's table, the unique_ptr requires a complete type at destruction. My curiosity caused me to add a test using what feels like an incomplete type, and it worked - cutting the time by a huge 2/3rds when compared to the grin_ptr version. Based on Hinnant's table and the fact that my incomplete type did call into the destructor, either I did something wrong, Hinnant is incorrect, or Microsoft's implementation is non-standard. That speedup is enough to think about replacing the grin_ptr in future code, and possibly older code when it comes to my attention. But it isn't worth going out of the way to change because I've never used grin_ptr in time-critical loops.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer www.randommonkeyworks.com
United States United States
I am the author of Laughing at the Devil: One Man’s Religious Discoveries. If you want to understand the astronomic investigations of our priests 3,000 years ago, LATD is the book to turn to. It opens up the thoughts that pushed them away from their earlier polytheism and towards our current definition of God.

Trained as a mechanical engineer, I have been involved with design, supervision, and project management. I taught myself C++ programming in order to play around with binaural beats more easily. I've also created various databases to help with project management and personal tasks.

Databases are cool and extremely useful! Happy coding, everybody!

Comments and Discussions

 
QuestionC++ 11 shared_ptr Pin
geoyar9-Jun-14 16:34
professionalgeoyar9-Jun-14 16:34 
AnswerRe: C++ 11 shared_ptr Pin
David O'Neil9-Jun-14 18:07
professionalDavid O'Neil9-Jun-14 18:07 
AnswerRe: C++ 11 shared_ptr Pin
Paul M Watt23-Aug-14 20:30
mentorPaul M Watt23-Aug-14 20:30 
Generalimpl_ptr by Vladimir Batov [modified] Pin
Rolf Kristensen22-Aug-08 22:17
Rolf Kristensen22-Aug-08 22:17 
Generalgrin_ptr vs. clone_ptr Pin
unjedai28-Nov-07 11:03
unjedai28-Nov-07 11:03 
QuestionHow to make Wallet strongly exception safe? Pin
~MyXa~24-Oct-05 22:09
~MyXa~24-Oct-05 22:09 
AnswerRe: How to make Wallet strongly exception safe? Pin
RandomMonkey25-Oct-05 4:30
RandomMonkey25-Oct-05 4:30 
GeneralRandomMonkey = David; Pin
~MyXa~25-Oct-05 21:31
~MyXa~25-Oct-05 21:31 
GeneralRe: RandomMonkey = David; Pin
RandomMonkey26-Oct-05 12:12
RandomMonkey26-Oct-05 12:12 
Generalpointing to abstractions. Pin
~MyXa~27-Oct-05 2:24
~MyXa~27-Oct-05 2:24 
GeneralRe: pointing to abstractions. Pin
RandomMonkey27-Oct-05 6:01
RandomMonkey27-Oct-05 6:01 
~MyXa~ wrote:
grin_ptr doesn't save us from writing operator=() by hand


Never said it did, so why is this an issue for you?

it doesn't help with abstract classes


Can you give an example? I do not understand.

and doesn't allow to write custom deleter.


I have never ran across a place where I needed a custom deleter, so this has never been an issue. Do you do that very often? If so, don't use grin_ptr, I guess.

May be I'm wrong, but it can't point to nowhere.


I had forgotten that I had rewritten it to get around this issue. Here is the file I use in place of arg_grin_ptr.h. You will still need the rest of Alan's arglib in order to get this to compile.

#ifndef RMW_GRIN_PTR_H
#define RMW_GRIN_PTR_H

#ifndef ARG_COMPILER_H
#include "arg_compiler.h"
#endif

#ifndef ARG_DEEP_COPY_UTILS_H
#include "arg_deep_copy_utils.h"
#endif

#include ARG_STLHDR(algorithm)

// ************
// The following is adopted from Alan Griffith's grin_ptr.  The changes made allow
// you to hold off constructing the 'pointee' until any time you choose - you don't
// have to construct it in the initializer list of a class.  I have also added a
// 'reset' function and a 'release' function, in order to give the maximum
// flexibility to the end programmer.
//
// Note that if you use these abilities, the class is not 'truly' safe, as you could
// theoretically try to construct an object held by a 'NULL' grin_ptr using the
// copy constructor, and it would blow up terribly.  Therefore, the end-programmer
// must keep this in mind whenever copying grin_ptr objects.
//
// For this reason, I have placed this into the rmw namespace, to differentiate
// it from Alan's original work.
//
// David O'Neil
// ************

/** \file arg_grin_ptr.h
*
*   grin_ptr<> - Provides support for "Cheshire Cat" idiom
*
*   Modification History
*< PRE>
*   06-Apr-00 : Reworked documentation following user feedback  Alan Griffiths
*   04-Nov-99 : Revised copyright notice						Alan Griffiths
*   21_Sep-99 : Original version                                Alan Griffiths
*< /PRE>
**/


/**
*   arglib: A collection of utilities. (E.g. smart pointers).
*   Copyright (C) 1999, 2000 Alan Griffiths.
*
*   This code is part of the 'arglib' library. The latest version of
*   this library is available from:
*
*      http:www.octopull.demon.co.uk/arglib/
*
*   This code may be used for any purpose provided that:
*
*   1. This complete notice is retained unchanged in any version
*       of this code.
*
*   2. If this code is incorporated into any work that displays
*       copyright notices then the copyright for the 'arglib'
*       library must be included.
*
*   This library is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
*   @author alan@octopull.demon.co.uk
**/
namespace rmw
{
   /**
   *  Pointer type to support "Cheshire Cat" idiom - only the indicated
   *  constructor requires access to the complete pointee type.
   *
   *  A const-qualified smart pointer that takes ownership of the
   *  referenced object and implements copy construction/assignment by
   *  copying the referenced object.
   *
   *  The correct algorithm for copying is resolved by using the
   *  arg::deep_copy() template.  This means that the referenced object
   *  will be  copied using the copy construction unless it is tagged
   *  by inheriting from arg::cloneable or an overload of arg::deep_copy()
   *  is provided.
   *
   *  All methods support the strong exception safety guarantee with the
   *  proviso that the referenced type must have a non-throwing destructor.
   *
   *                                               @author Alan Griffiths
   **/
	template<typename p_type>
	class grin_ptr
	{
		///
        typedef void (*delete_ftn)(p_type* p);
		
		///
        typedef p_type* (*copy_ftn)(const p_type* p);

	public:
	//  Construction & assignment

		/// Construct owining p - p_type must be a complete type
		explicit grin_ptr(p_type* pointee)
			: do_copy(&my_copy_ftn), p(pointee), do_delete(my_delete_ftn)
            {
            // "sizeof(p_type)" will force a diagnostic for an incomplete type
        	    sizeof(p_type);
            }
		
		grin_ptr()  //Allow to hold a NULL default parameter (Added by David O'Neil)
			: do_copy(&my_copy_ftn), p(NULL), do_delete(my_delete_ftn) { }

		/// Makes a copy of the object referenced by rhs
		grin_ptr(const grin_ptr& rhs);

		/// Destroys the referenced object
		~grin_ptr() throw()              { do_delete(p); }

	
	//  Accessors - (overloaded on const)

		/// Return contents (const)
		const p_type* get() const        { return p; }
	
		/// Return contents (non-const)
		p_type* get()                    { return p; }
	
		/// Indirection operator (const)
		const p_type* operator->() const { return p; }
	
		/// Indirection operator (non-const)
		p_type* operator->()             { return p; }

      void reset(p_type * ptr = NULL) { do_delete(p); p = ptr; } //Added by David O'Neil

      void release() { p = NULL; } //Give programmer the option.  Added by David O'Neil

		/// Dereference operator (const)
		const p_type& operator*() const	 { return *p; }

		/// Dereference operator (non-const)
		p_type& operator*()              { return *p; }

	
	//  Mutators

		/// Swaps contents with "with"
		void swap(grin_ptr& with) throw()
			{ p_type* pp = p; p = with.p; with.p = pp; }
		
		/// Makes a copy of the object referenced by rhs (destroys old)
		grin_ptr& operator=(const grin_ptr& rhs);

	
	private:
		copy_ftn	do_copy;
		p_type*		p;
		delete_ftn	do_delete;

        static void my_delete_ftn(p_type* p)
        {
        	delete p;
        }
		
        static p_type* my_copy_ftn(const p_type* p)
        {
        	return arg::deep_copy(p);
        }
	};


	template<typename p_type>
    inline grin_ptr<p_type>::grin_ptr(const grin_ptr& rhs)
	:
		do_copy(rhs.do_copy),
		p(do_copy(rhs.p)),
		do_delete(rhs.do_delete)
	{
	}
        
	template<typename p_type>
	inline grin_ptr<p_type>& grin_ptr<p_type>::operator=(const grin_ptr& rhs)
	{
		p_type* pp = do_copy(rhs.p);
		do_delete(p);
		p = pp;
		return *this;

	}
}

namespace std
{
	/// Overload swap algorithm to provide an efficient, non-throwing version
    template<class p_type>
	inline void swap(
		::rmw::grin_ptr<p_type>& lhs,
		::rmw::grin_ptr<p_type>& rhs) throw()
	{
		lhs.swap(rhs);
	}
}
#endif



~MyXa~ wrote:
It's still useful?


Because of those changes, yes, it is still useful to me. Let me know if you delve into it more, and see something I missed. Also, note the comment I placed at the top of the code, pointing out a potential caveat. If you create solutions to your other problems using this code, post them, and I'll change this.

David

ps - do you have a pointer that you would recommend to use to overcome all of the limitations you pointed out?

Debugging - The high art and magic of cussing errors into 'features'

-- modified at 12:17 Thursday 27th October, 2005 (damn '<'s!)
GeneralGreat article Pin
whatever200020-Oct-05 4:25
whatever200020-Oct-05 4:25 
GeneralRe: Great article Pin
David O'Neil20-Oct-05 12:15
professionalDavid O'Neil20-Oct-05 12:15 
GeneralNot a good idea to put all globals in one place Pin
John W Wilkinson10-Oct-05 21:05
John W Wilkinson10-Oct-05 21:05 
GeneralRe: Not a good idea to put all globals in one place Pin
David O'Neil10-Oct-05 23:05
professionalDavid O'Neil10-Oct-05 23:05 
GeneralRe: Not a good idea to put all globals in one place Pin
David O'Neil11-Oct-05 6:33
professionalDavid O'Neil11-Oct-05 6:33 
GeneralRe: Not a good idea to put all globals in one place Pin
John W Wilkinson11-Oct-05 20:42
John W Wilkinson11-Oct-05 20:42 
GeneralRe: Not a good idea to put all globals in one place Pin
David O'Neil12-Oct-05 22:36
professionalDavid O'Neil12-Oct-05 22:36 
GeneralNice article. Pin
raindog9-Oct-05 7:33
raindog9-Oct-05 7:33 
GeneralRe: Nice article. Pin
David O'Neil9-Oct-05 14:24
professionalDavid O'Neil9-Oct-05 14:24 
GeneralI believe I use auto_ptrs in this way all the time. Pin
John M. Drescher7-Oct-05 7:18
John M. Drescher7-Oct-05 7:18 
GeneralRe: I believe I use auto_ptrs in this way all the time. Pin
David O'Neil7-Oct-05 12:56
professionalDavid O'Neil7-Oct-05 12:56 
GeneralRe: I believe I use auto_ptrs in this way all the time. Pin
John M. Drescher7-Oct-05 14:03
John M. Drescher7-Oct-05 14:03 
GeneralRe: I believe I use auto_ptrs in this way all the time. Pin
David O'Neil7-Oct-05 17:21
professionalDavid O'Neil7-Oct-05 17:21 
GeneralRe: I believe I use auto_ptrs in this way all the time. Pin
David O'Neil8-Oct-05 11:46
professionalDavid O'Neil8-Oct-05 11:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.