Detailed explanation of c++---special class design

Design a class that cannot be copied

Copy will only be released in two scenarios: copy constructor and assignment operator overloading, so if you want to make a class prohibit copying, you only need to make the class unable to call the copy constructor and assignment operator overloading. Then the way c++98 adopts is to overload the copy constructor and assignment operator, just declare that it is not defined, and set its access permission to private, for example, the following code:

class CopyBan
{
    
    
    // ...

private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

Set to private: If only the declaration is not set to private, if the user defines it outside the class, copying cannot be prohibited. The reason for only declaring but not defining is that the function will not be called at all. It is meaningless to define it, so don’t write it On the contrary, it is simpler, and if it is defined, it will not prevent internal copying of member functions. C++11 extends the usage of delete. In addition to releasing the resources applied for by new, if delete is followed by =delete, it means that the compiler deletes the default member function. Then the code here is as follows:

class CopyBan
{
    
    
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

Design a class that can only create objects from the heap

We can create class objects in three places, namely on the heap and on the stack and on the static area. The creation methods are as follows:

int main()
{
    
    
    HeapOnly tmp1;//栈上
    HeapOnly* tmp2 = new HeapOnly;//堆上
    static HeapOnly tmp3;//静态区上
}

When creating an object, the constructor must be called, so here you can put the constructor in private, and then create a function to create objects on the heap through new, but there will be a question of whether the chicken or the egg comes first. So we make the function that creates the object static, such as the following code:

class HeapOnly
{
    
    
public:
    static HeapOnly* CreateObject()
    {
    
    
        return new HeapOnly;
    }
private:
    HeapOnly() {
    
    }
};

Written in this way, the container can intelligently create space on the heap. Then the result of running the above code is as follows:
Insert image description here
You can see that the above code cannot call the constructor when there is a problem. So if you want to create an object here, just call it intelligently. function, for example the following code

int main()
{
    
    
    HeapOnly* tmp = HeapOnly::CreateObject();
}

Although the above writing method can effectively avoid creating objects on the stack and static area, it can still create objects on the stack through copy construction, so copy construction must be disabled. For example, the following code:

class HeapOnly
{
    
    
public:
    static HeapOnly* CreateObject()
    {
    
    
        return new HeapOnly;
    }
private:
    HeapOnly() {
    
    }
    HeapOnly(const HeapOnly&) = delete;
};

Another method here is to privatize the destructor, because the variables on the stack will be automatically destroyed and the destructor will be called, so when the life cycle of the object is over, the destructor will be called automatically to free up space, but the destructor The function is privatized so it cannot be removed, which further prevents the object from being shipped on the stack, but the object created on the heap cannot be removed from the destructor, so here you can create a function for the variables on the heap to call the destructor To free up space, the code here is as follows:

#include<iostream>
using namespace std;
class HeapOnly
{
    
    
public:
    HeapOnly()
    {
    
    }
    void Destory()
    {
    
    
        this->~HeapOnly();
    }
private:
    ~HeapOnly()
    {
    
    }
    HeapOnly(const HeapOnly& tmp) = delete;
};
int main()
{
    
    
    HeapOnly* tmp1 = new HeapOnly;
    tmp1->Destory();
    return 0;
}

The running result is also error-free.
Insert image description here

Design a class that can only create objects on the stack

Here is also similar to the above idea, create a function, create an object on the stack in the function and return the object, for example, the following code:

class StackOnly
{
    
    
public:
    static StackOnly creatobj()
    {
    
    
        return StackOnly();
    }
private:
    StackOnly()
    {
    
    }
};

Then we can create objects on the stack with the following code:

{
    
    
    StackOnly tmp1 = StackOnly::creatobj();
    return 0;
}

If a space is created in another location, an error will be reported directly, such as the following code:

int main()
{
    
    
    StackOnly* tmp1 = new StackOnly;
    static StackOnly tmp2;
    return 0;
}

The content of the error is as follows:
Insert image description here
But this method is not completely sealed, we can still open up space in the static area through copy construction, such as the following code:

int main()
{
    
    
    static StackOnly tmp1 = StackOnly::creatobj();
    return 0;
}

Can we also block the copy constructor? The answer is no, because after the copy constructor is sealed, not only can no space be created in the static area, but also there is no way to create objects on the stack. Here is another way to block operator new and operator delete, such as the following code:

class StackOnly
{
    
    
public:
    StackOnly()
    {
    
    }
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
private:
};

Then using new to create an object at this time will directly report an error, such as the following running results:
Insert image description here

However, this method can only guarantee that no space can be created on the heap, and it cannot guarantee that no space can be created on the static area. For example, the following code can still run normally:

int main()
{
    
    
    static StackOnly tmp2;
    return 0;
}

Therefore, there is no way to completely block the conventional method. If you want to completely block it, there is only one way to block the copy constructor and not accept the object. Call the function directly by referencing or creating a temporary anonymous object, such as the following Code:

int main()
{
    
    
    StackOnly::creatobj().Print();
	const StackOnly& so4 = StackOnly::creatobj();
	so4.Print();
    return 0;
}

The running result of the code is as follows:
Insert image description here
However, such an implementation has a flaw that it cannot modify the contents of the object.

Design a class that cannot be inherited

The first method is to make the constructor private. In C++98, the constructor is privatized. The constructor of the base class cannot be called in the derived class, so it cannot be inherited.

class NonInherit
{
    
    
public:
 static NonInherit GetInstance()
 {
    
    
 return NonInherit();
 }
private:
 NonInherit()
 {
    
    }
};

The second method is to add final. The final modified class indicates that the class cannot be inherited, so the code here is as follows:

class NonInherit  final
{
    
    
    // ....
};

What is singleton pattern

A class can only create one object, that is, the singleton mode. This mode can ensure that there is only one instance of the class in the system and provide a global access point to access it. This instance is shared by all program modules. For example, in a server program, the server's configuration information is stored in a file. These configuration data are uniformly read by a singleton object, and then other objects in the service process obtain the configuration information through this singleton object. This method simplifies configuration management in complex environments. There are two ways to implement the singleton mode. The first is the hungry man mode, and the second is the lazy man mode. Let's first take a look at the principle of the hungry man mode.

Hungry mode

The characteristic of the singleton pattern is that there is only one unique object in the world. How to ensure that there is only one unique object in the world? First, the constructor is sealed. If the constructor is not sealed, the user can use this class to create multiple objects. Then we can create a class that contains a map container and contains some data, and then the class provides some Functions are used to access map data, modify map data, etc., and then put the constructor of this class in private, for example, the following code:

class InfoSingleton
{
    
    
public:
    
private:
    InfoSingleton()
    {
    
    }
    map<string, int> _info;
};

Then there is a question how to create an object and ensure that there is only one object? The answer is to create a static variable in the class and provide a static member function to obtain a reference to the object, such as the following code:

class InfoSingleton
{
    
    
public:
   	static InfoSingleton& GetInstance()
    {
    
    
        return _sins;
    }
private:
    InfoSingleton()
    {
    
    }
    map<string, int> _info;
    static InfoSingleton  _sins;
};
InfoSingleton InfoSingleton::_sins;

The external static member variables are definitions, and the internal static member variables are declarations. In order to facilitate the insertion and modification of data into the object, we also need to create an insert function, in which the internal data is modified through square brackets, for example Code below:

void insert(string name, int salary)
{
    
    
    _info[name] = salary;
}

Then we can insert or modify data in the main function by reference or anonymous call, for example, the following code:

int main()
{
    
    
    InfoSingleton::GetInstance().insert("张三", 10000);
    InfoSingleton::GetInstance().insert("李四", 12000);
    InfoSingleton& tmp = InfoSingleton::GetInstance();
    tmp.insert("王五", 13000);
    tmp.insert("老六", 14000);
    return 0;
}

Then we can also create a print function to print the internal data, such as the following code:

void print()
{
    
    
    for (auto& ch : _info)
    {
    
    
        cout << ch.first << ":" << ch.second << endl;
    }
}

Then we can view the data in the container, and the running results here are as follows:
Insert image description hereThis method is the hungry man mode that creates the object at the beginning, but there is a problem with this method that copy construction and assignment overloading will create one more object. It does not meet the characteristics, so the copy construction and assignment overloading must be removed here. Then the code here is as follows:

class InfoSingleton
{
    
    
public:
    static InfoSingleton& GetInstance()
    {
    
    
        return _sins;
    }
    void insert(string name, int salary)
    {
    
    
        _info[name] = salary;
    }
    void print()
    {
    
    
        for (auto& ch : _info)
        {
    
    
            cout << ch.first << ":" << ch.second << endl;
        }
    }
private:
    InfoSingleton()
    {
    
    }
    InfoSingleton(InfoSingleton const&) = delete;
    InfoSingleton& operator=(InfoSingleton const&) = delete;
    map<string, int> _info;
    static InfoSingleton  _sins;
};
InfoSingleton InfoSingleton::_sins;

We call this implementation the Hungry Pattern because it creates an object at the beginning of the program.

Disadvantages of the hungry man model

1. If there is too much data when initializing the Hungry Mode, it will cause a slower start-up speed, because the Hungry Mode must be initialized before the main function, and the data content may be very large and the data may need to be linked to the database, etc. etc., so it may cause the startup speed to be very slow.
2. Multiple singleton classes have initialization dependencies, and the Hungry Man mode cannot be controlled. For example, A and B are both single-interest classes. It is required to initialize A first, and then initialize B, because B will depend on A. However, the variables in the hungry mode are global variables and the initialization order cannot be guaranteed, so errors may occur here. Then To solve this problem in the future, someone proposed the lazy man model.

lazy mode

The hungry mode is to create objects at the beginning of the program, while the lazy mode is to not rush to create objects first, and then create objects when needed. Then we modify the static object in the class into a static pointer object, and add it outside the class. It is initialized to be empty. In the GetInstance function, it is judged whether the current pointer is empty. If it is empty, an object is created and finally the object pointed to by the current pointer is returned. Then the code here is as follows:

class InfoSingleton
{
    
    
public:
    static InfoSingleton& GetInstance()
    {
    
    
    //第一次调用的时候创建对象
        if (_psins == nullptr)
        {
    
    
            _psins = new InfoSingleton;
        }
        return *_psins;
    }
    void insert(string name, int salary)
    {
    
    
        _info[name] = salary;
    }
    void print()
    {
    
    
        for (auto& ch : _info)
        {
    
    
            cout << ch.first << ":" << ch.second << endl;
        }
    }
private:
    InfoSingleton()
    {
    
    }
    InfoSingleton(InfoSingleton const&) = delete;
    InfoSingleton& operator=(InfoSingleton const&) = delete;
    map<string, int> _info;
    static InfoSingleton*  _psins;
};
InfoSingleton* InfoSingleton::_psins=nullptr;

Then here is not to create the object at the beginning, but to create the object when you call it.

Advantages of lazy mode

1. The object will be created after the main function, which will not affect the startup sequence.
2. The initialization sequence can be actively controlled. For example, A depends on B, then we can call A first and then call B to solve this problem.

Disadvantages of lazy mode

When multiple threads call a singleton object together, multiple objects may be created when creating the object. If the first thread sees that there is no object of the current class, a new one will be created. The first thread has not yet completed the creation and the second thread runs over. It is found that there is still no one, so another object will be created at this time, so at this time, a shackle variable must be added to the class, because static member functions cannot access non-static member variables without this pointer, so a static member variable must be created here. yoke, then the code here is as follows:

class InfoSingleton
{
    
    
public:
    static InfoSingleton& GetInstance()
    {
    
    
        mtx.lock();
        if (_psins == nullptr)
        {
    
    
            _psins = new InfoSingleton;
        }
        mtx.unlock();
        return *_psins;
    }
    void insert(string name, int salary)
    {
    
    }
    void print()
    {
    
    }
private:
    InfoSingleton()
    {
    
    }
    InfoSingleton(InfoSingleton const&) = delete;
    InfoSingleton& operator=(InfoSingleton const&) = delete;
    map<string, int> _info;
    static InfoSingleton*  _psins;
    static mutex mtx;
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;

But there is a problem here. We will only have problems when we create the object for the first time, and we have to unlock the shackles every time we use this function. Will there be a time consumption here, so can we put What about putting the shackles inside the if? The answer is no. Thread safety will appear here, because two threads may enter the created object, and this method will increase the risk of thread safety, so here you can add a double-layer check for shackle protection, then here The code is as follows:

static InfoSingleton& GetInstance()
{
    
    
    if (_psins == nullptr)//对象new出来了,避免每次都枷锁的检查,提高性能。
    {
    
    
        mtx.lock();
        if (_psins == nullptr)//保证线程安全且执行一次
        {
    
    
            _psins = new InfoSingleton;
        }
        mtx.unlock();
    }
    return *_psins;
}

But new may throw an exception here, so the exception must be caught here. If it is not caught, it will not be unlocked. Then the improved code is as follows:

static InfoSingleton& GetInstance()
 {
    
    
     if (_psins == nullptr)
     {
    
    
         mtx.lock();
         try
         {
    
    
             if (_psins == nullptr)
             {
    
    
                 _psins = new InfoSingleton;
             }
         }
         catch (...)
         {
    
    
             mtx.unlock();
             throw;
         }
     }
     return *_psins;
 }

The above release method is a bit ugly, so here we can use the idea of ​​​​smart pointers to solve the problem here. Create a class that contains a reference object of the lock. Then the constructor of this class is to lock the lock. The class The destructor is to unlock this class, so the code here is like this:

template<class Lock>
class LockGuard
{
    
    
public:
    LockGuard(Lock& lk)
        :_lk(lk)
    {
    
    
        _lk.lock();
    }

    ~LockGuard()
    {
    
    
        _lk.unlock();
    }

private:
    Lock& _lk;
};

Then we can write the above function as follows:

static InfoSingleton& GetInstance()
{
    
    
    if (_psins == nullptr)
    {
    
    
        LockGuard<mutex> lock(mtx);
        if (_psins == nullptr)
        {
    
    
            _psins = new InfoSingleton;
        }
    }
    return *_psins;
}

Generally, singleton objects do not need to consider memory release, because singleton objects are generally used throughout the entire program, but singleton objects must be manually processed when not in use to allow some resources to report errors, so at this time, a delete must be provided. function to manually release, then the function code here is as follows:

static void DelInstance()
{
    
    
    	/*保存数据到文件
    	...*/
    std::lock_guard<mutex> lock(mtx);
    if (_psins)
    {
    
    
    	delete _psins;
    	_psins = nullptr;
    }
}

So can this class be automatically released here? The answer is yes, we can define an inner class and call DelInstance in the destructor of the class, and then add a static object of this class to the outer class, so that when the singleton class is destroyed, the object of the inner class will be will be destroyed. When the inner class object is destroyed, its destructor will be called, and then the destructor will call the GetInstance function to release it. The complete code is as follows:

template<class Lock>
class LockGuard
{
    
    
public:
    LockGuard(Lock& lk)
        :_lk(lk)
    {
    
    
        _lk.lock();
    }

    ~LockGuard()
    {
    
    
        _lk.unlock();
    }

private:
    Lock& _lk;
};
class InfoSingleton
{
    
    
public:
    static InfoSingleton& GetInstance()
    {
    
    
        if (_psins == nullptr)
        {
    
    
            LockGuard<mutex> lock(mtx);
                if (_psins == nullptr)
                {
    
    
                    _psins = new InfoSingleton;
                }
         
        }
        return *_psins;
    }
    void insert(string name, int salary)
    {
    
    
        _info[name] = salary;
    }
    void print()
    {
    
    
        for (auto& ch : _info)
        {
    
    
            cout << ch.first << ":" << ch.second << endl;
        }
    }
    static void DelInstance()
    {
    
    
        	/*保存数据到文件
        	...*/
        std::lock_guard<mutex> lock(mtx);
        if (_psins)
        {
    
    
        	delete _psins;
        	_psins = nullptr;
        }
    }
    class GC
    {
    
    
    public:
        ~GC()
        {
    
    
        	if (_psins)
        	{
    
    
        		cout << "~GC()" << endl;
        		DelInstance();
        	}
        }
    }; 	
private:
    InfoSingleton()
    {
    
    }
    InfoSingleton(InfoSingleton const&) = delete;
    InfoSingleton& operator=(InfoSingleton const&) = delete;
    map<string, int> _info;
    static InfoSingleton*  _psins;
    static mutex mtx;
    static GC _gc;  
};
mutex InfoSingleton::mtx;
InfoSingleton* InfoSingleton::_psins=nullptr;
InfoSingleton::GC InfoSingleton::_gc;

special lazy guy

class InfoSingleton
{
    
    
public:
	static InfoSingleton& GetInstance()
	{
    
    
		static InfoSingleton sinst;
		return sinst;
	}

	void Insert(string name, int salary)
	{
    
    
		_info[name] = salary;
	}

	void Print()
	{
    
    
		for (auto kv : _info)
		{
    
    
			cout << kv.first << ":" << kv.second << endl;
		}
		cout << endl;
	}

private:
	InfoSingleton()
	{
    
    
		cout << "InfoSingleton()" << endl;
	}

	InfoSingleton(const InfoSingleton& info) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;


	map<string, int> _info;
	// ...
};

We have said before that the static member variables in the function are created and initialized when the function is called for the first time. After the first call, the code will not be executed again, so the above code can ensure that only one object is created. And the creation of the object also occurs after the main function, but when facing multi-threading, may the above code have thread safety issues? The answer is that before C++11, there is no guarantee that the initialization of sinst is thread-safe. After C++11, it can be guaranteed to be safe. Therefore, this writing method is not necessarily safe and is not a universal method. Therefore, if the compiler supports c++11, it can be written. If it does not, it cannot be written.

Guess you like

Origin blog.csdn.net/qq_68695298/article/details/131625145