Special class design, singleton mode

Keywords: C++ Interview Singleton pattern

catalogue

1, Please design a class that can only create objects on the heap

2, Designing a class can only create objects on the stack

3, Design a class that cannot be copied

4, Designing a class cannot be inherited

5, When designing a class, only one object can be instantiated

        five point one   The singleton mode consists of two implementation modes

         5.2 comparison between lazy man mode and hungry man mode

1, Please design a class that can only create objects on the heap

        Idea: we can only create objects on the heap. Note: we can't create objects outside at will, so we need to provide an external interface for the class to create an object on the heap. Here are two points to note:

  1. You cannot create objects outside at will.
  2. Class provides an interface for us to create objects on the heap.

         Private members can be called in a class.

You cannot create objects outside:

        Creating an object must call the constructor of the class, so we need to disable the constructor. In C++98, the constructor is set to private. The constructor cannot use delete in C++11. new also needs to call the constructor.

        We need to disable not only the constructor, but also the copy constructor, because after the object is created, the construction object can also be copied.

        In order not to be troublesome, the copy constructor is only declared and not defined. The constructor cannot be declared only. The new object needs to call the constructor.

Class provides an interface. After the object is created on the heap, it will be given to us:

        The member function needs to be set as a static member function. Because the member function in the class needs to be used to create the object, there is no class object outside the class. If you need to call the member function, you must set the function as a static member function, belonging to the whole class.

#include<iostream>
using namespace std;

class HeadOnly{

public:
	//Provide an interface to create an object return on the heap
	static HeadOnly* CreateObj(){
		return new HeadOnly();
	}

	//C++11, the constructor cannot be delete d, and new also needs to call the constructor
	//HeadOnly(const HeadOnly& hd) = delete;
private:
	//C++98 makes the construction private, and new also needs to call the constructor
	HeadOnly()
	{

	}
	//C++98 declares the copy construct private
	HeadOnly(const HeadOnly& hd);
};

int main(){

	HeadOnly* hd = HeadOnly::CreateObj();

	return 0;
}

2, Designing a class can only create objects on the stack

  • Idea 1: ibid

You cannot create objects on the heap outside, you can create objects on the stack

        When creating new objects on the heap, the constructor will be called. We need to disable the constructor outside. That is, make the constructor private.

        Constructors are private, and objects cannot be constructed directly outside the class. However, constructors can be called within a class.

        Copy constructors cannot be disabled. The member function returns the construction object that needs to be copied.

Class provides an interface to create objects on the stack for us:

          The member function needs to be set as a static member function for the same reason as above. There is no object outside, so the member function needs to be set to belong to the whole class.

class StackOnly{

public:
	//Provide an interface to create an object return on the stack
	static StackOnly CreateObj(){
		return StackOnly();
	}

private:
	//C++98 makes the construction private, and new also needs to call the constructor
	StackOnly()
	{

	}

};

int main(){
	//Copy construct needs to be called
	StackOnly st = StackOnly::CreateObj();

	return 0;
}
  • Idea 2: shield new

        The new bottom layer calls the global function operator new function. You only need to mask the operator new function.

How to mask operator new?

        If you customize operator new in the class and set it to private, the global operator new function will not be called.

        However, there is a drawback that objects can be defined in static areas.

class StackOnly{

public:

private:
	//Custom operator new function
	void* operator new(size_t size)
	{};

};
int main(){

	StackOnly st1;

	//Defect, object defined in static area
	static StackOnly st2;

	return 0;
}

3, Design a class that cannot be copied

        Copy mainly includes two aspects: copy constructor and assignment operator overload function. Therefore, you only need to disable the two functions.

class CopyBan{
public:
	//C++11, set to delete
	//CopyBan(CopyBan& cb) = delete;
	//CopyBan& operator=(const CopyBan& cb) = delete;

private:
	//C++98, set to private
	CopyBan(CopyBan& cb);
	CopyBan& operator=(const CopyBan& cb);

};

4, Designing a class cannot be inherited

  • C++98 sets the constructor to private. The derived class cannot call the constructor of the base class and cannot inherit.
class NoInherit{

public:
	//Provide an interface to create an object return on the stack
	static NoInherit CreateObj(){
		return NoInherit();
	}

private:

	NoInherit()
	{

	}

};
  • C++11, modify the class with the final keyword and cannot be inherited
class NoInherit final{
	//...
};

5, When designing a class, only one object can be instantiated

Singleton mode: a class can only instantiate one object.

This pattern can ensure that there is only one instance of this class in the system and provide a global access point to access it.

For example, in a server program, the configuration information of the server is stored in a file. These configuration data are uniformly read by an instance object, and then other objects of the service process obtain these configuration files through this single instance object. This way simplifies the configuration management in a complex environment.

        five point one   The singleton mode consists of two implementation modes

  • Hungry man model

        Hungry man mode: the object of the instance is created when the program starts.

        Pay attention to several details:

  1. The constructor, copy constructor, assignment operator and overloaded function need to be shielded to prevent external instantiation of objects.
  2. Create a static class object in the class, which will be created when the program runs. Needs to be initialized outside the class.
  3. Write an interface in the class to return the address of the static class object. You cannot directly return the class object or object reference, and you cannot call the copy construct.
  4. Member functions also need to be written as static member functions because there are no objects outside the class.
class SingelTon{
public:
	static SingelTon* GetInstance(){
		return &st;
	}
private:
	//Create a static object
	static SingelTon st;

	//Mask constructors, copy constructs, assignment overloaded operators
	SingelTon(){};
	SingelTon(const SingelTon& st);

	SingelTon& operator=(const SingelTon& st);
};

SingelTon SingelTon::st;//initialization

int main(){
	SingelTon *st = SingelTon::GetInstance();

	return 0;
}
  • Lazy mode

         Lazy mode: create objects when needed.

Details of lazy mode:

  • The constructor, copy constructor, assignment operator and overloaded function need to be shielded to prevent external instantiation of objects.
  • In a class, a member contains a class object pointer, not an object.
  • Class. When the class object is not created, the created class object returns.
static SingelTon* GetInstance(){
			
	if (st == nullptr){//Objects are created only when they are not created
		st = new SingelTon();
	}
				
	return st;
}
  • Member functions also need to be written as static member functions because there are no objects outside the class.

Thread safety issues in lazy mode:

        When st is empty, one thread enters the judgment. Before the object is created, another thread enters the judgment. Because the object is not created, the judgment is successful, and you can also enter the judgment code. At this time, two spaces are applied on the heap, but st only points to one space. When the object is destroyed, only one space will be released. Find out that the space applied by other threads is not released, resulting in memory leakage.

        In the case of multithreading, because the judgment is not atomic and st belongs to critical resources. In order to save that when a thread judges and applies for resources, other threads will not enter. It needs to lock before judgment and unlock after applying for resources.

        Locks also need to be defined as static and belong to the whole class. Let the thread see a lock.

static SingelTon* GetInstance(){
		
		mt.lock();
		if (st == nullptr){//Objects are created only when they are not created
			st = new SingelTon();
		}
		mt.unlock();
		
	return st;
}

Optimization:

        As long as one thread applies for an object, other threads do not need to lock and unlock. Locking will lead to thread blocking, and locking and unlocking also need a price. In order to improve efficiency, judge whether the object has been applied before locking.

	static SingelTon* GetInstance(){
		if (st != nullptr){//Prevent frequent locking and affect efficiency
			mt.lock();
			if (st == nullptr){//Objects are created only when they are not created
				st = new SingelTon();
			}
			mt.unlock();
		}
		return st;
	}

Overall Code:

#include<mutex>
class SingelTon{
private:
	SingelTon(){};
	SingelTon(const SingelTon& st);
	SingelTon& operator=(const SingelTon& st);
	//Declare a class pointer
	static SingelTon* st;
	static mutex mt;//lock
public:
	//Create objects as needed
	static SingelTon* GetInstance(){
		if (st != nullptr){//Prevent frequent locking and affect efficiency
			mt.lock();
			if (st == nullptr){//Objects are created only when they are not created
				st = new SingelTon();
			}
			mt.unlock();
		}
		return st;
	}

	//Deconstruction
	~SingelTon(){
		//Free up space
		if (st){
			delete st;
		}
	}
};
//Static member, initialized outside the class
SingelTon* SingelTon::st = nullptr;
mutex SingelTon::mt;


int main(){

	SingelTon *st = SingelTon::GetInstance();

	return 0;
}

ps: when applying for resources here, new may fail and throw exceptions. The lock is not released. We can use RAII thought, unique_lock() to manage lock resources.

	static SingelTon* GetInstance(){
		if (st != nullptr){//Prevent frequent locking and affect efficiency

			unique_lock<mutex>(mt);//Using unique_lock manages locks to prevent new throwing exceptions

			if (st == nullptr){//Objects are created only when they are not created
				st = new SingelTon();
			}
			
		}
		return st;
	}

         5.2 comparison between lazy man mode and hungry man mode

Hungry man model

advantage:

  • The implementation is simple and does not need to consider thread safety.

Disadvantages:

  • Slow start. If the object of the instance occupies a lot of resources, it needs to be loaded at startup.
  • If there are multiple singleton class objects, the order of instance objects is uncertain at startup. If there are dependencies between objects, it's troublesome.

Lazy mode

advantage:

  • Start fast. Objects are instantiated and resources are loaded when needed.
  • For multiple singleton class objects, the instantiation order can be determined. Depends on the order in which the functions of the class are called.

Disadvantages:

  • The implementation is complex. Thread safety needs to be considered.

Posted by jhenary on Sat, 30 Oct 2021 21:30:46 -0700