737-C++STL - principle of container space allocator

Keywords: C++ Container STL

Implement a simple vector container

The implementation of all C++ STL containers needs to rely on a spatial configurator allocator. Although we don't pay attention to it when using containers, we have been using it all the time. The C++ STL library provides a simple implementation of the default spatial configurator allocator. Of course, we need to deeply understand the principle of spatial configurator, Then you can provide a custom allocator.

Let's look at the implementation of a simple vector container. The code is as follows:

#include <iostream>
using namespace std;
/*
This article mainly describes the space configurator, so the vector method is relatively simple,
The method does not provide the corresponding moving function with R-value reference parameters, and does not consider too much
 Abnormal conditions
*/
template<typename T>
class Vector
{
public:
	//Constructor
	Vector(int size = 0)
		:mcur(0), msize(size)
	{
		mpvec = new T[msize];
	}
	//Destructor
	~Vector()
	{
		delete[]mpvec;
		mpvec = nullptr;
	}
	//copy constructor 
	Vector(const Vector<T>& src)
		:mcur(src.mcur), msize(src.msize)
	{
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
	}
	//Assignment overloaded function
	Vector<T>& operator=(const Vector<T>& src)
	{
		if (this == &src)
			return *this;

		delete[]mpvec;

		mcur = src.mcur;
		msize = src.msize;
		mpvec = new T[msize];
		for (int i = 0; i < msize; ++i)
		{
			mpvec[i] = src.mpvec[i];
		}
		return *this;
	}
	//Tail insert data function
	void push_back(const T& val)
	{
		if (mcur == msize)
			resize();
		mpvec[mcur++] = val;
	}
	//Tail delete data function
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
	}
private:
	T* mpvec;//A dynamic array that holds the elements of the container
	int mcur;//Save the number of currently valid elements
	int msize;//Total length of storage container after expansion

	//Double expansion function of container
	void resize()
	{
		/*The default vector object is constructed. The memory expansion is twice that from 0-1-2-4-8-16-32 -
		For capacity expansion, the initial memory usage efficiency of the vector container is particularly low, and reserve can be used
		The reserved space function provides the efficiency of the container.*/
		if (msize == 0)
		{
			mpvec = new T[1];
			mcur = 0;
			msize = 1;
		}
		else
		{
			T* ptmp = new T[2 * msize];
			for (int i = 0; i < msize; ++i)
			{
				ptmp[i] = mpvec[i];
			}
			delete[]mpvec;
			mpvec = ptmp;
			msize *= 2;
		}
	}
};

The above is the code implementation of a simple vector container. The following code uses the above vector:

//Test class A
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
};
int main()
{
	Vector<A> vec(10);//10 represents the size of the space opened up by the bottom layer, but 10 A objects are constructed
	A a1, a2, a3;
	cout << "---------------" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "---------------" << endl;
	vec.pop_back(); // Deleting a3 does not destruct the object
	cout << "---------------" << endl;

	//When the vec container is destructed, there are only 2 valid A objects inside, but it is destructed 10 times
	return 0;
}


The comments on the running results are resolved as follows:

A()
A()
A()
A()
A()
A()
A()
A()
A()
A() //As mentioned above, 10 objects are constructed in the vector container
A() //Here we start with the following three a constructors, which construct A1, A2 and A3 objects
A()
A()
+++++++++++++++
+++++++++++++++
+++++++++++++++ //There's A problem here, vec.pop_back() deletes the end A object, but does not make A destruct call, which may cause resource leakage
~A()
~A()
~A() //The three destructors above here destruct A1, A2 and A3 objects
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A() //The above 10 destructors are used to destruct all the objects in the vector container

Container problems

The above code has three problems as follows:
1. When defining the container, vector < A > VEC (10), we hope that the bottom layer can open up A space that can accommodate 10 elements. We don't need to construct 10 A objects for me, because I don't intend to add data to the container at this time. So many constructor calls are A waste of efficiency.

2. Vec.pop when deleting an element from A container_ Back(), which means that object A at the end of the container is deleted, but the destructor of object A is not called. If object A occupies external resources, the resource release code must be in the destructor of object A, which causes the problem of resource leakage.

3. When the vec container destructs the function scope, it does not destruct the valid a object. In fact, in the above code, there are only two valid objects a1, a2 and a3 in the vec container. They should be destructed only twice, but they are destructed 10 times, which is unreasonable.

Our solution to these three problems is as follows:
1. When defining a container, you should only apply for memory without constructing so many invalid objects. When adding elements to the container, you can reconstruct the objects at the corresponding positions.

2. When deleting elements from the container, you should not only do -- mcur, but also call the destructor of the deleted object to release the external resources occupied by the object.

3. When the container vec is destructed, the valid objects in the container should be destructed, and then the memory resources of the whole container should be released.

Based on the above solutions, we have such a demand:
1. Separate the two procedures of object memory development and constructor call
2. Separate the two processes of object deconstruction and memory release

Therefore, at this time, new and delete cannot be used directly in the container, because new can not only open up memory, but also automatically call the constructor to construct objects; Delete first destructs the object, and then releases memory. So, who will complete the above requirement task?
Space allocator for container

Introduction to space Configurator

The core function of the space configurator is to decompose the process of object memory development and object construction, and the process of object decomposition and memory release. Therefore, the space configurator mainly provides the following four functions:

The following provides an implementation of space configurator code similar to that in C++ STL library

//Custom space Configurator
template<typename T>
struct myallocator
{
	//Open up memory space
	T* allocate(size_t size)
	{
		return (T*)::operator new(sizeof(T) * size);//It is equivalent to malloc allocating memory
	}
	//Free up memory space
	void deallocate(void* ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T) * size);//free is equivalent to freeing memory
	}
	//Responsible for object construction
	void construct(T* ptr, const T& val)
	{
		new ((void*)ptr) T(val);//Constructs an object on the specified memory by locating new
	}
	//Responsible for object deconstruction
	void destroy(T* ptr)
	{
		ptr->~T();//Displays the destructor of the calling object
	}
};

The space configurator implemented above is relatively simple. The memory management still uses operator new and operator delete, which is actually the memory management of malloc and free. Of course, we can also attach a memory pool implementation to the allocator, which is equivalent to customizing the memory management method.

Take a look at the class template definition header of the vector container in the C++ STL library. The code is as follows:

template<class _Ty,
	class _Alloc = allocator<_Ty>>
	class vector

As can be seen from the above vector container class template definition, it has two template type parameters_ Ty is the type of data stored in the container_ Alloc is the type of space configurator. If the user does not customize it, the default allocator in the library will be used, which is similar to the implementation of the space configurator code provided above.

Implement vector container with space Configurator

Add the vector code provided at the beginning to our own space configurator allocator, and the modified code is as follows:

#include <iostream>
using namespace std;

//Custom space Configurator
template<typename T>
struct myallocator
{
	//Open up memory space
	T* allocate(size_t size)
	{
		return (T*)::operator new(sizeof(T) * size);//It is equivalent to malloc allocating memory
	}
	//Free up memory space
	void deallocate(void* ptr, size_t size)
	{
		::operator delete(ptr, sizeof(T) * size);//free is equivalent to freeing memory
	}
	//Responsible for object construction
	void construct(T* ptr, const T& val)
	{
		new ((void*)ptr) T(val);//Constructs an object on the specified memory by locating new
	}
	// Responsible for object deconstruction
	void destroy(T* ptr)
	{
		ptr->~T();//Displays the destructor of the calling object
	}
};

/*
Add a space configurator allocator to the implementation of the Vector container
*/
template<typename T, typename allocator = myallocator<T>>
class Vector
{
public:
	//Constructor, you can pass in a self-defined space configurator, otherwise use the default allocator
	Vector(int size = 0, const allocator& alloc = allocator())
		:mcur(0), msize(size), mallocator(alloc)
	{
		//Only the bottom space of the container is opened up, and no objects are constructed
		mpvec = mallocator.allocate(msize);
	}
	//Destructor
	~Vector()
	{
		//Destruct the object in the container first
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec + i);
		}
		//Free heap memory occupied by container
		mallocator.deallocate(mpvec, msize);
		mpvec = nullptr;
	}
	//copy constructor 
	Vector(const Vector<T>& src)
		:mcur(src.mcur)
		, msize(src.msize)
		, mallocator(src.mallocator)
	{
		//Only the bottom space of the container is opened up, and no objects are constructed
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			//Construct an object with the value src.mpvec[i] at the specified address mpvec+i
			mallocator.construct(mpvec + i, src.mpvec[i]);
		}
	}
	//Assignment overloaded function
	Vector<T> operator=(const Vector<T>& src)
	{
		if (this == &src)
			return *this;

		//Destruct the object in the container first
		for (int i = 0; i < mcur; ++i)
		{
			mallocator.destroy(mpvec + i);
		}
		//Free heap memory occupied by container
		mallocator.deallocate(mpvec, msize);

		mcur = src.mcur;
		msize = src.msize;
		//Only the bottom space of the container is opened up, and no objects are constructed
		mpvec = mallocator.allocate(msize);
		for (int i = 0; i < mcur; ++i)
		{
			//Construct an object with the value src.mpvec[i] at the specified address mpvec+i
			mallocator.construct(mpvec + i, src.mpvec[i]);
		}
		return *this;
	}
	//Tail insert data function
	void push_back(const T& val)
	{
		if (mcur == msize)
			resize();
		mallocator.construct(mpvec + mcur, val);
		mcur++;
	}
	//Tail delete data function
	void pop_back()
	{
		if (mcur == 0)
			return;
		--mcur;
		//Destruct the deleted object
		mallocator.destroy(mpvec + mcur);
	}
private:
	T* mpvec;//A dynamic array that holds the elements of the container
	int mcur;//Save the number of currently valid elements
	int msize;//Total length of storage container after expansion
	allocator mallocator;//Defines the space configurator object for the container

	//Double expansion function of container
	void resize()
	{
		if (msize == 0)
		{
			mpvec = mallocator.allocate(sizeof(T));
			mcur = 0;
			msize = 1;
		}
		else
		{
			T* ptmp = mallocator.allocate(2 * msize);
			for (int i = 0; i < msize; ++i)
			{
				mallocator.construct(ptmp + i, mpvec[i]);
			}
			//Destruct the object in the container first
			for (int i = 0; i < msize; ++i)
			{
				mallocator.destroy(mpvec + i);
			}
			//Free heap memory occupied by container
			mallocator.deallocate(mpvec, msize);
			mpvec = ptmp;
			msize *= 2;
		}
	}
};
//Test class A
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
};
int main()
{
	Vector<A> vec(10);//Only memory is opened up here, and no objects are constructed
	A a1, a2, a3;
	cout << "+++++++++++++++" << endl;
	vec.push_back(a1);
	vec.push_back(a2);
	vec.push_back(a3);
	cout << "+++++++++++++++" << endl;
	vec.pop_back();//Delete a3 and destruct a3 objects
	cout << "+++++++++++++++" << endl;

	//When the vec container is destructed, there are only two valid A objects inside. It has been destructed twice. It is correct
	return 0;
}


The operation results are analyzed as follows:

A()
A()
A() //The top three are a, A1, A2, A3; Printing of objects on three stacks
+++++++++++++++
+++++++++++++++
~A() //This is vec.pop_back() destructs the a3 object
+++++++++++++++
~A()
~A()
~A() //The top three are a, A1, A2, A3; Destruct call of three objects
~A()
~A() //The above two are the destructors of two A objects in the vec container

Through printing, we can see the three problems we mentioned in the first container:
1. When defining the container, vector < A > VEC (10), we hope that the bottom layer can open up A space that can accommodate 10 elements. We don't need to construct 10 A objects for me, because I don't intend to add data to the container at this time. So many constructor calls are A waste of efficiency.
2. Vec.pop when deleting an element from A container_ Back(), which means that object A at the end of the container is deleted, but the destructor of object A is not called. If object A occupies external resources, the resource release code must be in the destructor of object A, which causes the problem of resource leakage.
3. When the vec container destructs the function scope, it does not destruct the valid a object. In fact, in the above code, there are only two valid objects a1, a2 and a3 in the vec container. They should be destructed only twice, but they are destructed 10 times, which is unreasonable.

Now it is solved through the space configurator allocator. Carefully compare the code implementation of the initial Vector and the modified Vector with the space configurator version to experience the specific use of allocator in the container.

SGI STL is another implementation version of standard STL, which is widely used. It has built-in implementation of primary and secondary space configurators, in which the secondary space configurator carries the implementation of a memory pool. For the implementation of memory pool, see my blog column "analysis of memory pool"

Posted by robin on Sat, 30 Oct 2021 06:31:20 -0700