The implementation of StrVec class in dynamic memory management of C++Primer

Design of StrVec class
The vector class stores its elements in contiguous memory. In order to achieve acceptable performance, vector allocates enough memory in advance to hold more elements that may be needed. The member function of each added element of vector checks to see if there is room for more elements. If so, the member function constructs an object in the next available location. If there is no space available, vector reallocates the space: it takes the new space, moves the existing elements to the new space, frees up the old space, and adds elements.
A similar strategy is adopted in the StrVec class, which uses allocator to obtain the original memory. Since the memory allocated by allocator is not constructed, we will use the construct member of allocator to create objects in the original memory when we need to add new elements, and use the destroy member to destroy when we need to delete elements.
Each StrVec has three pointer members pointing to the memory used by its elements:
1. elements, pointing to the first element in allocated memory
2. First? Free, pointing to the position after the last actual element
3. cap, pointing to the location after the end of the allocated memory
StrVrc also has a static member called alloc, which is of type allocator < string >. The alloc member allocates the memory used by StrVec. Our class also has four utility functions:
1. Alloc ABCD copy allocates memory and copies elements in a given range
2. free destroys the constructed elements and frees memory
3. Chk ABCD alloc ensures that StrVec has space for at least one new element. If there is no space to add new elements, chk ﹣ n ﹣ alloc will call reallocate to allocate more memory
4. reallocate allocates new memory for StrVec when memory runs out
StrVec class definition

//Simplified implementation of memory allocation strategy of class vector
class StrVec{
public:
	StrVec():elements(nullptr), first_free(nullptr), cap(nullptr){}  //Default initialization
	StrVec(const StrVec&);   //copy constructor    
	StrVec &operator=(const StrVec&);   //copy assignment operator 
	~StrVec();   
	void push_back(const std::string&);   //Copy element
	size_t size() const {return first_free - elements;}
	size_t capacity() const {return cap - elements;}
	std::string *begin() const {return elements;} 
	std::string *end() const {return first_free;}	
private:
	static std::allocator<std::string> alloc;   //Distribution elements
	//Used by the function of the added element
	void chk_n_alloc(){
	if(size() == capacity()) reallocate();}
	//Utility functions used by copy constructors, assignment operators, and destructors
	std::pair<string*, string*> alloc_n_copy(const string*, const string*);
	void free();         //Destroy elements and release internal training
	void reallocate();   //Get more memory and copy existing elements
	string *elements;
	string *first_free;
	string *cap;
}

Using construct
The function push back calls chk n alloc to make sure there is room for new elements. When the function is changed to return, push back knows that there must be room for new elements. It requires allocator members to construct new tail elements:

void StrVec::push_back(const string &s){
	chk_n_alloc();
	alloc.construct(first_free++, s);
}

When we allocate memory with allocator, we must remember that memory is not constructed. In order to use the modified memory, you must use construct to construct an object in that memory. The first parameter passed to construct must be a pointer. Points to the unstructured memory space allocated by allocate. The remaining parameters determine which constructor to use to construct the object. In this case, there is only one extra parameter of type string, so a copy constructor of string will be used.
Calling construct constructs an object at the address currently specified by first_free and increments first_free to point to the next element that is not constructed.
Alloc ABCD copy member
The alloc ABCD copy member allocates enough memory to hold the elements of a given range and copies them to the newly allocated memory. This function returns the pair of one pointer, which points to the start position and the end position of the new space respectively:

std::pair<string*, string*> alloc_n_copy(const string *b, const string *e){
	//Allocate space to hold elements in a given range
	auto data = alloc.allocate(e - b);
	//Initialize and return a pair consisting of the return values of data and uninitialized_
	return {data, uninitialized_copy(b, e, data)};
}

It completes the copy work in the return statement, which initializes the return value list. The first member of the return pair points to the beginning of the allocated memory; the second member is the return value of uninitialized_copy, which is a pointer to the position after the last construction element.
free() members
The free() member has two responsibilities, first the destroy element, and then freeing the memory space allocated by StrVec itself. for loop calls the destroy member of allocator. From the end of the construction to the first element, destroy all elements in reverse order:

void StrVec::free(){
	if (elements){
		for (auto p = first_free; p != elements; p--)
			alloc.destroy(p);
		alloc.deallocate(elements, cap - elements);  //Free allocated memory
	}
}

The pointer passed to deallocate must be the pointer returned by a previous allocate call. Therefore, before calling deallocate, we first check whether the elements are empty.
Copy control member
The copy constructor calls alloc ﹣ n ﹣ copy:

StrVec::StrVec(const StrVec &s){
	//Call alloc > copy to allocate space to hold as many elements as in s
	auto newdata = alloc_n_copy(s.begin(), s.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}

The return value of alloc_n_copy is the pair of a pointer. Its first member points to the element of the first construct, and its second member points to the position after the element of the last construct. Since the allocated space of alloc ﹣ n ﹣ copy exactly holds the given element, cap also points to the position after the last construction element.
Destructor call free:

StrVec::~StrVec(){free();}

The copy assignment operator calls alloc_n_copy before it is released, so that the assignment can be processed correctly.

StrVec& StrVec::operator=(const StrVec &rhs){
	auto data = alloc_n_copy(s.begin(), s.end());
	free();   //free lose oneself
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}

Similar to the copy constructor, the copy assignment operator initializes its pointer with the return value of alloc ﹐ n ﹐ copy

Move rather than copy elements during memory reallocation
Functions that reallocate member functions should implement:
1. Allocate memory for a new and larger string array
2. Construct objects in the previous part of memory space to save existing elements
3. Destroy the elements in the original memory space and release this memory
String has class value behavior. When copying a string, memory space must be allocated for these characters, while destroying a string must release the occupied memory.
The reallocate member function copies the strings in StrVec. After copying, each string has a unique user. Once the old elements are copied to the new space, we will destroy the original string immediately.
Therefore, copying the data in these strings is redundant. When reallocating memory space, if we can avoid the extra overhead of allocating and releasing strings, the performance of StrVec will be much better.

Move constructor and std::move
Move constructors typically "move" resources rather than copying them to the object being created. Moreover, the standard library ensures that the original string remains in an effective and destructable state. For strings, we can imagine that each string has a pointer to a char array. It can be assumed that the move constructor of string copies the pointer instead of allocating memory for the character and then copying the character.
The standard library functions for move are defined in the utility header file.
1. When reallocate constructs a string in new memory, it must call move to indicate that it wants to use the string's move constructor. If the move call is omitted, the copy constructor of string will be used
2. When using move, call std::move instead of move

reallocate members
First call allocate to allocate the new memory space. Every time we reallocate memory, we double the capacity of StrVec. If StrVec is empty, we will allocate space to hold an element:

void StrVec::reallocate() {
	//We will allocate twice the current size of memory
	auto newcapacity = size() ? 2 * size() : 1;
	//Allocate new memory
	auto newdata = alloc.allocate(newcapacity);
	//Move data from old memory to new memory
	auto dest = newdata;     //Points to the next free position in the new array
	auto elem = elements;    //Points to the next element in the old array
	for (size_t i = 0; i != size(); ++i) {
		alloc.construct(dest++, std::move(*elem++));
	}
	free();   //Free up old memory after move
	//Update data structure, execute new elements
	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}

a.construct(p, args): p must be a pointer of type T *, pointing to a block of original memory. arg is passed to the constructor of type T, which is used to construct an object in the memory pointed by p.

Exercise 13.39: write your own version of StrVec, including your own versions of reserve, capacity, and resize

//capacity
size_t capacity() const { return cap - elements; }
//reserve reserves some space. reallocate() member function is required
void reserve(size_t n) { if (n > capacity()) reallocate(n); }
//There are two versions of resize, the difference is without / with initial value
void StrVec::resize(size_t n) {
	if (n > size()) {
		while (size() < n)
			push_back("");
	}
	else if (n < size()) {
		while (size() > n)
			alloc.destroy(--first_free);
	}
}
//Add object
inline void StrVec::resize(size_t n, const std::string& s) {
	if (n > size()) {
		while (size() < n)
			push_back(s);
	}
}

Exercise 13.40: add a constructor to the StrVec class that takes an initializer_list < string > parameter

StrVec::StrVec(initializer_list<string> il) {
	//Call "alloc" to allocate as much space as the element array in the list il
	auto newdata = alloc_n_copy(il.begin(), il.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}

The complete StrVec class code is as follows:

#include <iostream>
#include <utility>
#include <string>
#include <memory>
using namespace std;
//Simplified implementation of memory allocation strategy of class vector
class StrVec {
public:
	StrVec() :elements(nullptr), first_free(nullptr), cap(nullptr) {}  //Default initialization
	StrVec(const StrVec&);   //copy constructor    
	StrVec(initializer_list<string> il);
	StrVec& operator=(const StrVec&);   //copy assignment operator 
	~StrVec();
	void push_back(const std::string&);   //Copy element
	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }
	// reserve reserves some space. reallocate() member function is required
	void reserve(size_t n) { if (n > capacity()) reallocate(n); } // reserve reserves some space. reallocate() member function is required
	std::string* begin() const { return elements; }
	std::string* end() const { return first_free; }
private:
	static allocator<string> alloc;   //Distribution elements
	//Used by the function of the added element
	void chk_n_alloc() {
		if (size() == capacity()) reallocate();
	}
	//Utility functions used by copy constructors, assignment operators, and destructors
	pair<string*, string*> alloc_n_copy(const string*, const string*);
	void free();         //Destroy elements and release internal training
	void reallocate();   //Get more memory and copy existing elements
	void reallocate(size_t n);
	void resize(size_t n);
	void resize(size_t n, const std::string& s);
	string* elements;
	string* first_free;
	string* cap;
};

inline void StrVec::push_back(const string& s) {
	chk_n_alloc();
	alloc.construct(first_free++, s);
}

inline pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e) {
	//Allocate space to hold elements in a given range
	auto data = alloc.allocate(e - b);
	//Initialize and return a pair consisting of the return values of data and uninitialized_
	return { data, uninitialized_copy(b, e, data) };
}
inline void StrVec::free() {
	if (elements) {
		for (auto p = first_free; p != elements; p--)
			alloc.destroy(p);
		alloc.deallocate(elements, cap - elements);  //Free allocated memory
	}
}
inline StrVec::StrVec(const StrVec& s) {
	//Call alloc > copy to allocate space to hold as many elements as in s
	auto newdata = alloc_n_copy(s.begin(), s.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}
inline StrVec::StrVec(initializer_list<string> il) {
	//Call "alloc" to allocate as much space as the element array in the list il
	auto newdata = alloc_n_copy(il.begin(), il.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}
inline StrVec::~StrVec() { free(); }
inline StrVec& StrVec::operator=(const StrVec& rhs) {
	auto data = alloc_n_copy(rhs.begin(), rhs.end());
	free();   //free lose oneself
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}
inline void StrVec::reallocate() {
	//We will allocate twice the current size of memory
	auto newcapacity = size() ? 2 * size() : 1;
	//Allocate new memory
	auto newdata = alloc.allocate(newcapacity);
	//Move data from old memory to new memory
	auto dest = newdata;     //Points to the next free position in the new array
	auto elem = elements;    //Points to the next element in the old array
	for (size_t i = 0; i != size(); ++i) {
		alloc.construct(dest++, std::move(*elem++));
	}
	free();   //Free up old memory after move
	//Update data structure, execute new elements
	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}
inline void StrVec::reallocate(size_t newcapacity) {
	//Allocate new memory
	auto newdata = alloc.allocate(newcapacity);
	//Move data from old memory to new memory
	auto dest = newdata;     //Points to the next free position in the new array
	auto elem = elements;    //Points to the next element in the old array
	for (size_t i = 0; i != size(); ++i) {
		alloc.construct(dest++, std::move(*elem++));
	}
	free();   //Free up old memory after move
	//Update data structure, execute new elements
	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}
//reserve reserves some space. reallocate() member function is required
//void reserve(size_t n) { if (n > capacity()) reallocate(n); }
//There are two versions of resize, the difference is without / with initial value
inline void StrVec::resize(size_t n) {
	if (n > size()) {
		while (size() < n)
			push_back("");
	}
	else if (n < size()) {
		while (size() > n)
			alloc.destroy(--first_free);
	}
}
//Add object
inline void StrVec::resize(size_t n, const std::string& s) {
	if (n > size()) {
		while (size() < n)
			push_back(s);
	}
}

Exercise 13.41: in push back, why do we post increment in the construct call? What happens if we pre increment?

Because first_free points to the first free location, which is the last element of the last string, you should use the post increment operator
 If the pre increment operation is used, the first UU free points to the last string, which is not consistent with the setting of the first UU free

Exercise 13.43: rewrite the free member and replace the for loop destroy element with for each() and lambda. Which implementation do you prefer?

for_each(elements, first_free, [](std::string &s){alloc.destroy(&s);});
//The address of s should be taken in lambda to call destory
Published 65 original articles, won praise 4, visited 955
Private letter follow

Keywords: Lambda

Added by kellog on Sun, 19 Jan 2020 06:01:36 +0200