Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
Any static member object inside a class is an
expression of Singleton: one and only one will be made. So in a sense, the
language has direct support for the idea; we certainly use it on a regular
basis. However, there s a problem with static objects (member or not):
the order of initialization, as described in Volume 1 of this book. If one static
object depends on another, it s important that the objects are initialized in
the correct order.
In Volume 1, you were shown how to control initialization order by defining a static object inside a function. This delays the initialization
of the object until the first time the function is called. If the function
returns a reference to the static object, it gives you the effect of a
Singleton while removing much of the worry of static initialization. For
example, suppose you want to create a log file upon the first call to a
function that returns a reference to that log file. This header file will do
the trick:
//: C10:LogFile.h
#ifndef LOGFILE_H
#define LOGFILE_H
#include <fstream>
std::ofstream& logfile();
#endif // LOGFILE_H ///:~
The implementation must not be inlined because that
would mean that the whole function, including the static object definition
within, could be duplicated in any translation unit where it s included, which
violates C++ s one-definition rule. This
would most certainly foil the attempts to control the order of initialization
(but potentially in a subtle and hard-to-detect fashion). So the implementation
must be separate:
//: C10:LogFile.cpp {O}
#include "LogFile.h"
std::ofstream& logfile() {
static std::ofstream log("Logfile.log");
return log;
} ///:~
Now the log object will not be initialized until the
first time logfile( ) is called. So if you create a function:
//: C10:UseLog1.h
#ifndef USELOG1_H
#define USELOG1_H
void f();
#endif // USELOG1_H ///:~
that uses logfile( ) in its implementation:
//: C10:UseLog1.cpp {O}
#include "UseLog1.h"
#include "LogFile.h"
void f() {
logfile() << __FILE__ << std::endl;
} ///:~
And you use logfile( ) again in another file:
//: C10:UseLog2.cpp
//{L} LogFile UseLog1
#include "UseLog1.h"
#include "LogFile.h"
using namespace std;
void g() {
logfile() << __FILE__ << endl;
}
int main() {
f();
g();
} ///:~
the log object doesn t get created until the first
call to f( ).
You can easily combine the creation of the static object
inside a member function with the Singleton class. SingletonPattern.cpp can be modified to use this approach:
//: C10:SingletonPattern2.cpp
// Meyers Singleton.
#include <iostream>
using namespace std;
class Singleton {
int i;
Singleton(int x) : i(x) { }
void operator=(Singleton&);
Singleton(const Singleton&);
public:
static Singleton& instance() {
static Singleton s(47);
return s;
}
int getValue() { return i; }
void setValue(int x) { i = x; }
};
int main() {
Singleton& s = Singleton::instance();
cout << s.getValue() << endl;
Singleton& s2 = Singleton::instance();
s2.setValue(9);
cout << s.getValue() << endl;
} ///:~
An especially interesting case occurs if two Singletons
depend on each other, like this:
//: C10:FunctionStaticSingleton.cpp
class Singleton1 {
Singleton1() {}
public:
static Singleton1& ref() {
static Singleton1 single;
return single;
}
};
class Singleton2 {
Singleton1& s1;
Singleton2(Singleton1& s) : s1(s) {}
public:
static Singleton2& ref() {
static Singleton2 single(Singleton1::ref());
return single;
}
Singleton1& f() { return s1; }
};
int main() {
Singleton1& s1 = Singleton2::ref().f();
} ///:~
When Singleton2::ref( ) is called, it causes its
sole Singleton2 object to be created. In the process of this creation, Singleton1::ref( )
is called, and that causes the sole Singleton1 object to be created.
Because this technique doesn t rely on the order of linking or loading, the
programmer has much better control over initialization, leading to fewer
problems.
Yet another variation on Singleton separates the
Singleton-ness of an object from its implementation. This is achieved using
the Curiously Recurring Template Pattern mentioned in Chapter 5:
//: C10:CuriousSingleton.cpp
// Separates a class from its Singleton-ness (almost).
#include <iostream>
using namespace std;
template<class T> class Singleton {
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
protected:
Singleton() {}
virtual ~Singleton() {}
public:
static T& instance() {
static T theInstance;
return theInstance;
}
};
// A sample class to be made into a Singleton
class MyClass : public Singleton<MyClass> {
int x;
protected:
friend class Singleton<MyClass>;
MyClass() { x = 0; }
public:
void setValue(int n) { x = n; }
int getValue() const { return x; }
};
int main() {
MyClass& m = MyClass::instance();
cout << m.getValue() << endl;
m.setValue(1);
cout << m.getValue() << endl;
} ///:~
MyClass is made a Singleton by:
1. Making
its constructor private or protected.
2. Making
Singleton<MyClass> a friend.
3. Deriving
MyClass from Singleton<MyClass>.
The self-referencing in step 3 may sound implausible, but as
we explained in Chapter 5, it works because there is only a static dependency
on the template argument in the Singleton template. In other words, the
code for the class Singleton<MyClass> can be instantiated by the
compiler because it is not dependent on the size of MyClass. It s only
later, when Singleton<MyClass>::instance( ) is first called,
that the size of MyClass is needed, and by then MyClass has been
compiled and its size is known.
It s interesting how intricate such a simple pattern as
Singleton can be, and we haven t even addressed issues of thread safety. Finally,
Singleton should be used sparingly. True Singleton objects arise rarely, and
the last thing a Singleton should be used for is to replace a global variable.
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |