Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VC10.0

Object Creation using Smart Pointers and Map

4.50/5 (2 votes)
6 Dec 2012CPOL5 min read 18.3K   88  
Using SmartMap to create objects that will clean up after themselves

Introduction

I needed a class that would create a Direct3D object (e.g.. texture, mesh, shader) from its file name if it didn't already exist, and if it did exist just return the instance of it. Doesn't sound too hard now, does it? Well, there are a few extra things that this class needed to do, such as destroy the Direct3D object's instance when there were no more references to it. Also, since I think the keywords 'new' and 'delete' make the code look outdated, it will need to use smart pointers too. On solving this problem, I realised that the solution could be useful in many different scenarios so I have decided to share with everyone the concept of SmartMap, when I say everyone I mean the 3 people that will bother reading this article. I have been agonizing over what to name it such as SmartFactory, MagicMap, MallocMap but SmartMap was the best I could do, but I am open to suggestions for a better fitting name.

Using the Code

I named it SmartMap because it uses smart pointers and it uses std::map underneath the hood, what is not represented is the fact that it allocates memory. This is better explained with an example:

C++
SmartMap<string,Foo> fooMaker;  

Like std::map, this has a key/value pair, string being the key and Foo being the type that is stored. It also uses array notation for access, but that is about where the similarities end as fooMaker["id 001"] returns a shared_ptr.

I have defined std::shared_ptr and std::weak_ptr to Retain (to use an iOS parlance) and Observer respectively as it better describes what I'm trying to achieve here. Let's take a look at the next example:

C++
Retain<Foo> foo1 = fooMaker["id 001"];  // creates a Foo with key "id 001"
Retain<Foo> foo2 = fooMaker["id 001"];  // doesn't need to create a Foo as it already exists
Retain<Foo> foo3 = foo2;                // now we have 3 references to the same Foo
cout << foo1->name << " has " << foo1.use_count() << " references" << endl;
C++
output: "id 001 has 3 references"

When the first fooMaker["id 001"] was accessed, there wasn't a Foo inside fooMaker that had the key "id 001" so a new Foo was created and returned. When the second fooMaker["id 001"] was accessed, it didn't need to create another Foo as one already existed with that key.

Let's take a closer look at Retain. foo1, foo2 and foo3 are all of type Retain<Foo> and if you are not familiar with std::shared_ptr, all this means is that when all 3 of them go out of scope then (and only then) the Foo that they all point to is destroyed. Now that you understand Retain, let's have a look at Observer:

C++
Observer<Foo> fooObserver = foo1;
cout << fooObserver->name << " has " << fooObserver.use_count() << " references" << endl;
C++
output: "id 001 has 3 references"

Observer<Foo> is simply a std::weak_ptr, which basically means it doesn't have any baring on the reference count so it doesn't influence when the object is destroyed. As you can see, we still have 3 references even though we've added an observer to the mix. As foo1, foo2 and foo3 all reference the same object fooObserver=foo1 is exactly the same as fooObserver=foo2

Look at the comments in the code snippet below to get a better idea of how memory allocation and destruction is handled:

C++
class Foo
{
   friend SmartMap<string,Foo>;
   // private constructor to ensure that Foo objects can only be created by
   // SmartMap<string,Foo> (notice it's a friend) which enforces 
   // smart pointers to avoid memory leaks 
   Foo(string n):name(n){}
public:
   string name;
}; 
C++
SmartMap<string,Foo> fooMaker;
Observer<Foo> fooObserver;
{
   Retain<Foo> foo1 = fooMaker["id 001"]; // creates a Foo with key "id 001"
   Retain<Foo> foo2 = fooMaker["id 001"]; // doesn't need to create a Foo as it already exists
   Retain<Foo> foo3 = foo2;               // now we have 3 references to the same Foo

   cout << foo1->name << " has " << 
   	foo1.use_count() << " references" << endl;
   // foo1, foo2, foo3 all have a reference count of 3 as they all contain the same Foo
   // output: "id 001 has 3 references"

   fooObserver = foo1;
   cout << fooObserver->name << " has " << 
   	fooObserver.use_count() << " references" << endl;
   // notice that we still have 3 references even though we've added an observer
   // that's because the observer has no baring on the reference count
   // output: "id 001 has 3 references"

   {
      Retain<Foo> foobar = fooMaker["id 999"];
      fooObserver = foobar;
      cout << fooObserver->name << " has " << 
      fooObserver.use_count() << " references" << endl;
      // the foo observer is now observing a foobar and reports that it has only 1 reference
      // output: "id 999 has 1 references"

      foobar = foo1;
      cout << "fooObserver expired? " << std::boolalpha << 
      	fooObserver.expired() << " and has " << 
      	fooObserver.use_count() << " references" << endl;
 
      // fooObserver is still pointing to a Foo with the key "id 999" but foobar is 
      // now pointing to "id 001", 
      // so the Foo with the key "id 999" has gone out of scope and 
      // deleted itself (expired). Remember, just because fooObserver is still pointing to 
      // it doesn't mean it is still in scope, because observers have no baring on the reference count
      // output: "fooObserver expired? yes and has 0 references"

      fooObserver = foo1;
      cout << fooObserver->name << " has " << 
      fooObserver.use_count() << " references" << endl;
      // since foobar has been added to the Foo with the 
      // key "id 001" we now have a reference count of 4
      // output: "id 001 has 4 references"
   } 
 
   fooObserver = foo1;
   cout << fooObserver->name << " has " << 
   fooObserver.use_count() << " references" << endl;
   // foobar is now out of scope so the Foo with the key "id 001" has dropped 1 reference
   // output: "id 001 has 3 references"
}
 
cout << "fooObserver expired? " << std::boolalpha << 
fooObserver.expired() << " and has " << 
	fooObserver.use_count() << " references" << endl;
// all the foos have gone out of scope so the Foo with the key of "id 001" has been deleted
// output: "fooObserver expired? yes and has 0 references" 

Now to look inside the SmartMap class

There is a single data member observers which is a std::map of all the objects created.

C++
std::map<Key,Observer<T> > observers;  

Notice it is a collection of Observer<T> not Retain<T>, this is because we don't want to influence the reference count, we want the Observer<T> to expire when there are no more Retain<T> references to it. This causes slight problem, if there are lots of different keys then the map might get bloated with expired entries. In cases like this, you can call flushExpired() which removes all expired observers;

Now for the function that does all the work:

C++
Retain<T> operator[](Key key)
{
   auto it = observers.find(key);
   if(it != observers.end()) 
   { 
      //thread safe version
      Retain<T> foundT = (*it).second.lock();
      if(foundT)			
         return foundT;
   }
	
   Retain<T> newT = Retain<T>(new T(key));
		
   observers[key] = newT;
   return newT;
} 

First it checks to see if an observer has already been created using the key, remember that the same key will return the same object. If no observer has been found, then the line...

C++
Retain<T> newT = Retain<T>(new T(key)); 

...creates a new object of type <code><code>T with key as a constructor parameter. Now of course this is just a matter of preference, you can change the parameter to however you see fit, but I found that created objects need the key as it's the differentiating factor between them.

A new object will also need to be created if an observer has been found and it has expired, this means that all references that belong to this key have gone out of scope. If an Observer<T> has been found that is not expired, then we can create a Retain<T> from it by calling lock().

Once created, the next step is to store an observer to it by: observers[key]=newT; that's all there is to it.

The entire SmartMap code:

C++
#pragma once
 
#include <map>
#include <memory>
 
#define Retain std::shared_ptr
#define Observer std::weak_ptr
 
template < typename Key, typename T >
class SmartMap
{
protected:
   std::map<Key,Observer<T> > observers;
	
public:
   Retain<T> operator[](Key key)
   {
      auto it = observers.find(key);
      if(it != observers.end()) 
      { 
         //thread safe version
         Retain<T> foundT = (*it).second.lock();
         if(foundT)			
            return foundT;
      }	
      Retain<T> newT = Retain<T>(new T(key));
		
      observers[key] = newT;
      return newT;
   }
 
   // This may need to be called if the map has a lot of expired entries
   void flushExpired()
   {
      for(auto it = observers.begin(); it!= observers.end(); )
      {
         if((*it).second.expired())
            observers.erase(it++);
         else
            it++;
      }
   }
};  

Notice it is a surprisingly tiny amount of code which leaves it to be easily customised for a particular problem that you may have. Remember at the start of this article, I mentioned Direct3D objects that I needed to manage, well I modified SmartMap to take a pointer to IDirect3DDevice9 in its constructor as all Direct3D objects it creates need a device.

Conclusion

I hope you have found this little trick as useful as I have. This is my first article and I was going to write about using Qt with DirectShow and Direct3D with a little MongoDB thrown in. I'm glad I cut my teeth on something like this as I'll need feedback to improve my writing before I can tackle something more complex.

The primary reason I wrote this is to improve as a programmer so I welcome feedback, though I am bracing myself for some harsh comments as I have seen on other articles, even ones I thought were quite good. Thank you for reading to the end.

License

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