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"