Singleton and object lifetime
This article is a continuation of my first article, “Using the Singleton Pattern” [0] . At first I wanted to set out everything related to the lifetime in this article, but the volume of material turned out to be large, so I decided to break it into several parts. This is the continuation of a series of articles about the use of various templates and techniques. This article focuses on the lifetime and development of the use of singleton. Before reading the second article, it is strongly recommended that you read my first article [0] .
In a previous article, the following implementation for singleton was used:
Function single returned to us treasured singleton. However, this approach has a flaw: in this case, we do not control the lifetime of the object and it can retire at the moment when we want to use this object. Therefore, you should use another mechanism for creating an object using the new operator.
It just so happened that in C ++ there is no garbage collector, so you need to monitor the creation and destruction of the object. And although this problem has long been known and even methods to solve it are clear, such errors are not a rare guest in our programs. In general, we can distinguish the following types of errors that programmers make:
As a result of such errors, the program either starts to “leak”, or starts to behave unpredictably, or simply “crashes”. You can, of course, talk about what is worse, but one thing is clear: such errors are quite serious.
Using the example of a singleton, you can easily show how such errors are made. We open the Wikipedia article [1] and find the implementation for C ++:
It can be seen that for a singleton, memory is allocated, but for some reason it is not freed. You can, of course, say that for a singleton this is not a serious problem, since its lifetime coincides with the time of the program. However, there is a number but:
You can, of course, say that these arguments are irrelevant. However, let's still do it the way it should. I always use the following principle for working with objects: the created object must be destroyed . And it doesn’t matter whether it is singleton or not, it is a general rule with no exceptions, which sets a certain quality of the program.
Analyzing the source code of various software products for myself, I identified 2 more important rules:
It is worth a little clarification of what I mean. It is clear that somewhere new and delete will be called anyway. The point is that this should be strictly in one place, it is better in one class, so as not to spray it on the program. Then, with the proper organization of this class, it will not be necessary to monitor the lifetime of objects. And I’ll say right away that this is possible! It is worth mentioning right away that I have never encountered such an approach. So we will be a kind of discoverers.
Fortunately, C ++ has a great tool called a smart pointer. Their cleverness is that, although they behave like ordinary pointers, they also control the lifetime of objects. To do this, they use a counter that independently counts the number of links to the object. When the counter reaches zero, the object is automatically destroyed. We will use the smart pointer from the standard std :: shared_ptr library of the memory header file. It is worth noting that such a class is available for modern compilers that support the C ++ 0x standard. For those using the old compiler, boost :: shared_ptr can be used. Their interfaces are absolutely identical.
We assign to our class An the following duties:
The following implementation satisfies these conditions:
It is worthwhile to dwell in more detail on the proposed implementation:
In this case, the singleton will be rewritten as follows:
Helper macros will be like this:
The BIND_TO_IMPL_SINGLE macro has undergone minor changes, which now uses the anSingle function instead of the single function, which, in turn, returns an already filled instance of An. I will talk about other macros later.
Now consider using the described class to implement a singleton:
Now it can be used as follows:
That on the screen will give the number 2, because for implementation, class Y was used.
Consider now an example that shows the importance of using smart pointers for singletones. To do this, we will analyze the following code:
Now let's see what is displayed on this call to the out function:
Let’s see what happens here. At the very beginning, we say that we want an implementation of class B taken from a singleton, so class B is created. Then we call the function out, which takes an implementation of class A from singleton and takes the value a. The value of a is set in constructor A, so the number 1 will appear on the screen. Now the program finishes its work. Objects begin to be destroyed in the reverse order, i.e. first, the class A created by the last is destroyed, and then the class B is destroyed. When class B is destroyed, we again call the out function from Sinton, but since object A is already destroyed, then on the screen we see the inscription -1. Generally speaking, the program could collapse, as we use the memory of an already destroyed object. Thus, this implementation shows that without control of the lifetime, the program can safely fall at the end.
Let's now see how you can do the same, but with control over the lifetime of objects. To do this, we will use our class An:
This code is practically no different from the previous one, except for the following important details:
Let's see what is now displayed on the screen:
As you can see, now we have extended the life of class A and changed the sequence of destruction of objects. The absence of -1 means that the object existed while accessing its data.
On this, the first part of the article, devoted to the lifetime of objects, came to an end. In the next part (or parts), the remaining generative design patterns using the developed functionality will be analyzed and general conclusions will be drawn.
Many people ask, and what, in fact, is the point? Why can't you just make a singleton? Why use some additional constructions that do not add clarity, but only complicate the code. In principle, with a careful reading of the first article [0] , one can already understand that this approach is more flexible and eliminates a number of singleton’s significant shortcomings. In the next article it will be clearly clear why I painted it like that, because it will already be talking not only about singleton. And through the article it will be generally clear that singleton has absolutely nothing to do with it. All I am trying to show is the use of the Dependency inversion principle [4] (see also The Principles of OOD [5]) Actually, it was after the first time I saw this approach in Java that I was disappointed that this is poorly used in C ++ (in principle, there are frameworks that provide similar functionality, but I would like something more lightweight and practical). The given implementation is just a small step in this direction, which is already of great benefit.
I would also like to note a few more things that distinguish the given implementation from the classic singleton (generally speaking, these are consequences, but they are important):
After reading the comments, I realized that there are some points that should be clarified, because many are not familiar with the dependency inversion principle, DIP or inversion of control, IoC. Consider the following example: we have a database that contains the information we need, for example, a list of users:
We have a class that provides the information we need, including the necessary user:
Here we create aDatabase member that says it needs some kind of database. It doesn’t matter for him to know what kind of database it will be, he doesn’t need to know who and when it will fill / fill. But the UserManager class knows that what it needs is poured there. All he says is: “give me the desired implementation, I don’t know which one, and I will do everything you need from this database, for example, provide the necessary information about the user from this database.”
Now we do the trick. Since we have only one database that contains all our information, we say: ok, since there is only one database, let's make a singleton, and so as not to bathe each time in the implementation fill, make the singleton fill itself :
Those. we create an implementation of MyDatabase and say that we will use it for the singleton using the BIND_TO_IMPL_SINGLE macro. Then the following code will automatically use MyDatabase:
Over time, it turned out that we have another database, which also has users, but, say, for another organization:
Of course, we want to use our UserManager, but with a different database. No problems:
And as if by magic, now we take the user from another database! This is a rather crude example, but it clearly shows the principle of dependency handling: this is when the IDatabase implementation is flooded with the UserManager instead of the traditional approach, when the UserManager itself searches for the necessary implementation. In this article, this principle is used, while a singleton for implementation is taken as a special case .
[0] Using the singleton pattern
[1] Wikipedia: singleton
[2] Inside C ++: singleton
[3] Generating patterns: Singleton
[4] Dependency inversion principle
[5] The Principles of OOD
[6] Wikipedia: Rvalue reference and move semantics
In a previous article, the following implementation for singleton was used:
template
T& single()
{
static T t;
return t;
}
Function single returned to us treasured singleton. However, this approach has a flaw: in this case, we do not control the lifetime of the object and it can retire at the moment when we want to use this object. Therefore, you should use another mechanism for creating an object using the new operator.
It just so happened that in C ++ there is no garbage collector, so you need to monitor the creation and destruction of the object. And although this problem has long been known and even methods to solve it are clear, such errors are not a rare guest in our programs. In general, we can distinguish the following types of errors that programmers make:
- Memory usage when an object is not created.
- The memory usage of an already remote object.
- Non-freeing memory occupied by the object.
As a result of such errors, the program either starts to “leak”, or starts to behave unpredictably, or simply “crashes”. You can, of course, talk about what is worse, but one thing is clear: such errors are quite serious.
Using the example of a singleton, you can easily show how such errors are made. We open the Wikipedia article [1] and find the implementation for C ++:
class OnlyOne
{
public:
static OnlyOne* Instance()
{
if(theSingleInstance==0)
theSingleInstance=new OnlyOne;
return theSingleInstance;
}
private:
static OnlyOne* theSingleInstance;
OnlyOne(){};
};
OnlyOne* OnlyOne::theSingleInstance=0;
It can be seen that for a singleton, memory is allocated, but for some reason it is not freed. You can, of course, say that for a singleton this is not a serious problem, since its lifetime coincides with the time of the program. However, there is a number but:
- Memory leak detection programs will always display these leaks for singletones.
- A singleton can be a rather complex object serving an open configuration file, communication with a database, and so on. Incorrect destruction of such objects can cause problems.
- It all starts with a small one: first, we do not monitor the memory for singletones, and then for the rest of the objects.
- And the main question: why do it wrong, if you can do it right?
You can, of course, say that these arguments are irrelevant. However, let's still do it the way it should. I always use the following principle for working with objects: the created object must be destroyed . And it doesn’t matter whether it is singleton or not, it is a general rule with no exceptions, which sets a certain quality of the program.
Analyzing the source code of various software products for myself, I identified 2 more important rules:
- Do not use new.
- Do not use delete.
It is worth a little clarification of what I mean. It is clear that somewhere new and delete will be called anyway. The point is that this should be strictly in one place, it is better in one class, so as not to spray it on the program. Then, with the proper organization of this class, it will not be necessary to monitor the lifetime of objects. And I’ll say right away that this is possible! It is worth mentioning right away that I have never encountered such an approach. So we will be a kind of discoverers.
Smart pointers
Fortunately, C ++ has a great tool called a smart pointer. Their cleverness is that, although they behave like ordinary pointers, they also control the lifetime of objects. To do this, they use a counter that independently counts the number of links to the object. When the counter reaches zero, the object is automatically destroyed. We will use the smart pointer from the standard std :: shared_ptr library of the memory header file. It is worth noting that such a class is available for modern compilers that support the C ++ 0x standard. For those using the old compiler, boost :: shared_ptr can be used. Their interfaces are absolutely identical.
We assign to our class An the following duties:
- Control the lifetime of objects using smart pointers.
- Creating instances, including derived classes, without using the new operators in the calling code.
The following implementation satisfies these conditions:
template
struct An
{
template
friend struct An;
An() {}
template
An(const An& a) : data(a.data) {}
template
An(An&& a) : data(std::move(a.data)) {}
T* operator->() { return get0(); }
const T* operator->() const { return get0(); }
bool isEmpty() const { return !data; }
void clear() { data.reset(); }
void init() { if (!data) reinit(); }
void reinit() { anFill(*this); }
T& create() { return create(); }
template
U& create() { U* u = new U; data.reset(u); return *u; }
template
void produce(U&& u) { anProduce(*this, u); }
template
void copy(const An& a) { data.reset(new U(*a.data)); }
private:
T* get0() const
{
const_cast(this)->init();
return data.get();
}
std::shared_ptr data;
};
It is worthwhile to dwell in more detail on the proposed implementation:
- The constructor uses the move semantics [6] from the C ++ 0x standard to increase copy performance.
- The create method creates an object of the desired class, by default an object of class T is created.
- The produce method creates an object depending on the accepted value. The purpose of this method will be described later.
- The copy method makes a deep copy of the class. It is worth noting that for copying, you must specify the type of a real instance of the class as a parameter, the base type is not suitable.
In this case, the singleton will be rewritten as follows:
template
struct AnAutoCreate : An
{
AnAutoCreate() { create(); }
};
template
T& single()
{
static T t;
return t;
}
template
An anSingle()
{
return single>();
}
Helper macros will be like this:
#define PROTO_IFACE(D_iface, D_an) \
template<> void anFill(An& D_an)
#define DECLARE_IMPL(D_iface) \
PROTO_IFACE(D_iface, a);
#define BIND_TO_IMPL(D_iface, D_impl) \
PROTO_IFACE(D_iface, a) { a.create(); }
#define BIND_TO_SELF(D_impl) \
BIND_TO_IMPL(D_impl, D_impl)
#define BIND_TO_IMPL_SINGLE(D_iface, D_impl) \
PROTO_IFACE(D_iface, a) { a = anSingle(); }
#define BIND_TO_SELF_SINGLE(D_impl) \
BIND_TO_IMPL_SINGLE(D_impl, D_impl)
#define BIND_TO_IFACE(D_iface, D_ifaceFrom) \
PROTO_IFACE(D_iface, a) { anFill(a); }
#define BIND_TO_PROTOTYPE(D_iface, D_prototype) \
PROTO_IFACE(D_iface, a) { a.copy(anSingle()); }
The BIND_TO_IMPL_SINGLE macro has undergone minor changes, which now uses the anSingle function instead of the single function, which, in turn, returns an already filled instance of An. I will talk about other macros later.
Using singleton
Now consider using the described class to implement a singleton:
// header file
struct X
{
X() { x = 1; }
int x;
};
// декларация заливки реализации
DECLARE_IMPL(X)
// cpp file
struct Y : X
{
Y() { x = 2; }
int y;
};
// связывание декларации X и реализации Y используя синглтон
BIND_TO_IMPL_SINGLE(X, Y)
Now it can be used as follows:
An x;
std::cout << x->x << std::endl;
That on the screen will give the number 2, because for implementation, class Y was used.
Life time control
Consider now an example that shows the importance of using smart pointers for singletones. To do this, we will analyze the following code:
struct A
{
A() { std::cout << "A" << std::endl; a = 1; }
~A() { std::cout << "~A" << std::endl; a = -1; }
int a;
};
struct B
{
B() { std::cout << "B" << std::endl; }
~B() { std::cout << "~B" << std::endl; out(); }
void out() { std::cout << single().a << std::endl; }
};
Now let's see what is displayed on this call to the out function:
single().out();
// вывод на экран
B
A
1
~A
~B
-1
Let’s see what happens here. At the very beginning, we say that we want an implementation of class B taken from a singleton, so class B is created. Then we call the function out, which takes an implementation of class A from singleton and takes the value a. The value of a is set in constructor A, so the number 1 will appear on the screen. Now the program finishes its work. Objects begin to be destroyed in the reverse order, i.e. first, the class A created by the last is destroyed, and then the class B is destroyed. When class B is destroyed, we again call the out function from Sinton, but since object A is already destroyed, then on the screen we see the inscription -1. Generally speaking, the program could collapse, as we use the memory of an already destroyed object. Thus, this implementation shows that without control of the lifetime, the program can safely fall at the end.
Let's now see how you can do the same, but with control over the lifetime of objects. To do this, we will use our class An:
struct A { A() { std::cout << "A" << std::endl; a = 1; } ~A() { std::cout << "~A" << std::endl; a = -1; } int a; }; // связывание декларации A с собственной реализацией используя синглтон BIND_TO_SELF_SINGLE(A) struct B { An
a; B() { std::cout << "B" << std::endl; } ~B() { std::cout << "~B" << std::endl; out(); } void out() { std::cout << a->a << std::endl; } }; // связывание декларации B с собственной реализацией используя синглтон BIND_TO_SELF_SINGLE(B) // код An b; b->out();
This code is practically no different from the previous one, except for the following important details:
- Objects A and B use the An class for singletones.
- Class B explicitly declares a dependency on class A using the corresponding public member of the class (more on this approach can be found in the previous article).
Let's see what is now displayed on the screen:
B
A
1
~B
1
~A
As you can see, now we have extended the life of class A and changed the sequence of destruction of objects. The absence of -1 means that the object existed while accessing its data.
Total
On this, the first part of the article, devoted to the lifetime of objects, came to an end. In the next part (or parts), the remaining generative design patterns using the developed functionality will be analyzed and general conclusions will be drawn.
PS
Many people ask, and what, in fact, is the point? Why can't you just make a singleton? Why use some additional constructions that do not add clarity, but only complicate the code. In principle, with a careful reading of the first article [0] , one can already understand that this approach is more flexible and eliminates a number of singleton’s significant shortcomings. In the next article it will be clearly clear why I painted it like that, because it will already be talking not only about singleton. And through the article it will be generally clear that singleton has absolutely nothing to do with it. All I am trying to show is the use of the Dependency inversion principle [4] (see also The Principles of OOD [5]) Actually, it was after the first time I saw this approach in Java that I was disappointed that this is poorly used in C ++ (in principle, there are frameworks that provide similar functionality, but I would like something more lightweight and practical). The given implementation is just a small step in this direction, which is already of great benefit.
I would also like to note a few more things that distinguish the given implementation from the classic singleton (generally speaking, these are consequences, but they are important):
- The singleton class can be used in multiple instances without any restrictions.
- A singleton is implicitly filled in via the anFill function, which controls the number of instances of an object, and you can use a specific implementation instead of a singleton if necessary (shown in the first article [0] ).
- There is a clear separation: the class interface, implementation, the relationship between the interface and the implementation. Each solves only his task.
- Explicit description of singleton dependencies, inclusion of this dependency in the class contract.
Update
After reading the comments, I realized that there are some points that should be clarified, because many are not familiar with the dependency inversion principle, DIP or inversion of control, IoC. Consider the following example: we have a database that contains the information we need, for example, a list of users:
struct IDatabase
{
virtual ~IDatabase() {}
virtual void beginTransaction() = 0;
virtual void commit() = 0;
...
};
We have a class that provides the information we need, including the necessary user:
struct UserManager
{
An aDatabase;
User getUser(int userId)
{
aDatabase->beginTransaction();
...
}
};
Here we create aDatabase member that says it needs some kind of database. It doesn’t matter for him to know what kind of database it will be, he doesn’t need to know who and when it will fill / fill. But the UserManager class knows that what it needs is poured there. All he says is: “give me the desired implementation, I don’t know which one, and I will do everything you need from this database, for example, provide the necessary information about the user from this database.”
Now we do the trick. Since we have only one database that contains all our information, we say: ok, since there is only one database, let's make a singleton, and so as not to bathe each time in the implementation fill, make the singleton fill itself :
struct MyDatabase : IDatabase
{
virtual void beginTransaction();
...
};
BIND_TO_IMPL_SINGLE(IDatabase, MyDatabase)
Those. we create an implementation of MyDatabase and say that we will use it for the singleton using the BIND_TO_IMPL_SINGLE macro. Then the following code will automatically use MyDatabase:
UserManager manager;
User user = manager.getUser(userId);
Over time, it turned out that we have another database, which also has users, but, say, for another organization:
struct AnotherDatabase : IDatabase
{
...
};
Of course, we want to use our UserManager, but with a different database. No problems:
UserManager manager;
manager.aDatabase = anSingle();
User user = manager.getUser(userId);
And as if by magic, now we take the user from another database! This is a rather crude example, but it clearly shows the principle of dependency handling: this is when the IDatabase implementation is flooded with the UserManager instead of the traditional approach, when the UserManager itself searches for the necessary implementation. In this article, this principle is used, while a singleton for implementation is taken as a special case .
Literature
[0] Using the singleton pattern
[1] Wikipedia: singleton
[2] Inside C ++: singleton
[3] Generating patterns: Singleton
[4] Dependency inversion principle
[5] The Principles of OOD
[6] Wikipedia: Rvalue reference and move semantics