📜 ⬆️ ⬇️

Singleton and object lifetime

This article is a continuation of my first article “Using the Singleton Pattern” [0] . At first, I wanted to explain everything related to the life time in this article, but the volume of material turned out to be great, so I decided to break it up into several parts. This is a continuation of a whole cycle of articles on the use of various templates and techniques. This article is devoted to the lifetime and development of using singleton. Before reading the second article, it is strongly recommended that you read my first article [0] .

The previous article used the following implementation for a singleton:
template<typename T> T& single() { static T t; return t; } 


The single function returned us the cherished 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 operator new.

It so happens that in C ++ there is no garbage collector, so you need to follow the creation and destruction of the object. And although this problem has long been known and even clear methods for how to solve it, this kind of error is not a rare guest in our programs. In general, the following types of errors that programmers make are:
  1. Memory usage for not created object.
  2. Memory usage of already deleted object.
  3. Non-release of the memory occupied by the object.

As a result of such errors, the program either starts “flowing”, starts to behave unpredictably, or simply “falls”. 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, one can easily show how such errors are made. 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 memory is allocated for a singleton, but for some reason it is not released. You can, of course, say that for a singleton, this is not a serious problem, since its lifetime coincides with the running time of the program. However, there are some but:
  1. Memory leak detection programs will show these leaks all the time for singletons.
  2. A singleton can be a rather complicated object, serving an open configuration file, connection to a database, and so on. Improper destruction of such objects can cause problems.
  3. It all starts small: first, we do not follow the memory for singletons, and then for other objects.
  4. And the main question: why do wrong, if you can do right?

You can, of course, say that these arguments are irrelevant. However, let's still do it right. I always use the following principle for working with objects: the created object must be destroyed . And it does not matter whether it is singleton or not, this is a general rule without exceptions, which sets a certain quality of the program.

Analyzing the source code of various software products, I highlighted 2 more important rules for myself:
  1. Do not use new.
  2. Do not use delete.

Here it is worth explaining a little what I mean. It is clear that somewhere, new and delete will be called anyway. Speech about the fact that it should be strictly in one place, it is better in one class, so that it does not spray on the program. Then, with the proper organization of this class, it will not be necessary to monitor the lifetime of objects. And I will immediately say that this is possible! It should immediately make a reservation that I have never met such an approach. So we will be some kind of pioneers.

Smart pointers


Fortunately, in C ++ there is a wonderful tool called smart pointer. Their intelligence lies in the fact that, although they behave like ordinary pointers, they control the lifetime of objects. To do this, they use a counter that automatically counts the number of references to the object. When the counter reaches zero, the object is automatically destroyed. We will use a smart pointer from the standard library std :: shared_ptr 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, you can use boost :: shared_ptr. Their interfaces are absolutely identical.

We assign the following responsibilities to our class An:
  1. Control the lifetime of objects using smart pointers.
  2. Creating instances, including derived classes, without using new operators in the calling code.


The following implementation satisfies these conditions:
 template<typename T> struct An { template<typename U> friend struct An; An() {} template<typename U> An(const An<U>& a) : data(a.data) {} template<typename U> An(An<U>&& 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<T>(); } template<typename U> U& create() { U* u = new U; data.reset(u); return *u; } template<typename U> void produce(U&& u) { anProduce(*this, u); } template<typename U> void copy(const An<U>& a) { data.reset(new U(*a.data)); } private: T* get0() const { const_cast<An*>(this)->init(); return data.get(); } std::shared_ptr<T> data; }; 


It is worthwhile to dwell upon the proposed implementation:
  1. The constructor uses the move-semantics [6] of the C ++ 0x standard to increase speed when copying.
  2. The create method creates an object of the required class; by default, an object of class T is created.
  3. The produce method creates an object depending on the value received. The purpose of this method will be described later.
  4. The copy method makes a deep copy of the class. It should be noted that for copying it is necessary to specify the type of a real class instance as a parameter, the base type is not suitable.


In this case, the singleton will be rewritten as follows:
 template<typename T> struct AnAutoCreate : An<T> { AnAutoCreate() { create(); } }; template<typename T> T& single() { static T t; return t; } template<typename T> An<T> anSingle() { return single<AnAutoCreate<T>>(); } 


Auxiliary macros will be as follows:
 #define PROTO_IFACE(D_iface, D_an) \ template<> void anFill<D_iface>(An<D_iface>& 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<D_impl>(); } #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<D_impl>(); } #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<D_ifaceFrom>(a); } #define BIND_TO_PROTOTYPE(D_iface, D_prototype) \ PROTO_IFACE(D_iface, a) { a.copy(anSingle<D_prototype>()); } 


The macro BIND_TO_IMPL_SINGLE, which now uses the anSingle function instead of the single function, which in turn returns an already filled An instance, has undergone some changes. I will tell about other macros later.

Use singleton


Now consider the use of 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> x; std::cout << x->x << std::endl; 


That on the screen will give figure 2, since class Y was used for implementation.

Lifetime control


Consider now an example that shows the importance of using smart pointers for singletons. To do this, 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>().a << std::endl; } }; 


Now let's see what is displayed on the out call:
 single<B>().out(); //    B A 1 ~A ~B -1 

We will understand what is happening here. At the very beginning we say that we want an implementation of class B taken from a singleton, so a class B is created. Then we call the function out, which takes the implementation of class A from the singleton and takes the value a. The value a is set in the constructor A, therefore the figure 1 will appear on the screen. Now the program finishes its work. Objects begin to be destroyed in reverse order, i.e. first the class A created last is destroyed, and then class B is destroyed. When class B is destroyed, we again call out function out of the singlton, but since if object A is already destroyed, then we see -1 on the screen. Generally speaking, the program could crash, because we use the memory of an already destroyed object. Thus, this implementation shows that without control of the lifetime, the program can safely fall off at the close.

Let's now see how you can do the same, but with the control of the lifetime of objects. For 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> 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; b->out(); 


This code is practically no different from the previous one, except for the following important details:
  1. Objects A and B use the class An for singletons.
  2. Class B explicitly declares a dependency on class A, using the corresponding public member of the class (for more details on this approach, see 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 lifetime of class A and changed the sequence of the destruction of objects. The absence of the value -1 indicates that the object existed while accessing its data.

Total


In this first part of the article, devoted to the lifetime of objects, came to an end. In the next part (or parts), the rest generating design patterns will be analyzed using the developed functionality and general conclusions will be made.

PS


Many people ask, but what is the meaning of it? Why not just make a singleton? Why use any additional constructions that do not add clarity, but only complicate the code. In principle, with careful reading of the first article [0] , it is already possible to understand that this approach is more flexible and eliminates a number of significant shortcomings of the singleton. In the next article it will be clear why I wrote it this way, because it will already be a question of not only singlton. And through the article it will be generally clear that Singleton has absolutely nothing to do with it. All I'm trying to show is using the Dependency inversion principle [4] (see also The Principles of OOD [5] ). Actually, it was after I first saw this approach in Java that I felt hurt that in C ++ this is hardly used (in principle, there are frameworks that provide similar functionality, but I would like something more lightweight and practical). This implementation is only a small step in this direction, which already brings great benefits.

I would also like to note a few things that distinguish the given implementation from the classic singleton (generally speaking, these are consequences, but they are important):
  1. The class describing a singleton can be used in several instances without any restrictions.
  2. A singleton is implicitly poured through 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] ).
  3. There is a clear separation: the class interface, the implementation, the connection between the interface and the implementation. Everyone solves only their problem.
  4. Explicit description of dependencies on singletons, including this dependency in class contract.


Update


After reading the comments, I realized that there are some points that should be clarified, because many are not familiar with dependency inversion principle (DIP or inversion of control, IoC). Consider the following example: we have a database that contains the information we need, such as a list of users:
 struct IDatabase { virtual ~IDatabase() {} virtual void beginTransaction() = 0; virtual void commit() = 0; ... }; 

We have a class that gives us the information we need, including the necessary user:
 struct UserManager { An<IDatabase> aDatabase; User getUser(int userId) { aDatabase->beginTransaction(); ... } }; 

Here we create a member of aDatabase, which says that it needs some kind of database. He does not care to know what it will be for the database, he does not need to know who and when it will fill / fill. But the UserManager class knows that it will be flooded with what it needs. All he says is: “give me the right implementation, I don't know which one, and I will do everything you need from this database, for example, I will provide the necessary information about the user from this database”.

Now we do a clever trick. Since we have only one database that contains all of our information, we say: ok, since there is only one database, let's make a singleton, and in order not to steam every time we fill in the implementation, we’ll make it so that the singleton is poured :
 struct MyDatabase : IDatabase { virtual void beginTransaction(); ... }; BIND_TO_IMPL_SINGLE(IDatabase, MyDatabase) 

Those. we create the MyDatabase implementation 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 in which there are also users, but, let's 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<AnotherDatabase>(); User user = manager.getUser(userId); 

And as if by magic, now we take a user from another database! This is a rather crude example, but it clearly shows the principle of addressing dependencies: this is when the IDatabase implementation is poured into the UserManager object instead of the traditional approach, when the UserManager itself searches for the necessary implementation. In the article under review, this principle is used, with the singleton being taken for implementation as a special case .

Literature


[0] Use pattern singleton
[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

Source: https://habr.com/ru/post/118368/


All Articles