📔 C++ Primer 0x0D exercise solution
Better reading experience (real-time update and correction)
13.1 copy, assignment and destruction
13.1.1 copy constructor
13.1 what is a copy constructor? When to use it?
If the first parameter of a constructor is a reference to its own class type (and generally a reference to const), and any additional parameters have default values, this constructor is a copy constructor
The copy constructor is used when the copy is initialized
Copy initialization occurs not only when we define variables with =, but also in the following cases
- Pass an object as an argument to a formal parameter of a non reference type
- Returns an object from a function whose return type is a non reference type
- Initializes elements in an array or members in an aggregate class with a curly brace list
- Some types also use copy initialization for their assigned objects (insert and push will copy initialization, and empty will initialize directly)
13.2 explain why the following statement is illegal:
Sales_data::Sales_data(Sales_data rhs);
The parameter should be a reference of its own type
13.3 what happens when we copy a StrBlob? Copy a StrBlobPtr?
StrBlob uses shared_ptr reference count plus one
StrBlobPtr uses weak_ptr reference count does not increase
Direct copy of other data members
13.4 assuming that Point is a class type and has a public copy constructor, indicate where the copy constructor is used in the following program fragments:
Point global; Point foo_bar(Point arg) // 1 { Point local = arg, *heap = new Point(global); // 2,3 *heap = local; //4 Point pa[4] = { local, *heap }; // 5, 6 return *heap; // 7 }
- Pass an object as an argument to a formal parameter of a non reference type (1)
- Use = to define variables (2,3,4)
- Initializes elements in an array or members in an aggregate class with a curly bracket list (used twice in 5 and 6)
- Return an object from a function whose return type is non reference type (7)
13.5 given the following class framework, write a copy constructor to copy all members. Your constructor should dynamically allocate a new string and copy the object to the location pointed by ps instead of ps itself:
class HasPtr { public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } private: std::string *ps; int i; }
#include <string> class HasPtr { public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){} private: std::string *ps; int i; };
13.1.2 copy assignment operator
13.6 what is the copy assignment operator? When to use it? What does the composite copy assignment operator do? When will the composite copy assignment operator be generated?
- The copy operator is an overload of the function operator =, and the copy assignment operator accepts a parameter of the same type as its class
The copy assignment operator is used when the assignment operation is used
Composite copy assignment operator
- If a class does not define its own copy assignment operator, the compiler will produce a composite copy assignment operator for it
- For some classes, the composite copy assignment operator is used to prohibit the assignment of objects of this type
- Generally, the copy assignment operator will assign each non static member of the right operand to the corresponding member of the left operand. This is done through the copy assignment operator of member type. For members of array type, assign array elements one by one
- The composite copy assignment operator returns a reference to the left operand
13.7 what happens when we assign one StrBlob to another? What about StrBlobPtr?
StrBlob: through shared_ The copy assignment operator of PTR class will be shared_ptr copy assignment, and its counter increases automatically.
StrBlobPtr: pass weak_ The copy assignment operator of PTR class will break_ PTR copy assignment. curr calls the assignment operator of the built-in type.
13.8 write assignment operators for the HasPtr class in exercise 13.5 in section 13.1.1. Similar to the copy constructor, your assignment operator should copy the object to the position pointed to by ps.
Remember to deal with self assignment and exception safety. This is the way to write the class value copy assignment operator
#include <string> class HasPtr { public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){} HasPtr& operator=(const HasPtr& rhs){ auto newp = new std::string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } ~HasPtr() { delete ps;} private: std::string *ps; int i; };
13.9 what is a destructor? What does the synthetic destructor do? When will the composite destructor be generated?
- Destructor: releases the resources used by the object and destroys the non static data members of the object
- A destructor is a member function of a class. Its name is tilde followed by the class name. It has no return value and does not accept parameters. It cannot be overloaded and is unique to a given class
What does the destructor do
- In the destructor, the function body is executed first, and then the members are destroyed. The members are destroyed in the reverse order of initialization
- Unlike constructors with initialization lists, the destructor is implicit. What happens when a member is destroyed depends entirely on the type of the member
- Destroy the members of the class type and execute the destructor of the class type
- There is no destructor for built-in types. There is no need to do anything to destroy built-in type members
- Implicitly destroying a member of a built-in pointer type does not delete the object it points to
- Unlike ordinary pointers, smart pointers are class types, so they have destructors. Smart pointer members are automatically destroyed during the destruct phase
Synthetic destructor
- When a class does not define its own destructor, the compiler will define a composite destructor for it
- For some classes, composite destructors are used to prevent objects of that type from being destroyed
- Generally, the function body of the synthetic destructor is empty
- **It is important to recognize that the destructor itself does not directly destroy members. Members are destroyed in the implicit destructor stage after the destructor body** In the whole object destruction process, the destructor body is carried out as another part besides the member destruction step
13.10 what happens when a StrBlob object is destroyed? When a StrBlobPtr object is destroyed?
StrBlob: shared_ The reference count of PTR decreases
StrBlobPtr: does not affect reference count
13.11 add a destructor to the HasPtr class in the previous exercise.
#include <string> class HasPtr { public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){} HasPtr& operator=(const HasPtr& rhs){ auto newp = new std::string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } ~HasPtr(){delete ps;} private: std::string *ps; int i; };
13.12 how many destructor calls will occur in the following code snippet?
bool fcn(const Sales_data *trans, Sales_data accum) { Sales_data item1(*trans), item2(accum); return item1.isbn() != item2.isbn(); }
3 times
item1, item2 and accum leave the scope and are destroyed
trans is a pointer to an object, and the destructor will not execute
13.13 a good way to understand copy control members and constructors is to define a simple class, define these members for this class, and each member prints its own name:
struct X { X() {std::cout << "X()" << std::endl;} X(const X&) {std::cout << "X(const X&)" << std::endl;} };
Add copy assignment operators and destructors to x, and write a program to use the objects of X in different ways: pass them as non reference parameters; Dynamically allocate them; Store them in containers; and so on. Observe the output of the program until you are sure you understand when copy control members are used and why they are used. When you look at the program output, remember that the compiler can skip calls to the copy constructor.
#include <iostream> #include <string> #include <vector> struct X { X() {std::cout << "X()" << std::endl;} X(const X&) {std::cout << "X(const X&)" << std::endl;} X& operator=(const X&rhs){std::cout << "X& operator=(const X&rhs)" << std::endl;} ~X(){std::cout << "~X()" << std::endl;} }; void f(X a,const X& b){ std::vector<X>v; std::cout << "push_back a" << std::endl; v.push_back(a); std::cout << "push_back b" << std::endl; v.push_back(b); } int main(){ std::cout << "create pb" << std::endl; X a; std::cout << "create pb" << std::endl; X* pb = new X(a); std::cout << "f(a,*pb)" << std::endl; f(a,*pb); delete pb; return 0; }
13.1.4 three / five rule
13.14 suppose numbered is a class with a default constructor, which can generate a unique sequence number for each object and store it in the data member named mysn. Suppose numbered uses the combined copy control member and gives the following function:
void f (numbered s) { cout << s.mysn < endl; }
What is the output of the following code?
numbered a, b = a, c = b; f(a); f(b); f(c);
Output three identical numbers
13.15 suppose numbered defines a copy constructor, which can generate a new serial number. Will this change the output of the call in the last question? If it will change, why? What is the new output?
Output three different numbers, but it has nothing to do with a, B and C
13.16 what happens if the parameter in f is const numbered &? Does this change the output? If it will change, why? What is the new output?
Output three different numbers: A, B and C
13.17 write numbered and f described in the first three questions respectively to verify whether you have correctly predicted the output results.
3.14
#include <iostream> #include <string> #include <vector> class numbered{ public: friend void f(numbered s); numbered() {mysn = num++;} private: static int num; int mysn; }; int numbered::num = 0; void f (numbered s) { std::cout << s.mysn << std::endl; } int main(){ numbered a,b=a,c=b; f(a);f(b);f(c); return 0; }
Output three zeros
3.15
#include <iostream> #include <string> #include <vector> class numbered{ public: friend void f(numbered s); numbered() {mysn = num++;} numbered(const numbered &s){ mysn = num++; } private: static int num; int mysn; }; int numbered::num = 0; void f (numbered s) { std::cout << s.mysn << std::endl; } int main(){ numbered a,b=a,c=b; f(a);f(b);f(c); return 0; }
Output 3,4,5
3.16
#include <iostream> #include <string> #include <vector> class numbered{ public: friend void f(const numbered &s); numbered() {mysn = num++;} numbered(const numbered &s){ mysn = num++; } private: static int num; int mysn; }; int numbered::num = 0; void f (const numbered &s) { std::cout << s.mysn << std::endl; } int main(){ numbered a,b=a,c=b; f(a);f(b);f(c); return 0; }
Output 0,1,2
13.1.6 prevent copying
3.18 define an Employee class, which contains the Employee's name and unique Employee ID number. Define a default constructor for this class and a constructor that accepts a string representing the Employee's name. Each constructor should generate a unique license number by incrementing a static data member.
class Employee{ public: Employee(){id = id_num++;} Employee(const std::string &s):name(s),id(id_num){ id_num++; } private: std::string name; int id; static int id_num; }; static int id_num = 1000;
3.19 does your Employee class need to define its own copy control members? If so, why? If not, why? Implement the copy control members you think employees need.
There is no need to copy, and employees do not have to increment the number because of the copy. The copy construction and assignment construction operators are defined as delete to prevent copy and assignment
class Employee{ public: Employee(){id = id_num++;} Employee(const std::string &s):name(s),id(id_num){ id_num++; } Employee(const Employee &)=delete; Employee& operator=(const Employee&)=delete; private: std::string name; int id; static int id_num; }; static int id_num = 1000;
13.20 explain what happens when we copy, assign, or destroy TextQuery and QueryResult class objects?
Because smart pointers are used in these two classes, all members of the class will be copied when copying, and all members will be destroyed when destroying.
13.21 do you think TextQuery and QueryResult classes need to define copy control members of their own versions? If so, why? Implement the copy control operations you think these two classes need.
No, because of the smart pointer, the composite version can automatically control the memory release
When we decide whether a class needs to define its own version of copy control members, a basic principle is to determine whether the class needs a destructor, and the classes that need a destructor also need copy and assignment operations
13.2 copy control and resource management
13.22 suppose we want HasPtr to behave like a value. That is, for the string member pointed to by the object, each object has its own copy. We will introduce the definition of copy control members in the next section. However, you have learned all the knowledge needed to define these members. Before continuing to the next section, write the copy constructor and copy assignment operator for HasPtr.
Same as 13.8
13.2.1 class of behavior image value
13.23 compare the copy control member you wrote in the previous exercise with the code in this section. Make sure you understand the difference between your code and ours.
Because I read the whole chapter before writing the exercises, so it's basically the same.
The simple writing method is to judge whether it is self assignment with if. The advanced writing method is realized with swap, which will be shown later
13.24 what happens if the HasPtr version of this section does not define a destructor? What happens if the copy constructor is not defined?
According to the three / five rule, several operations controlled by copy members should be regarded as a whole. Generally, if you want to write them, you can write them all
Undefined destructors can cause memory leaks
Undefined copy constructor will only copy the value of pointer, and several pointers point to the same address
13.25 suppose you want to define the class value version of StrBlob and want to continue using shared_ptr, so that our StrBlobPtr class can still use the weak pointing to the vector_ PTR. Your modified class will need a copy constructor and a copy assignment operator, but no destructor. Explain what copy constructors and copy assignment operators must do. Explain why destructors are not required.
Copy constructor and copy assignment operator need to reallocate memory dynamically
Because the smart pointer used can automatically control the memory release, and the reference count is 0, the object is automatically destroyed, so there is no need for a destructor
13.26 write your own version of the strBlob class described in the previous question.
Add in StrBlob
//copy construction StrBlob(const StrBlob& sb):data(std::make_shared<std::vector<std::string>>(*sb.data)){} //copy assignment StrBlob& operator=(const StrBlob& sb){ data = std::make_shared<std::vector<std::string>>(*sb.data); return *this; }
13.2.2 define classes that behave like pointers
13.27 define your own version of HasPtr using reference counting.
class HasPtr { public: HasPtr(const std::string &s = std::string()) :ps(new std::string(s)),i(0),use(new std::size_t(1)){} HasPtr(const HasPtr &p) :ps(p.ps),i(p.i),use(p.use){++*use;} HasPtr& operator=(const HasPtr& rhs){ ++*rhs.use; if(--*use == 0){ delete ps; delete use; } ps = rhs.ps; i = rhs.i; use = rhs.use; return *this; } ~HasPtr(){ if(--*use == 0){ delete ps; delete use; } } private: std::string *ps; int i; std::size_t *use;//Reference count };
13.28 given the following class, implement a default constructor and necessary copy control members for it.
TreeNode has been modified. Count should be a reference count. It should be stored in memory instead of belonging to any object, so it has been changed to type int *
(a) class TreeNode { private: std::string value; int *count; TreeNode *left; TreeNode *right; }; (b) class BinStrTree{ private: TreeNode *root; };
#include <cstddef> #include <string> class TreeNode { public: TreeNode() :value(std::string()),count(new int(1)),left(nullptr),right(nullptr){} TreeNode(const TreeNode& t) :value(t.value),count(t.count),left(t.left),right(t.right){ ++*count; } TreeNode& operator=(const TreeNode& rhs){ ++*rhs.count; if(--*count == 0){ delete left; delete right; delete count; } value = rhs.value; left = rhs.left; right = rhs.right; return *this; } ~TreeNode(){ if(--*count == 0){ delete left; delete right; delete count; } } private: std::string value; int* count; TreeNode *left; TreeNode *right; }; class BinStrTree{ public: BinStrTree():root(nullptr){} BinStrTree(const BinStrTree &bst):root(new TreeNode(*bst.root)){} BinStrTree& operator=(const BinStrTree& rhs){ auto newroot = new TreeNode(*rhs.root); delete root; root = newroot; return *this; } ~BinStrTree(){delete root;} private: TreeNode *root; };
13.3 switching operation
13.29 explain that the call to swap in swap (hasptr &, hasptr &) will not cause a recursive loop.
Because no custom swap is written for built-in types, std::swap of the standard library is preferred
13.30 write the swap function for your class valued version of HasPtr and test it. Add a print statement to your swap function to indicate when the function will execute.
#include <string> #include <iostream> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){} HasPtr& operator=(const HasPtr& rhs){ auto newp = new std::string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } ~HasPtr() {delete ps;} private: std::string *ps; int i; }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); std::cout << "using HasPtr's swap" << std::endl; }
13.31 define a < operator for your HasPtr class and a HasPtr vector. Add some elements to the vector and sort it. Note when swap will be called.
#include <string> #include <iostream> #include <vector> #include <algorithm> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); friend bool operator<(const HasPtr& lhs,const HasPtr& rhs); public: HasPtr(const std::string& s = std::string()): ps(new std::string(s)), i(0) { } HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){} HasPtr& operator=(HasPtr rhs){//The assignment copy should be written in the form of swap, which will be used swap(*this,rhs); return *this; } ~HasPtr(){delete ps;} std::string show(){return *ps;} private: std::string *ps; int i; }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); std::cout << "using HasPtr's swap" << std::endl; } inline bool operator<(const HasPtr& lhs,const HasPtr& rhs){ return *lhs.ps < *rhs.ps; } int main(){ std::vector<HasPtr>vec; HasPtr a("fbc"),b("cde"),c("avpq"); vec.push_back(a); vec.push_back(b); vec.push_back(c); sort(vec.begin(),vec.end()); for(auto i:vec){ std::cout <<i.show() << std::endl; } }
Output results
using HasPtr's swap using HasPtr's swap using HasPtr's swap using HasPtr's swap using HasPtr's swap avpq cde fbc
Will the HasPtr version of 13.32 class pointers benefit from the swap function? If so, what are the benefits? If not, why?
No, the class value version uses its own written swap to avoid reallocation of memory by exchanging pointers. The class pointer version itself is a pointer, so there is no profit
13.4 copy control example
13.33 why are the parameters of save and remove members of Message a Folder &? Why can't we define the parameter as Folder or const Folder?
Because save and remove operate on a specific object, not a class
13.34 write the Message described in this section.
13.36 design and implement the corresponding Folder class. This class should save a set pointing to the Message contained in the Folder.
13.37 add members to the Message class to add and delete a given Folder * to folders. These two members are similar to the addMsg and remMsg operations of the Folder class. (here, the referenced parameters are used instead)
#include <string> #include <iostream> #include <set> #include <vector> class Folder; class Message{ public: friend void swap(Message &lhs,Message &rhs); friend class Folder; public: explicit Message(const std::string &str = ""):contents(str){} Message(const Message&); Message& operator=(const Message&); ~Message(); void save(Folder &);//Add this message to the specified folder void remove(Folder &);//Delete this message from the specified folder private: std::string contents;//Save content std::set<Folder*>folders;//In which folder s do you want to save this message void add_to_Folders(const Message&); void remove_from_Folders(); }; class Folder{ public: friend void swap(Message &lhs,Message &rhs); friend class Message; public: Folder()= default; Folder(const Folder &f); Folder& operator=(const Folder &f); ~Folder(); private: std::set<Message*>messages; void add_to_Message(const Folder&); void remove_from_Message(); void addMsg(const Message *); void remMsg(const Message *); }; void Message::save(Folder &f){ folders.insert(&f); f.addMsg(this); } void Message::remove(Folder &f){ folders.erase(&f); f.remMsg(this); } void Message::add_to_Folders(const Message &m){ for(auto f:m.folders) f->addMsg(this); } Message::Message(const Message &m):contents(m.contents),folders(m.folders){ add_to_Folders(m); } Message::~Message(){ remove_from_Folders(); } Message& Message::operator=(const Message &rhs){ remove_from_Folders(); contents = rhs.contents; folders = rhs.folders; add_to_Folders(rhs); return *this; } void swap(Message &lhs,Message &rhs){ using std::swap; for(auto f:lhs.folders) f->remMsg(&lhs); for(auto f:rhs.folders) f->remMsg(&rhs); swap(lhs.folders,rhs.folders); swap(lhs.contents,rhs.contents); for(auto f:lhs.folders) f->addMsg(&lhs); for(auto f:rhs.folders) f->addMsg(&rhs); } void Folder::add_to_Message(const Folder &f) { for (auto m : f.messages) m->save(*this); } Folder::Folder(const Folder &f) : messages(f.messages) { add_to_Message(f); } void Folder::remove_from_Message() { for (auto m : messages) m->remove(*this); } Folder::~Folder() { remove_from_Message(); } Folder &Folder::operator=(const Folder &rhs) { remove_from_Message(); messages = rhs.messages; add_to_Message(rhs); return *this; } int main(){ return 0; }
13.35 what happens if a Message uses a synthetic copy control member?
Synthetic copy control members may cause existing Folders and messages to be out of sync after assignment
13.38 we do not use copy exchange to design the assignment operator of Message. What do you think is the reason?
For the example of dynamically allocating memory, copy exchange is a simple design. The Message class here does not need to dynamically allocate memory, and using copy exchange will only increase the complexity of implementation.
13.5 dynamic memory management class
13.39 write your own version of StrVec, including your own version of reserve, capacity and resize.
Referring to the answer of applenob, I feel that the refactoring and simplification of several codes are well written (such as refining copy_n_move)
Header file
#ifndef StrVec_H #define StrVec_H #include <cstdlib> #include <string> #include <iostream> #include <vector> #include <memory> #include <utility> #include <initializer_list> class StrVec{ public: StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){} StrVec(const StrVec&); StrVec& operator=(const StrVec&); ~StrVec(); public: void push_back(const std::string&); 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;} void reserve(size_t new_cap); void resize(size_t count); void resize(size_t count,const std::string& s); private: static std::allocator<std::string>alloc; void chk_n_alloc(){ if (size() == capacity())reallocate(); } std::pair<std::string*,std::string*>alloc_n_copy(const std::string*,const std::string*); void free(); void reallocate(); void alloc_n_move(size_t new_cap); private: std::string *elements; std::string *first_free; std::string *cap; }; #endif
Implementation file
#include "StrVec.hpp" #include <memory> #include <string> void StrVec::push_back(const std::string& s){ chk_n_alloc(); alloc.construct(first_free++,s); } std::pair<std::string*,std::string*> StrVec::alloc_n_copy (const std::string*b,const std::string*e){ auto data = alloc.allocate(e-b); return {data,std::uninitialized_copy(b, e,data)}; } void StrVec::free(){ if(elements){ for(auto p = first_free; p != elements;) alloc.destroy(--p); alloc.deallocate(elements,cap-elements); } } StrVec::StrVec(const StrVec &s){ auto newdata = alloc_n_copy(s.begin(), s.end()); elements = newdata.first; first_free = cap = newdata.second; } StrVec::~StrVec(){free();} StrVec& StrVec::operator=(const StrVec &rhs){ auto data = alloc_n_copy(rhs.begin(),rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } void StrVec::alloc_n_move (size_t new_cap){ auto newdata = alloc.allocate(new_cap); auto dest = newdata; auto elem = elements; for(size_t i = 0 ;i != size();++i) alloc.construct(dest++,std::move(*elem++)); free(); elements = newdata; first_free = dest; cap = elements + new_cap; } void StrVec::reallocate (){ auto newcapacity = size()?2*size():1; alloc_n_move(newcapacity); } void StrVec::reserve(size_t new_cap){ if(new_cap <= capacity())return; alloc_n_move(new_cap); } void StrVec::resize(size_t count){ resize(count,std::string()); } void StrVec::resize(size_t count,const std::string& s){ if(count > size()){ if(count > capacity())reserve(count*2); for(size_t i = size(); i != count;++i){ alloc.construct(first_free++,s); } }else if(count <size()){ while(first_free != elements + count) alloc.destroy(--first_free); } }
13.40 add a constructor for your StrVec class, which accepts an initializer_ List < string > parameter.
#ifndef StrVec_H #define StrVec_H #include <cstdlib> #include <string> #include <iostream> #include <vector> #include <memory> #include <utility> #include <initializer_list> class StrVec{ public: StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){} StrVec(const StrVec&); StrVec& operator=(const StrVec&); ~StrVec(); StrVec(std::initializer_list<std::string>); public: void push_back(const std::string&); 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;} void reserve(size_t new_cap); void resize(size_t count); void resize(size_t count,const std::string& s); private: static std::allocator<std::string>alloc; void chk_n_alloc(){ if (size() == capacity())reallocate(); } std::pair<std::string*,std::string*>alloc_n_copy(const std::string*,const std::string*); void free(); void reallocate(); void alloc_n_move(size_t new_cap); void range_initialize(const std::string *first, const std::string *last); private: std::string *elements; std::string *first_free; std::string *cap; }; #endif
#include "StrVec.hpp" #include <memory> #include <string> void StrVec::push_back(const std::string& s){ chk_n_alloc(); alloc.construct(first_free++,s); } std::pair<std::string*,std::string*> StrVec::alloc_n_copy (const std::string*b,const std::string*e){ auto data = alloc.allocate(e-b); return {data,std::uninitialized_copy(b, e,data)}; } void StrVec::free(){ if(elements){ for(auto p = first_free; p != elements;) alloc.destroy(--p); alloc.deallocate(elements,cap-elements); } } StrVec::StrVec(const StrVec &s){ auto newdata = alloc_n_copy(s.begin(), s.end()); elements = newdata.first; first_free = cap = newdata.second; } StrVec::~StrVec(){free();} StrVec& StrVec::operator=(const StrVec &rhs){ auto data = alloc_n_copy(rhs.begin(),rhs.end()); free(); elements = data.first; first_free = cap = data.second; return *this; } void StrVec::range_initialize(const std::string *first, const std::string *last){ auto newdata = alloc_n_copy(first, last); elements = newdata.first; first_free = cap = newdata.second; } StrVec::StrVec(std::initializer_list<std::string> il) { range_initialize(il.begin(), il.end()); } void StrVec::alloc_n_move (size_t new_cap){ auto newdata = alloc.allocate(new_cap); auto dest = newdata; auto elem = elements; for(size_t i = 0 ;i != size();++i) alloc.construct(dest++,std::move(*elem++)); free(); elements = newdata; first_free = dest; cap = elements + new_cap; } void StrVec::reallocate (){ auto newcapacity = size()?2*size():1; alloc_n_move(newcapacity); } void StrVec::reserve(size_t new_cap){ if(new_cap <= capacity())return; alloc_n_move(new_cap); } void StrVec::resize(size_t count){ resize(count,std::string()); } void StrVec::resize(size_t count,const std::string& s){ if(count > size()){ if(count > capacity())reserve(count*2); for(size_t i = size(); i != count;++i){ alloc.construct(first_free++,s); } }else if(count <size()){ while(first_free != elements + count) alloc.destroy(--first_free); } }
13.41 push_ In back, why do we use post increment operation in construct call? What happens if you use a pre increment operation?
An empty location will be skipped
13.42 test your StrVec class by replacing vector < string > with your StrVec class in your TextQuery and QueryResult classes.
13.43 rewrite the free member with for_ Replace the for loop destroy element with each and lambda. Which implementation do you prefer and why?
void StrVec::free(){ if(elements){ for_each(elements,first_free,[this](std::string& s){alloc.destroy(&s);}) alloc.deallocate(elements,cap-elements); } }
for_each+lambda is more concise and can avoid crossing boundaries
13.44 write a simplified version of the standard library String class, named String. Your class should have at least one default constructor and one constructor that accepts C-style String pointer parameters. Use allocator to allocate the required memory for your String class.
StrVec and applenon in reference books
MyString.hpp
#ifndef MyString_H #define MyString_H #include <memory> #include <algorithm> #include <iterator> class MyString{ public: MyString():MyString(""){} MyString(const char*); MyString(const MyString&); MyString& operator=(const MyString&); ~MyString(); public: size_t size()const{return ends-elements;} const char* begin()const{return elements;} const char* end()const{return ends;} private: std::allocator<char> alloc; std::pair<char*,char*>alloc_n_copy(const char*,const char*); void free(); void range_initializer(const char *first, const char *last); private: char* elements; char* ends; }; #endif
MyString.cpp
#include "MyString.hpp" std::pair<char*,char*> MyString::alloc_n_copy(const char*b,const char*e){ auto data = alloc.allocate(e-b); return {data,std::uninitialized_copy(b, e,data)}; } void MyString::free(){ if(elements){ std::for_each(elements,ends,[this](char &c){alloc.destroy(&c);}); alloc.deallocate(elements,ends-elements); } } void MyString::range_initializer(const char *first, const char *last){ auto newstr = alloc_n_copy(first, last); elements = newstr.first; ends = newstr.second; } MyString::MyString(const char* s){ char * sl = const_cast<char*>(s); while(*sl) ++sl; range_initializer(s,++sl); } MyString::~MyString(){free();} MyString& MyString::operator=(const MyString &rhs){ auto data = alloc_n_copy(rhs.begin(),rhs.end()); free(); elements = data.first; ends = data.second; std::cout << "call MyString& MyString::operator=(const MyString &rhs)" << std::endl; return *this; } MyString::MyString(const MyString& ms){ range_initializer(ms.begin(),ms.end()); std::cout << "MyString::MyString(const MyString& ms)" << std::endl; }
Test.cpp
#include <memory> #include <vector> #include <iostream> #include "MyString.hpp" void foo(MyString x) { std::cout << x.begin() << std::endl; } void bar(const MyString& x) { std::cout << x.begin() << std::endl; } MyString baz() { MyString ret("world"); return ret; } int main() { char text[] = "world"; MyString s0; MyString s1("hello"); MyString s2(s0); MyString s3 = s1; MyString s4(text); s2 = s1; foo(s1); bar(s1); foo("temporary"); bar("temporary"); MyString s5 = baz(); std::vector<MyString> svec; svec.reserve(8); svec.push_back(s0); svec.push_back(s1); svec.push_back(s2); svec.push_back(s3); svec.push_back(s4); svec.push_back(s5); svec.push_back(baz()); svec.push_back("good job"); for (const auto &s : svec) { std::cout << s.begin() << std::endl; } }
makefile
main:Test.o MyString.o clang++ Test.o MyString.o -o main -g Test.o:Test.cpp clang++ -c Test.cpp -o Test.o MyString.o:MyString.cpp MyString.hpp clang++ -c MyString.cpp -o MyString.o clean: rm *.o main
13.6 object movement
13.6.1 right value reference
13.45 explain the difference between lvalue reference and lvalue reference?
Lvalue reference: general reference
R-value reference: a reference bound to an R-value
- The right value reference must be bound to the right value (the expression requiring conversion, literal constant, and the expression returning the right value). It cannot be directly bound to an left value. It can be obtained by & & instead of &
- Functions that return lvalue references, such as connected assignment, subscript, dereference and pre increment / decrement operators, are all examples of lvalue expressions
- Functions that return non reference types, connected arithmetic, relation, bit and post value increment / decrement operators, all generate right values. You cannot bind an lvalue reference to such an expression, but you can bind a cosnt lvalue reference or an rvalue reference to such an expression
- The right value reference can only be bound to an object to be destroyed (just like playing a game, there are weapons on the opposite side, and you can pick up weapons only when the opposite side is dead)
- Lvalue persistence: the identity of an object with a persistent state
- R-value is short: the value of the object is either a literal constant or a temporary object created during the evaluation of the expression
13.46 what types of references can be bound to the following initializers?
int f(); vector<int> vi(100); int? r1 = f();//R-value reference, return function of non reference type int? r2 = vi[0];//Lvalue reference, subscript int? r3 = r1;//Lvalue reference, variable is lvalue, even if the variable is lvalue reference (lvalue reference is not equal to lvalue). int? r4 = vi[0] * f();//R-value reference, which returns the expression of R-value (R-value is temporary)
13.47 for the String class you defined in exercise 13.44, add a statement for its copy constructor and copy assignment operator, and print a message each time the function executes.
13.48 define a vector < String > and call push multiple times on it_ back. Run your program and observe how many times the String has been copied.
See exercise 13.44
13.6.2 move constructor and move assignment operator
13.49 add a move constructor and a move assignment operator to your StrVec, String and Message classes.
Take String as an example
#ifndef MyString_H #define MyString_H #include <memory> #include <algorithm> #include <iterator> #include <iostream> class MyString{ public: MyString():MyString(""){} MyString(const char*); MyString(const MyString&); MyString& operator=(const MyString&); MyString(MyString &&) noexcept;//move constructor MyString& operator=(MyString &&)noexcept;//Move assignment function ~MyString(); public: size_t size()const{return ends-elements;} const char* begin()const{return elements;} const char* end()const{return ends;} private: std::allocator<char> alloc; std::pair<char*,char*>alloc_n_copy(const char*,const char*); void free(); void range_initializer(const char *first, const char *last); private: char* elements; char* ends; }; #endif
MyString.cpp
#include "MyString.hpp" std::pair<char*,char*> MyString::alloc_n_copy(const char*b,const char*e){ auto data = alloc.allocate(e-b); return {data,std::uninitialized_copy(b, e,data)}; } void MyString::free(){ if(elements){ std::for_each(elements,ends,[this](char &c){alloc.destroy(&c);}); alloc.deallocate(elements,ends-elements); } } void MyString::range_initializer(const char *first, const char *last){ auto newstr = alloc_n_copy(first, last); elements = newstr.first; ends = newstr.second; std::cout << "void MyString::range_initializer(const char *first, const char *last)" << std::endl; } MyString::MyString(const char* s){ char * sl = const_cast<char*>(s); while(*sl) ++sl; range_initializer(s,++sl); } MyString::~MyString(){free();} MyString& MyString::operator=(const MyString &rhs){ auto data = alloc_n_copy(rhs.begin(),rhs.end()); free(); elements = data.first; ends = data.second; std::cout << "call MyString& MyString::operator=(const MyString &rhs)" << std::endl; return *this; } MyString::MyString(const MyString& ms){ range_initializer(ms.begin(),ms.end()); std::cout << "MyString::MyString(const MyString& ms)" << std::endl; } MyString::MyString(MyString && ms) noexcept:elements(ms.elements),ends(ms.ends){ std::cout << "MyString::MyString(MyString && ms)" << std::endl; ms.elements = ms.ends = nullptr; } MyString& MyString::operator=(MyString && ms)noexcept{ if(this != &ms){ free(); elements = ms.elements; ends = ms.ends; ms.elements = ms.ends = nullptr; } std::cout << "MyString& MyString::operator=(MyString && ms)" << std::endl; return *this; }
Test.cpp
#include <memory> #include <vector> #include <iostream> #include "MyString.hpp" void foo(MyString x) { std::cout << x.begin() << std::endl; } void bar(const MyString& x) { std::cout << x.begin() << std::endl; } MyString baz() { MyString ret("world"); return ret; } int main(){ //Reference https://blog.csdn.net/qq_24739717/article/details/104473034#t48 char text[] = "world"; std::cout << "---------------definition-----------------" << std::endl; MyString s0; MyString s1("hello"); MyString s2(s0); MyString s3 = s1; MyString s4(text); s2 = s1; std::cout << "---------------call-----------------" << std::endl; foo(s1); bar(s1); foo("temporary"); bar("temporary"); MyString s5 = baz(); std::cout << "---------------add to-----------------" << std::endl; std::vector<MyString> svec; svec.reserve(8); svec.push_back(s0); svec.push_back(s1); svec.push_back(s2); svec.push_back(s3); svec.push_back(s4); svec.push_back(s5); svec.push_back(baz());//Avoid copying svec.push_back("good job");//Avoid copying std::cout << "---------------output-----------------" << std::endl; for (const auto &s : svec) { std::cout << s.begin() << std::endl; } }
13.50 add a print statement to the move operation of your String class, and rerun the program in exercise 13.48 in section 13.6.1. It uses a vector < String > to observe when copying will be avoided.
---------------definition----------------- void MyString::range_initializer(const char *first, const char *last) void MyString::range_initializer(const char *first, const char *last) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) call MyString& MyString::operator=(const MyString &rhs) ---------------call----------------- void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) hello hello void MyString::range_initializer(const char *first, const char *last) temporary void MyString::range_initializer(const char *first, const char *last) temporary void MyString::range_initializer(const char *first, const char *last) ---------------add to----------------- void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(const MyString& ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(MyString && ms) void MyString::range_initializer(const char *first, const char *last) MyString::MyString(MyString && ms) ---------------output----------------- hello hello hello world world world good job
13.51 although unique_ptr cannot be copied, but we wrote a clone function in section 12.1.5, which returns a unique value_ ptr. Explain why the function is legal and why it works correctly.
We can copy or assign a unique to be destroyed_ The most common example of PTR is to return a unique from a function_ ptr
In fact, the move constructor or move assignment operator is called to take over the unique function in this function_ Ownership of PTR
13.52 explain in detail what happened to the assignment of HasPtr object on page 478? In particular, step by step, describe what changes have taken place in the value of the parameter rhs in the assignment operators of hp, hp2 and HasPtr.
Left value copy, right value move
hp = hp2;
- hp2 is an lvalue, so the parameter is copied through the copy constructor
- rhs is a copy of hp2. They are independent and have the same string content. After the assignment, the rhs is destroyed
hp = std::move(hp2);
- std::move binds an R-value reference to hp2 and uses the move constructor
- rhs takes over ownership of hp2
Regardless of whether a copy construct or a move construct is used, the function body of the assignment operator will swap the states of the two operands. After exchanging the two object pointers, the pointer in rhs points to the string used by the original left operands. When rhs leaves the scope, this string will be destroyed
13.53 from the perspective of bottom efficiency, the assignment operator of HasPtr is not ideal. Why? Implement a copy assignment operator and a move assignment operator for HasPtr, and compare the operations performed in your new move assignment operator with those performed in the copy and exchange version.
Compile related parameters
clang++ -Wall -std=c++14 -O2 -fsanitize=undefined,address
#include <string> #include <iostream> #include <ctime> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); public: HasPtr(const std::string& s = std::string()):ps(new std::string(s)), i(0) { }//Default construction HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){}//copy construction HasPtr& operator=(HasPtr rhs){//Copy assignment swap version swap(*this,rhs); return *this; } HasPtr(HasPtr && hp)noexcept:ps(hp.ps),i(hp.i){//Mobile structure hp.ps = nullptr; } ~HasPtr() {delete ps;} private: std::string *ps; int i; }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); } int main(){ HasPtr hp1("test1"),hp2("test2"); clock_t start = clock(); hp1 = hp2; clock_t end = clock(); std::cout << end - start << std::endl; return 0; }
#include <string> #include <iostream> #include <ctime> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); public: HasPtr(const std::string& s = std::string()):ps(new std::string(s)), i(0) { }//Default construction HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){}//copy construction HasPtr(HasPtr && hp)noexcept:ps(hp.ps),i(hp.i){//Mobile structure hp.ps = nullptr; } HasPtr& operator=(const HasPtr& rhs){//copy assignment auto newp = new std::string(*rhs.ps); delete ps; ps = newp; i = rhs.i; return *this; } HasPtr& operator=(HasPtr&& rhs)noexcept{ if(this != &rhs){ delete ps; ps = rhs.ps; rhs.ps = nullptr; } return *this; } ~HasPtr() {delete ps;} private: std::string *ps; int i; }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); } int main(){ HasPtr hp1("test1"),hp2("test2"); clock_t start = clock(); hp1 = hp2; clock_t end = clock(); std::cout << end - start << std::endl; return 0; }
It seems that the gap is not big, and even the swap is faster? There may be a problem with the measurement / writing
Referring to the explanation of other predecessors, the swap operation will create a temporary space to store the value of the temporary variable, so the assignment operation efficiency is low
13.54 what happens if we define the move assignment operator for HasPtr, but do not change the copy and exchange operator? Write code to verify your answer.
#include <string> #include <iostream> #include <ctime> class HasPtr { public: friend void swap(HasPtr&, HasPtr&); public: HasPtr(const std::string& s = std::string()):ps(new std::string(s)), i(0) { }//Default construction HasPtr(const HasPtr& hp):ps(new std::string(*hp.ps)),i(hp.i){}//copy construction HasPtr(HasPtr && hp)noexcept:ps(hp.ps),i(hp.i){//Mobile structure hp.ps = nullptr; } HasPtr& operator=(HasPtr rhs){//Copy assignment swap version swap(*this,rhs); return *this; } HasPtr& operator=(HasPtr&& rhs)noexcept{ if(this != &rhs){ delete ps; ps = rhs.ps; rhs.ps = nullptr; } return *this; } ~HasPtr() {delete ps;} private: std::string *ps; int i; }; inline void swap(HasPtr& lhs, HasPtr& rhs){ using std::swap; swap(lhs.ps, rhs.ps); swap(lhs.i,rhs.i); } int main(){ HasPtr hp1("test"),*hp2 = new HasPtr("train"); hp1 = std::move(*hp2); return 0; }
report errors
error: use of overloaded operator '=' is ambiguous (with operand types 'HasPtr' and 'typename std::remove_reference<HasPtr &>::type' (aka 'HasPtr'))
13.6.3 right value reference and member function
13.55 add an R-value reference version of push for your StrBlob_ back.
void push_back(string &&t){data->push_back(std::move(t));}
13.56 what happens if sorted is defined as follows?
Foo Foo::sorted() const & { Foo ret(*this); return ret.sorted(); }
ret is generated by the copy constructor. It is an lvalue and called circularly
13.57 what happens if sorted is defined as follows:
Foo Foo::sorted() const & { return Foo(*this).sorted(); }
Foo(*this) is a temporary quantity, which is an R-value. It will call sorted() of the R-value version
13.58 write a new version of Foo class. There are printed statements in its sorted function. Test this class to verify whether your answers to the first two questions are correct.
#include <algorithm> #include <string> #include <vector> #include <iostream> class Foo{ public: Foo sorted() &&; Foo sorted() const &; private: std::vector<int> data; }; Foo Foo::sorted()&&{ sort(data.begin(),data.end()); std::cout << "Foo Foo::sorted()&&" << std::endl; return *this; } Foo Foo::sorted() const &{ Foo ret(*this); sort(ret.data.begin(),ret.data.end()); std::cout << "Foo Foo:sorted() const &" << std::endl; return ret; } int main(){ Foo().sorted();//Call the right value version. If there is no written right value version, the left value version will be called Foo f; f.sorted();//Call lvalue version return 0; }
output
Foo Foo::sorted()&& Foo Foo:sorted() const &