Confessions of a Wall Street Programmer

practical ideas (and perhaps some uncommon knowledge) on software architecture, design, construction and testing

Shared Singleton's

Singleton’s are kind of like Rodney Dangerfield – they don’t get no respect. And yet, there are scenarios where a singleton is just the ticket – for instance, when a relatively expensive and/or long-lived resource needs to be shared among a number of independent threads.

shared_ptr’s can help make this easier and less error-prone, but even so there are some edge cases that need to be considered.

In our case, we had a need for a singleton object to refer to a middleware connection which was being shared across a large number of child objects, which were in turn being manipulated on a number of different threads.

In our case, we also wanted lazy initialization (construct on first use), and to clean up the singleton properly when the code was done using it. Since the creation and destruction order of the using objects is not deterministic, the obvious solution is to use reference counting to manage the singleton’s lifetime, and the simplest way to do that is to use shared_ptr’s.

shared_ptr’s were introduced in Boost, and were subsequently adopted by the C++ standards committee as part of the base language with the tr1 and C++0x standards. As such, they have been widely supported (think gcc and MS compilers) for a while now.

One of the “magic” qualities of shared_ptr’s is that all shared_ptr’s that point to the same thing (what I’m going to refer to as a “target” from here on) share a single reference count (hence the “shared” part of the name), so as individual shared_ptr’s go out of scope, the reference counter on the target itself is automatically decremented, and the target is deleted when its reference count goes to zero.

However, as we’ll see, there are some fine points that need to be considered even in this relatively simple case.

Our first attempt looked something like this:

(smart1.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
using namespace std;
#include <tr1/memory>
using namespace std::tr1;

struct SharedClass
{
   SharedClass();
   ~SharedClass();

};

SharedClass::SharedClass()
{
   cout << "SharedClass ctor:" << this << endl;
}

SharedClass::~SharedClass()
{
   cout << "SharedClass dtor:" << this << endl;
}

struct ContainerClass
{
   ContainerClass();
   ~ContainerClass();

	shared_ptr<SharedClass> getShared();

	shared_ptr<SharedClass> memberPtr;

	static shared_ptr<SharedClass>  masterPtr;
};

shared_ptr<SharedClass>   ContainerClass::masterPtr;

shared_ptr<SharedClass> ContainerClass::getShared()
{
   if (!masterPtr.get()) {
      masterPtr = shared_ptr<SharedClass>(new SharedClass());
   }

   return masterPtr;
}


ContainerClass::ContainerClass()
{
   cout << "ContainerClass ctor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   memberPtr = getShared();

   cout << "\tafter\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;
   cout << endl;
}

ContainerClass::~ContainerClass()
{
   cout << "ContainerClass dtor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   cout << endl;
}

int main(int argc, char** argv)
{
   cout << "Entering main" << endl;

   ContainerClass* pClass1 = new ContainerClass();
   ContainerClass* pClass2 = new ContainerClass();

   delete pClass1;
   delete pClass2;

   ContainerClass* pClass3 = new ContainerClass();
   delete pClass3;

   cout << "Exiting main" << endl;
}

The implementation constructs the singleton into a class-static shared_ptr on the first call to getShared(), and then hands out copies of the shared_ptr to each caller. When run, the following output is produced:

(smart1.out) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Entering main
ContainerClass ctor:0x154b9010
	before	copy: 0	master: 0
SharedClass ctor:0x154b9030
	after	copy: 2	master: 2

ContainerClass ctor:0x154b90a0
	before	copy: 0	master: 2
	after	copy: 3	master: 3

ContainerClass dtor:0x154b9010
	before	copy: 3	master: 3

ContainerClass dtor:0x154b90a0
	before	copy: 2	master: 2

ContainerClass ctor:0x154b90a0
	before	copy: 0	master: 1
	after	copy: 2	master: 2

ContainerClass dtor:0x154b90a0
	before	copy: 2	master: 2

Exiting main
SharedClass dtor:0x154b9030

This implementation works, but with one major caveat: as you can see from the output, the shared_ptr’s get an initial reference count of two – one for the creation of the singleton itself, and one for the copy of the shared_ptr that is returned from getShared.

This means that the singleton will not actually be deleted until after returning from main(), which is when static objects get deleted. (For more information on why this is so, see section 3.6.3 of the C++ standard).1 In many cases, that would not be a problem, but in our case the middleware connection to which the shared_ptr’s refer gets cleaned up in the main thread prior to returning, and so the final destructor referred to an object that was no longer valid.

Another issue has to do with RAII 2 – if you’re relying on the destruction of the target to release resources, the fact that the target is effectively never deleted is a potential problem.

Last but not least is the issue of overall tidiness – given that the shared object is allocated within the scope of main(), it just seems wrong to let it go out of scope after main() exits. Ideally, we’d like to destroy the shared object prior to returning from main(), but how to do that?

Our next attempt looks like this:

(smart2.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <iostream>
using namespace std;
#include <tr1/memory>
using namespace std::tr1;

struct SharedClass
{
   SharedClass();
   ~SharedClass();

};

SharedClass::SharedClass()
{
   cout << "SharedClass ctor:" << this << endl;
}

SharedClass::~SharedClass()
{
   cout << "SharedClass dtor:" << this << endl;
}

struct ContainerClass
{
   ContainerClass();
   ~ContainerClass();

	shared_ptr<SharedClass> getShared();

	shared_ptr<SharedClass> memberPtr;

	static shared_ptr<SharedClass>  masterPtr;
};

shared_ptr<SharedClass>   ContainerClass::masterPtr;

shared_ptr<SharedClass> ContainerClass::getShared()
{
   if (!masterPtr.get()) {
      masterPtr = shared_ptr<SharedClass>(new SharedClass());
   }

   return masterPtr;
}


ContainerClass::ContainerClass()
{
   cout << "ContainerClass ctor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   memberPtr = getShared();

   cout << "\tafter\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;
   cout << endl;
}

ContainerClass::~ContainerClass()
{
   cout << "ContainerClass dtor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   memberPtr.reset();
   if (masterPtr.unique())
      masterPtr.reset();

   cout << "\tafter\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;
   cout << endl;
}

int main(int argc, char** argv)
{
   cout << "Entering main" << endl;

   ContainerClass* pClass1 = new ContainerClass();
   ContainerClass* pClass2 = new ContainerClass();

   delete pClass1;
   delete pClass2;

   ContainerClass* pClass3 = new ContainerClass();
   delete pClass3;

   cout << "Exiting main" << endl;
}

In the destructor for the container class, we first reset() the container’s copy of the shared_ptr. This reduces its reference count to zero, and also decrements the reference counter on the static shared_ptr that was initialized when the singleton was created. (The destructor of the container’s copy of the shared_ptr will still be executed on return from the container’s destructor, but since the reference count is already zero, it will do nothing).

Then we test the static shared_ptr to see if it’s reference count is equal to one (by calling unique()), and if it is we call reset on it to decrement its reference count once again, which ends up deleting the singleton (since the reference count is now zero).

When we run this version of the code, we see the following output:

(smart2.out) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Entering main
ContainerClass ctor:0x642f010
	before	copy: 0	master: 0
SharedClass ctor:0x642f030
	after	copy: 2	master: 2

ContainerClass ctor:0x642f0a0
	before	copy: 0	master: 2
	after	copy: 3	master: 3

ContainerClass dtor:0x642f010
	before	copy: 3	master: 3
	after	copy: 0	master: 2

ContainerClass dtor:0x642f0a0
	before	copy: 2	master: 2
SharedClass dtor:0x642f030
	after	copy: 0	master: 0

ContainerClass ctor:0x642f0a0
	before	copy: 0	master: 0
SharedClass ctor:0x642f030
	after	copy: 2	master: 2

ContainerClass dtor:0x642f0a0
	before	copy: 2	master: 2
SharedClass dtor:0x642f030
	after	copy: 0	master: 0

Exiting main

We can see that the singleton does in fact get destroyed prior to returning from main(), which is what we want. We also see that the singleton gets destroyed when nobody is using it, and automatically gets recreated if needed, which is all well and good.

But, the code to make this happen does seem a bit messy, and potentially unclear. Surely, there’s a better way to get the behavior we want without having to directly manipulate reference counts.

Which brings us to shared_ptr’s cousin, the weak_ptr. A weak_ptr is similar to a shared_ptr, except that assigning to a weak_ptr does not increment the shared reference count:

(smart3.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
using namespace std;
#include <tr1/memory>
using namespace std::tr1;

struct SharedClass
{
   SharedClass();
   ~SharedClass();

};

SharedClass::SharedClass()
{
   cout << "SharedClass ctor:" << this << endl;
}

SharedClass::~SharedClass()
{
   cout << "SharedClass dtor:" << this << endl;
}

struct ContainerClass
{
   ContainerClass();
   ~ContainerClass();

	shared_ptr<SharedClass> getShared();

	shared_ptr<SharedClass> memberPtr;

	static weak_ptr<SharedClass>  masterPtr;
};

weak_ptr<SharedClass>   ContainerClass::masterPtr;

shared_ptr<SharedClass> ContainerClass::getShared()
{
   shared_ptr<SharedClass> temp = masterPtr.lock();
   if (!temp) {
      temp.reset(new SharedClass());
      masterPtr = temp;
   }

   return temp;
}


ContainerClass::ContainerClass()
{
   cout << "ContainerClass ctor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   memberPtr = getShared();

   cout << "\tafter\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;
   cout << endl;
}

ContainerClass::~ContainerClass()
{
   cout << "ContainerClass dtor:" << this << endl;
   cout << "\tbefore\tcopy: " << memberPtr.use_count() << "\t" << "master: " << masterPtr.use_count() << endl;

   cout << endl;
}

int main(int argc, char** argv)
{
   cout << "Entering main" << endl;

   ContainerClass* pClass1 = new ContainerClass();
   ContainerClass* pClass2 = new ContainerClass();

   delete pClass1;
   delete pClass2;

   ContainerClass* pClass3 = new ContainerClass();
   delete pClass3;

   cout << "Exiting main" << endl;
}

In this version, we change the code to use a weak_ptr instead of a shared_ptr for the static class member that is initialized on creation of the singleton object, and we remove the fiddling with reference counts that we had to do make things come out right in the previous example.

(smart3.out) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Entering main
ContainerClass ctor:0x18bf8010
	before	copy: 0	master: 0
SharedClass ctor:0x18bf8030
	after	copy: 1	master: 1

ContainerClass ctor:0x18bf80a0
	before	copy: 0	master: 1
	after	copy: 2	master: 2

ContainerClass dtor:0x18bf8010
	before	copy: 2	master: 2

ContainerClass dtor:0x18bf80a0
	before	copy: 1	master: 1

SharedClass dtor:0x18bf8030
ContainerClass ctor:0x18bf80a0
	before	copy: 0	master: 0
SharedClass ctor:0x18bf8030
	after	copy: 1	master: 1

ContainerClass dtor:0x18bf80a0
	before	copy: 1	master: 1

SharedClass dtor:0x18bf8030
Exiting main

Et voila – we can see that the behavior is exactly what we wanted, but without the need to manipulate reference counts directly. Not to mention, the behavior is a bit more like what one would expect (e.g., member variables get destroyed on exit from the destructor, not in the body of the destructor).

Acknowledgements

Thanks to the folks at cplusplus.com, especially “simeonz” for this post, which discusses the general problem and provides a nice code example that I used as the basis for this discussion.

  1. A free copy of the most recent working draft can be downloaded here.

  2. http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Comments