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:
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:
Retain<Foo> foo1 = fooMaker["id 001"]; Retain<Foo> foo2 = fooMaker["id 001"]; Retain<Foo> foo3 = foo2; cout << foo1->name << " has " << foo1.use_count() << " references" << endl;
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
:
Observer<Foo> fooObserver = foo1;
cout << fooObserver->name << " has " << fooObserver.use_count() << " references" << endl;
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:
class Foo
{
friend SmartMap<string,Foo>;
Foo(string n):name(n){}
public:
string name;
};
SmartMap<string,Foo> fooMaker;
Observer<Foo> fooObserver;
{
Retain<Foo> foo1 = fooMaker["id 001"]; Retain<Foo> foo2 = fooMaker["id 001"]; Retain<Foo> foo3 = foo2;
cout << foo1->name << " has " <<
foo1.use_count() << " references" << endl;
fooObserver = foo1;
cout << fooObserver->name << " has " <<
fooObserver.use_count() << " references" << endl;
{
Retain<Foo> foobar = fooMaker["id 999"];
fooObserver = foobar;
cout << fooObserver->name << " has " <<
fooObserver.use_count() << " references" << endl;
foobar = foo1;
cout << "fooObserver expired? " << std::boolalpha <<
fooObserver.expired() << " and has " <<
fooObserver.use_count() << " references" << endl;
fooObserver = foo1;
cout << fooObserver->name << " has " <<
fooObserver.use_count() << " references" << endl;
}
fooObserver = foo1;
cout << fooObserver->name << " has " <<
fooObserver.use_count() << " references" << endl;
}
cout << "fooObserver expired? " << std::boolalpha <<
fooObserver.expired() << " and has " <<
fooObserver.use_count() << " references" << endl;
Now to look inside the SmartMap
class
There is a single data member observers
which is a std::map
of all the objects created.
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:
Retain<T> operator[](Key key)
{
auto it = observers.find(key);
if(it != observers.end())
{
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...
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:
#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())
{
Retain<T> foundT = (*it).second.lock();
if(foundT)
return foundT;
}
Retain<T> newT = Retain<T>(new T(key));
observers[key] = newT;
return newT;
}
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.