Design of C + + special class

Designing a class can only create objects on the heap

The desired effect is that you can't create objects directly on the stack.

First of all, cpp needs to call the constructor as long as it creates an object, so first ban the constructor and design the constructor as private. But you can't create it yourself.

Therefore, a created interface is provided, which can only be called, and new is written inside the interface. Moreover, to call this interface, you need to call the object pointer first, and to have an object, you need to call the constructor instantiation first, so it must be designed as a static function.

However, note that there is also a copy function that can call HeapOnly copy(*p). At this time, the object on the stack is also generated. Therefore, copy the private structure and only declare that it is not implemented (implementation is also possible, but no one uses it). This method is called copy prevention in c++98, such as mutual exclusion lock.

#include<iostream>
using namespace std;

class HeapOnly
{
private:
	HeapOnly()
	{ }
    //C++98 -- copy prevention
    HeapOnly(const HeapOnly&);
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
};
int main()
{
	HeapOnly* p = HeapOnly::CreateObj();
	return 0;
}

For copy prevention, there is a new way in C++11. Function = delete.

#include<iostream>
using namespace std;

class HeapOnly
{
private:
	HeapOnly()
	{ }
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
    //C++11 -- copy prevention
    HeapOnly(const HeapOnly&) =delete;
};
int main()
{
	HeapOnly* p = HeapOnly::CreateObj();
	return 0;
}

Summary:

  1. Declare the constructor of the class private and the copy constructor private. Prevent others from calling copy to generate objects on the stack.
  2. Provides a static member function in which the creation of heap objects is completed

Designing a class can only create objects on the stack

  • Method 1: at the same time, privatize the constructor, and then design a static method to create an object to return.

Since a temporary object is returned, the copy construct cannot be disabled.

class StackOnly 
{ 
    public: 
    static StackOnly CreateObject() 
    { 
        return StackOnly(); 
    }
    private:
    StackOnly() {}
};
  • Method 2: call the exclusive operator new and operator delete of the class and set them to private.

Because new calls void* operator new(size_t size) function at the bottom, you only need to mask this function. Note: also prevent positioning new. New first calls operator new to apply the space, then calls the constructor. Delete first calls the destructor to release the space applied by the object, and then calls operator delete to release the applied object space.

class StackOnly 
{ 
    public: 
    StackOnly() {}
    private: //C++98
    void* operator new(size_t size);
    void operator delete(void* p);
};
int main()
{
  	static StackOnly st;//Defects, no static area is forbidden.  
}
class StackOnly 
{ 
    public: 
    StackOnly() {}
    //C++11
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
};
int main()
{
  	static StackOnly st;//Defects, no static area is forbidden.  
}

Design a class that cannot be copied

Copy will only be released in two scenarios: copy constructor and assignment operator overloading. Therefore, if you want a class to prohibit copy, just make the class unable to call copy constructor and assignment operator overloading.

  • C++98 overloads the copy constructor and assignment operator, only declares that they are not defined, and sets their access rights to private.
class CopyBan
{
    // ...

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

reason:

  1. Set to private: if you only declare that it is not set to private, if you define it outside the class, you can't prohibit copying
  2. Only declare but not define: not define because the function will not be called at all. In fact, it is meaningless to define it. It is simple not to write it. Moreover, 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 requested by new, if delete is followed by the default member function, it means that the compiler is asked to delete the default member function.
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

Design a class that cannot inherit

  • C++98
// The constructor is privatized in C++98, and the constructor of the base class is not derived from the derived class. Cannot inherit
class NonInherit
{
    public:
    static NonInherit GetInstance()
    {
        return NonInherit();
    }
    private:
    NonInherit()
    {}
};
class B : public NonInherit
{};

int main()
{
    //In C++98, this method that cannot be inherited is not thorough enough. In fact, it can be inherited. The limitation is that objects cannot be instantiated after subclass inheritance
    B b;
    return 0;
}
  • In order to be more intuitive, C++11 adds the final keyword
class A final
{   };
class C: A
{};

Design pattern (only one class can be created)

I've touched adapter mode and iterator mode before.

You can take another look at the factory mode, observer mode and so on.

The concept of singleton pattern

Design Pattern: Design Pattern is a set of code design experience that is repeatedly used, known by most people, classified and summarized.

Why do design patterns come into being? Just like the development of human history will produce the art of war. In the beginning, the war between tribes was fought by people. Later, during the spring and Autumn period and the Warring States period, there were often wars between the seven countries, and it was found that there was a routine in fighting. Later, Sun Tzu summarized the art of war. Sun Tzu's art of war is similar.

The purpose of using design patterns: to make the code reusable, make the code easier to be understood by others, and ensure the reliability of the code.

Design pattern makes code writing truly engineering; Design pattern is the cornerstone of software engineering, just like the structure of a building.

  • Singleton mode

A class can only create one object, that is, 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 configuration information of the server is stored in a file. These configuration data are uniformly read by a singleton object, and then other objects in the service process obtain these configuration information through this singleton object. This way simplifies the configuration management in a complex environment.

  1. How to ensure that there is only one unique instance object globally (in a process)

Reference can only create objects on the heap and on the stack. Construction and copy construction and assignment are prohibited.

Provide a GetInstance to get the singleton object.

  1. How to provide only one instance?

Hungry man mode and lazy man mode.

Implementation of singleton mode

Hungry man model

Hungry man mode: before the program starts main execution, it creates a singleton object, provides a static member pointer to the singleton object, and initially new an object to it.

class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	int _val;
};
Singleton* Singleton::_inst = new Singleton;
int main()
{
    cout<<Singleton::GetInstance()<<endl;
    cout<<Singleton::GetInstance()<<endl;
    cout<<Singleton::GetInstance()<<endl;
    Singleton::GetInstance()->Print();
}

Lazy mode

Lazy mode:

The reason for the lazy mode is that there is a lot of configuration initialization in the constructor of the singleton class, so the hungry man is not suitable, which will lead to the slow start of the program.

linux is Posix's pthread library, and windows has its own thread library. Therefore, conditional compilation should be used to ensure compatibility. Therefore, c++11 provides language level encapsulation for specification (essentially conditional compilation, which is implemented in the Library).

For the scenario where the protection needs to be locked for the first time and does not need to be locked later, double check locking can be used.

#include<mutex>
#ifdef _WIN32
//windos provides multithreading api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //For the scenario where the protection needs to be locked for the first time and does not need to be locked later, double check locking can be used
            //Features: lock for the first time, no lock behind, protect thread safety and improve efficiency at the same time
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//() default parameterless constructor
int main()
{
    Singleton::GetInstance()->Print();
}

Comparison between the hungry man model and the lazy man model

  • Hungry man model

    • Advantages: simple
    • Disadvantages:
      • If the singleton object constructor works more, it will cause the program to start slowly and fail to enter the main function.
      • If there are multiple singleton objects and there are initialization dependencies between them, the hungry man mode will also have problems. For example, there are two singleton classes A and B, which require singleton A to be initialized first, and B must be initialized after A, so the hungry man cannot guarantee. In this scenario, the lazy mode is used. The lazy can call A::GetInstance() first and then B::GetInstance().
  • Lazy mode

    • Advantages: it solves the disadvantage of hungry man, because he creates an initialization singleton object when calling GetInstance for the first time
    • Disadvantages: it's a little more complicated than a hungry man.

Optimization of lazy mode

Achieved "lazier".

Disadvantages: the singleton object is in the static area. If the singleton object is too large, it is inappropriate. Again, the static object cannot actively control the release.

#include<mutex>
#ifdef _WIN32
//windos provides multithreading api
#else
//linux pthread
#endif //
//Other versions of lazy man
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            static Singleton inst;
            return &inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static std::mutex _mtx;
    	int _val;
};
std::mutex Singleton::_mtx;//() default parameterless constructor
int main()
{
    Singleton::GetInstance()->Print();
}

Release of singleton object

Singleton objects generally do not need to be released. The global is always used without deleting. If the process is destroyed normally, the process will release the corresponding resources.

Direct release of singleton object

#include<mutex>
#ifdef _WIN32
//windos provides multithreading api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //For the scenario where the protection needs to be locked for the first time and does not need to be locked later, double check locking can be used
            //Features: lock for the first time, no lock behind, protect thread safety and improve efficiency at the same time
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	static void DelInstance()/*There are few adjustments. You can double check or not double check*/
        {
            _mtx.lock();
            if(!_inst)
            {
                delete _inst;
                _inst=nullptr;
            }
            _mtx.unlock();
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {
          	//Suppose that in the singleton class constructor, you need to do a lot of configuration initialization   
        }
    	~Singletion()
        {
            //At the end of the program, you need to deal with it and persist some data
        }
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//() default parameterless constructor
int main()
{
    Singleton::GetInstance()->Print();
}

Internal garbage collection

In fact, the above scenario can be extended.

Suppose that some data of the destructor needs to be saved and persisted. There will be problems if the destructor is not called, so it needs to be processed when the destructor is called. This ensures that private is called at the end of the main function.

However, there may be forgetting when explicitly calling DelInstance.

#include<mutex>
#ifdef _WIN32
//windos provides multithreading api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //For the scenario where the protection needs to be locked for the first time and does not need to be locked later, double check locking can be used
            //Features: lock for the first time, no lock behind, protect thread safety and improve efficiency at the same time
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {
          	//Suppose that in the singleton class constructor, you need to do a lot of configuration initialization   
        }
    	~Singletion()
        {
            //At the end of the program, you need to deal with it and persist some data
        }
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    
    	//Implement an embedded garbage collection class
    	class CGarbo{
            public:
            	~CGarbo()
                {
                    //DelInstance();
                	if(_inst)
                    {
                         delete _inst;
                        _inst = nullptr;
                    }
                }
        }
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	static GCarbo _gc;//Define static gc objects to help us recycle
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//() default parameterless constructor
Singleton::CGarbo Singleton::_gc;
int main()
{
    Singleton::GetInstance()->Print();
}

Keywords: C++ Back-end

Added by MMeticulous on Sun, 30 Jan 2022 13:02:39 +0200