[The road to C++ practice] 33. Special class design

insert image description here
Every day without dancing is a disappointment to life

  • Master the design methods of common special classes

1. 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.

  • C++98

    Overloading the copy constructor and assignment operator only declares no definition, and sets its access rights to private.

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

reason:

  1. Set to private: If only the statement is not set to private, if the user defines it outside the class, copying cannot be prohibited
  2. Only declare but not define: not define because the function will not be called at all, and it is meaningless to define it. It is simple if not written, and if it is defined, it will not prevent internal copying of member functions.
  • C++11

    C++11 extends the use of delete. In addition to releasing the resources requested by new, if delete follows the default member function with =delete, it means that the compiler deletes the default member function.

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

2. Design a class that can only create objects on the heap

1. Create objects of common classes

Ordinary classes, objects can be created in three places:

  1. the stack
  2. heap
  3. static area
#include<iostream>
using namespace std;
class HeapOnly
{
    
    };
int main()
{
    
    
	HeapOnly hp1;//栈
	HeapOnly* php2 = new HeapOnly;//堆
	static HeapOnly hp3;//静态区
	return 0;
}

2. Classes that can only create objects on the heap

If you want to only create objects on the heap, you must do something about the constructor, because the constructor creates objects on the stack by default.

Method to realize:

  1. Make the constructor of the class private, and declare the copy constructor as private. Prevent others from calling copy to generate objects on the stack.
  2. Provide a static member function, and complete the creation of the heap object in the static member function.
#include<iostream>
using namespace std;
class HeapOnly
{
    
    
public:
	static HeapOnly* CreateObject()
	{
    
    
		return new HeapOnly;
	}
private:
	HeapOnly()
    {
    
    }
};

int main()
{
    
    
	HeapOnly* php = HeapOnly::CreateObject();
	return 0;
}

Why add static?

If CreateObject does not add static, then the method needs to be used on the basis of the existing object when calling the method, and the object must use the constructor by default, but the constructor has been privatized. There is an egg problem, so be sure to add static.

But as far as the current situation is concerned, it is still possible to create objects on the stack. First of all, friends must be possible. Secondly, the copy constructor will be generated by default without explicit calls, so objects can still be created on the stack in the following way:

int main()
{
    
    
	HeapOnly* php2 = HeapOnly::CreateObject();
	HeapOnly php3(*php2);//栈上创建对象
	return 0;
}

Therefore, the copy constructor also needs to be disabled, and it is a class that can only be created on the heap:

#include<iostream>
using namespace std;
class HeapOnly
{
    
    
public:
	static HeapOnly* CreateObject()
	{
    
    
		return new HeapOnly;
	}
private:
	HeapOnly() {
    
    }
	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
    
    
	HeapOnly* php2 = HeapOnly::CreateObject();
	return 0;
}

The second way to create classes only on the heap: destructor privatization

If the destructor is private, then creating the object directly will show that there is no suitable constructor, so that the object cannot be created on the stack.

class HeapOnly
{
    
    
public:
	HeapOnly()
	{
    
    }
private:
	~HeapOnly()
	{
    
    }

	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
    
    
	HeapOnly hp1;
	return 0;
}

image-20230709125511089

But at this time, it can be created on the heap, so this time is divided into the following steps:

  1. destructor private
  2. Constructor public display call
  3. Added Destory method to release heap space
class HeapOnly
{
    
    
public:
	HeapOnly()
	{
    
    }
	void Destory()
	{
    
    
		this->~HeapOnly();
	}
private:
	~HeapOnly()
	{
    
    }

	HeapOnly(const HeapOnly&) = delete;
};

int main()
{
    
    
	HeapOnly* php1 = new HeapOnly;
	php1->Destory();

	return 0;
}

image-20230709125822252

Destory has no requirements for static, and it is entirely up to us to decide whether to use static decoration or not.

Note: In vs2019, the above this must show that there is no error in the call.

3. Design a class that can only create objects on the stack

Method 1: (same as above)

  1. Make the constructor private.
  2. Then design the static method to create the object and return it.
//请设计一个类,只能在栈上创建对象
class StackOnly
{
    
    
public:
	static StackOnly CreateObj()
	{
    
    
		return StackOnly();
	}
private:
	StackOnly()
	{
    
    }

};
int main()
{
    
    
	StackOnly so1 = StackOnly::CreateObj();
	// 下面两种静态区和堆的位置都不能创建
	//static StackOnly so2;
	//StackOnly* pso3 = new StackOnly;
	return 0;
}

In fact, this method is not completely blocked, and the following method can still be created in the static area:

int main()
{
    
    
	static StackOnly so2 = StackOnly::CreateObj();
	return 0;
}

image-20230709143846050

The way to solve this problem:

For the mandatory type conversion designed here, a temporary object will be generated in the middle of the mandatory type conversion, and this temporary object will be copied to the object that needs to be defined. If the copy construction is sealed, then not only this will report an error, but the previous one will also report an error, because the former The assignment is also a temporary copy of the returned object:

image-20230709144616655

Therefore, there is no good way to completely block it. But if you want to seal it, that is, seal the copy structure, then don't use = to get it, but call it directly, as follows:

//请设计一个类,只能在栈上创建对象
class StackOnly
{
    
    
public:
	static StackOnly&& CreateObj()
	{
    
    
		return StackOnly();
	}

	void Print() const
	{
    
    
		cout << "StackOnly::Print()" << endl;
	}
private:
	StackOnly()
	{
    
    }
	StackOnly(const StackOnly&) = delete;

};
int main()
{
    
    
	/*StackOnly so1 = StackOnly::CreateObj();
	static StackOnly so2 = StackOnly::CreateObj();*/

	StackOnly::CreateObj().Print();
	const StackOnly& so4 = StackOnly::CreateObj();
	so4.Print();
	return 0;
}

The purpose is to prevent copying.

ps, because StackOnly() is a local object, it will be destroyed when it goes out of scope, so it can be passed out only by using an rvalue reference.


Method 2: seal operator newandoperator delete

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

This method is also possible, because in the process of new object, there must be a step of operator new. But this method can only seal the ones on the pile, but cannot seal the static ones.

So the best way is to use method one.

4. Design a class that cannot be inherited

  • C++98 way
// C++98中构造函数私有化,派生类中调不到基类的构造函数。则无法继承
class NonInherit
{
    
    
public:
    static NonInherit GetInstance()
    {
    
    
        return NonInherit();
    }
private:
    NonInherit()
    {
    
    }
};
  • C++11 method

The final keyword, final modified class, means that the class cannot be inherited.

class A  final
{
    
    
  // ....
};

Five. Singleton mode

  • Singleton mode: Only one object can be created.

1. What is Design Pattern?

A design pattern is a set of problem-solving experience that has been proven through repeated practice in software engineering, and is used to solve common design problems. Here are some common design patterns:

  1. Creational Patterns:

    • Factory Method Pattern
    • Abstract Factory Pattern
    • Singleton Pattern
    • Builder Pattern
    • Prototype Pattern
  2. Structural Patterns:

    • Adapter Pattern
    • Bridge Pattern
    • Composite Pattern¶
    • Decorator Pattern
    • Facade Pattern
    • Flyweight Pattern
    • Proxy Pattern
  3. Behavioral Patterns:

    • Observer Pattern
    • State Pattern
    • Strategy Pattern
    • Command Pattern
    • Chain of Responsibility Pattern
    • Iterator Pattern
    • Mediator Pattern
    • Memento Pattern
    • Visitor Pattern
    • Template Method Pattern
  4. Concurrent Patterns:

    • Semaphore Pattern
    • Thread Pool Pattern
    • Read-Write Lock Pattern
    • Producer-Consumer Pattern

The above are just some common design patterns, in fact there are other design patterns. Each design pattern has specific application scenarios and ways to solve problems. Please note that when using design patterns, the appropriate design pattern should be selected according to specific needs and situations.

For example, the iterator mode, which encapsulates complex things, can avoid touching the complex underlying structure when using it.

For example, adapter mode, etc., also means this.

The purpose of using design patterns: for code reusability, to make the code easier for others to understand, and to ensure code reliability. Design patterns make code writing truly engineering; design patterns are the cornerstone of software engineering, just like the structure of a building.

2. Singleton mode

A class can only create one object, that is, the singleton mode, which can ensure that there is only one instance of the class in the system, and provide a global access point to access it, which is shared by all program modules. For example, in a server program, the configuration information of the server is stored in a file, and 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, which is This approach simplifies configuration management in complex environments.

Since there can only be one global object, in other words, to obtain this object, you need to operate on the constructor.

There are two implementation modes of singleton mode: hungry man mode and lazy man mode

Hungry man mode: Whether you use it or not in the future, a unique instance object will be created when the program starts.

The condition of the hungry man mode: initialized before the main function

Steps to design the hungry man pattern:

  1. Make the constructor private, and seal copy construction and overload assignment
  2. Define a member variable, the variable type isstatic 类型名
  3. Initialize the object of this singleton outside the class
  4. Add other member methods
//单例模式的类:全局只有一个唯一对象
// 饿汉模式(main函数之前初始化)
// 缺点:1、单例对象初始化时对象太多,导致启动慢 
//		 2、多个单例类有初始化依赖关系,饿汉模式无法控制
class InfoSingleton
{
    
    
public:
	static InfoSingleton& GetInstance()
	{
    
    
		return _sins;
	}

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

	void Print()
	{
    
    
		for (auto kv : _info)
		{
    
    
			cout << kv.first << ":" << kv.second << endl;
		}
	}
private:
	InfoSingleton()
	{
    
    }
	InfoSingleton(const InfoSingleton&) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
	// ...
private:
	static InfoSingleton _sins;
};

InfoSingleton InfoSingleton::_sins;

int main()
{
    
    
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infosl = InfoSingleton::GetInstance();
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);

	infosl.Print();
	cout << endl;
	InfoSingleton::GetInstance().Insert("张三", 18000);
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);
	infosl.Print();
	return 0;
}

image-20230709192832157

It can be seen that the amount of code can be simplified by reference when calling.

Disadvantages of Hungry Man Mode:

  1. The singleton object initializes too much data, resulting in slow startup
  2. Multiple singleton classes have initialization dependencies, and the hungry man mode cannot be controlled

Assuming that there are two singleton classes A and B, representing the database and the file system respectively, it is required to initialize A first, and then initialize B, and B will depend on A, then the Hungry Man mode cannot control the order at this time.

If this singleton object is frequently used in a multi-threaded high-concurrency environment and has high performance requirements, it is obviously better to use the hungry man mode to avoid resource competition and improve response speed.

lazy mode

If the construction of a singleton object is very time-consuming or takes up a lot of resources, such as loading plug-ins, initializing network connections, reading files, etc., and it is possible that the object will not be used when the program is running, then it must be created at the beginning of the program Just initializing, it will cause the program to start very slowly. So it's better to use lazy mode (lazy loading) in this case.

Conditions for lazy mode:

  1. The object will be created after the main function and will not affect the startup sequence
  2. You can actively control the order of creation

Steps to design the lazy man mode: (basically the same as the hungry man mode)

  1. Make the constructor private, and seal copy construction and overload assignment
  2. Define a member variable, the variable type isstatic 类型名
  3. Initialize the object of this singleton outside the class
  4. Add other member methods

Differences from Hungry Man Mode:

  1. The object will be created after the main function and will not affect the startup sequence
  2. You can actively control the order of creation
  3. Change object creation to be created on the heap
  4. In lazy mode, multiple objects call GetInstance at the same time, there is a risk of thread safety, and multiple objects may be new, so a lock needs to be added, and a new member object of the lock needs to be added, which is defined as a static type; the hungry mode starts Just one object, no creation, so no locks.

Note: Locks cannot be copied, so when defining member variables of locks, they can be defined by pointers (addresses) or references. It is not common for C++ to use addresses, and it is better to use references.

Locking is also particular, if code like this:

//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
static InfoSingleton& GetInstance()
{
    
    
    _pmtx.lock();
    if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
    {
    
    
        _psins = new InfoSingleton;
    }
    _pmtx.unlock();
    return *_psins;
}

Since this method needs to be locked every time, but in fact, only the first creation of the object requires locking, so in order to avoid the impact of locks on efficiency, double-layer if checks are used; in addition, for new, once an exception is thrown, it needs to be caught , you can use try-catch at this time, but this way of writing is not feasible. Through the previous RAII idea of ​​smart pointers, we can set a class by ourselves: a management class based on RAII ideas to prevent lock problems.

Why try-catch is not feasible, because it is still in the locking stage, once the capture jump is made, then the lock will always be locked. In order to avoid this situation, the idea of ​​RAII is used. There are also corresponding library function methods in the C++ thread library, but you can still tear one up here.

//RAII的锁管理类
template<class Lock>
class LockGuard
{
    
    
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
    
    
		lk.lock();
	}

	~LockGuard()
	{
    
    
		_lk.unlock();
	}
private:
	Lock& _lk;//成员变量用引用-->避免拷贝
};

//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序

//问题:
class InfoSingleton
{
    
    
public:
	//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
	static InfoSingleton& GetInstance()
	{
    
    
		//第一次获取单例对象的时候创建对象
		//双检查加锁
		if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
		{
    
    
			// t1  t2
			LockGuard<mutex> mtx(_smtx);
			if (_psins == nullptr) //保证线程安全且只new一次
			{
    
    
				_psins = new InfoSingleton;
			}
		}

		return *_psins;
	}

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

	void Print()
	{
    
    
		for (auto kv : _info)
		{
    
    
			cout << kv.first << ":" << kv.second << endl;
		}
	}
private:
	InfoSingleton()
	{
    
    }
	InfoSingleton(const InfoSingleton&) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
	// ...
private:
	static InfoSingleton* _psins;
	static mutex _smtx;
};

InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;

int main()
{
    
    
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infosl = InfoSingleton::GetInstance();
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);

	infosl.Print();
	cout << endl;
	InfoSingleton::GetInstance().Insert("张三", 18000);
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);
	infosl.Print();
	return 0;
}

image-20230709203833667

There are also corresponding locking methods in the library:

static InfoSingleton& GetInstance()
{
    
    
    //第一次获取单例对象的时候创建对象
    //双检查加锁
    if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
    {
    
    
        // t1  t2
        //LockGuard<mutex> mtx(_smtx);
        std::lock_guard<mutex> lock(_smtx);//库中的方法
        if (_psins == nullptr) //保证线程安全且只new一次
        {
    
    
            _psins = new InfoSingleton;
        }
    }

    return *_psins;
}

Does it need to be released after new comes out?

Generally, singleton mode objects do not need to consider release. An object of a singleton class is usually used throughout the running of the program, so there will be no problem if it is not deleted at the end. As long as the process ends normally, the resources of the object will be automatically released by the OS.

When do singleton objects need to be released?

When the singleton object is not in use, it must be handled manually, and some resources need to be saved. Assuming that the salary list needs to be saved in a file, and the information is required to be saved before the system ends, manual processing is required at this time. Therefore, you can add a new method DelInstance(), whether you need to call it depends on yourself:

//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序
class InfoSingleton
{
    
    
public:
	//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
	static InfoSingleton& GetInstance()
	{
    
    
		//第一次获取单例对象的时候创建对象
		//双检查加锁
		if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
		{
    
    
			// t1  t2
			//LockGuard<mutex> mtx(_smtx);
			std::lock_guard<mutex> lock(_smtx);
			if (_psins == nullptr) //保证线程安全且只new一次
			{
    
    
				_psins = new InfoSingleton;
			}
		}

		return *_psins;
	}

	//一般单例对象不需要考虑释放
	//单例对象不用时,必须手动处理,一些资源需要保存
	static void DelInstance()
	{
    
    
		//保存数据到文件
		// ...
		std::lock_guard<mutex> lock(_smtx);
		if (_psins)
		{
    
    
			delete _psins;
			_psins = nullptr;
		}
	}

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

	void Print()
	{
    
    
		for (auto kv : _info)
		{
    
    
			cout << kv.first << ":" << kv.second << endl;
		}
	}
private:
	InfoSingleton()
	{
    
    }
	InfoSingleton(const InfoSingleton&) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
	// ...
private:
	static InfoSingleton* _psins;
	static mutex _smtx;
};

InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;

int main()
{
    
    
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infosl = InfoSingleton::GetInstance();
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);

	infosl.Print();
	cout << endl;
	InfoSingleton::GetInstance().Insert("张三", 18000);
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);
	infosl.Print();

	InfoSingleton::DelInstance();//主动调用
	return 0;
}

If you forget to actively call, errors will also occur, so it is still necessary to design a method that can be automatically recycled. Here, a new internal class GC is used, using the idea of ​​​​RAII. Once the active recycling is forgotten, it will be automatically recycled at the end of the main function. , then you need to add a new member variable and inner class:

Note: Inner classes are friends of outer classes

//懒汉模式
//1、对象在main函数之后才会创建,不会影响启动顺序
//2、可以主动控制创建顺序
class InfoSingleton
{
    
    
public:
	//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
	static InfoSingleton& GetInstance()
	{
    
    
		//第一次获取单例对象的时候创建对象
		//双检查加锁
		if (_psins == nullptr)//避免对象new出来以后每次都加锁,提高性能
		{
    
    
			// t1  t2
			//LockGuard<mutex> mtx(_smtx);
			std::lock_guard<mutex> lock(_smtx);
			if (_psins == nullptr) //保证线程安全且只new一次
			{
    
    
				_psins = new InfoSingleton;
			}
		}

		return *_psins;
	}

	//一般单例对象不需要考虑释放
	//单例对象不用时,必须手动处理,一些资源需要保存
	static void DelInstance()
	{
    
    
		//保存数据到文件
		// ...
		std::lock_guard<mutex> lock(_smtx);
		if (_psins)
		{
    
    
			delete _psins;
			_psins = nullptr;
		}
	}
	//忘记调用DelInstance(),自动回收
	class GC
	{
    
    
	public:
		~GC()
		{
    
    
			if (_psins)
			{
    
    
				cout << " ~GC()" << endl;
				DelInstance();
			}
		}
	};

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

	void Print()
	{
    
    
		for (auto kv : _info)
		{
    
    
			cout << kv.first << ":" << kv.second << endl;
		}
	}
private:
	InfoSingleton()
	{
    
    }
	InfoSingleton(const InfoSingleton&) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
	// ...
private:
	static InfoSingleton* _psins;
	static mutex _smtx;
	static GC _gc;
};

InfoSingleton* InfoSingleton::_psins = nullptr;
mutex InfoSingleton::_smtx;
InfoSingleton::GC InfoSingleton::_gc;

int main()
{
    
    
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infosl = InfoSingleton::GetInstance();
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);

	infosl.Print();
	cout << endl;
	InfoSingleton::GetInstance().Insert("张三", 18000);
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);
	infosl.Print();
	return 0;
}

image-20230709205650451

Therefore, it can be recycled actively or automatically at the end of the program, but singleton objects generally do not need to be recycled.

Another way to implement lazy:

There is another way to implement the lazy man mode, direct static, which only creates an object once, so the following way is also possible, but it is not a general way.

//是懒汉:因为静态的局部变量是在main函数之后才创建初始化的:局部静态变量的初始化只初始化一次。
//C++11之前,不能保证sinst的初始化是线程安全的。
//C++11之后,可以。
class InfoSingleton
{
    
    
public:
	//多个对象一起调用GetInstance,存在线程安全的风险,可能new出来多个对象,因此需要加锁
	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;
		}
	}
private:
	InfoSingleton()
	{
    
    
		cout << "InfoSingleton()" << endl;
	}
	InfoSingleton(const InfoSingleton&) = delete;
	InfoSingleton& operator=(const InfoSingleton& info) = delete;
	map<string, int> _info;
	// ...
private:
};

int main()
{
    
    
	InfoSingleton::GetInstance().Insert("张三", 10000);
	InfoSingleton& infosl = InfoSingleton::GetInstance();
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);

	infosl.Print();
	cout << endl;
	InfoSingleton::GetInstance().Insert("张三", 18000);
	infosl.Insert("李四", 12000);
	infosl.Insert("王五", 15000);
	infosl.Insert("赵六", 11000);
	infosl.Print();
	return 0;
}

Guess you like

Origin blog.csdn.net/NEFUT/article/details/131627873