C++ Primer learning notes - object move

Keywords: C++

background

The new feature of C++ 11 is object movement. Objects can be moved instead of copied. In some cases, objects are destroyed immediately after being copied, such as value passing parameters, objects are returned by value passing, and temporary objects construct another object. In these cases, using moving objects instead of copying objects can significantly improve performance.

string s1(string("hello")); // The nameless object string("hello") is a temporary object that will be destroyed immediately after the construction s1 is copied

[======]

rvalue reference

When it comes to object movement, you have to mention two elements: the right value reference and the std::move library function.

An rvalue reference is a reference that must be bound to an rvalue. It mainly includes nameless objects, expressions, and literals. Use & & to get a reference to the right value.
In short, an R-value reference is a reference to a temporary object, but the temporary object is not necessarily an R-value, but the temporary object to be destroyed immediately is an R-value object. For example, a named object defined in a function is a temporary object and is not destroyed immediately.

Right value reference property

1) An R-value reference can only be bound to one object to be destroyed. You can freely "move" a resource referenced by an R-value to another object;
2) Similar to an lvalue reference, an lvalue reference is also an alias of an object;

The difference between right value reference and left value reference

Lvalue reference is a familiar general reference. It is proposed to distinguish right value references. The feature is that the lvalue reference cannot be bound to 1) the expression requiring conversion; 2) Literal constant; 3) An expression that returns an R-value;

For example,

int a = 2;
int &i = a * 2; // Error: the temporary calculation result a * 2 is an R-value and cannot be bound to an l-value reference
const int& ii = a * 2; // Correct: you can bind a const reference to an rvalue
int &&r = a * 2; // Correct: std::move converts the left value a to the right value, which can be bound to the right value reference

int &i1 = 42; // Error: 42 is a literal constant and cannot be bound to an lvalue reference
int &&r1 = 42; // Correct: 42 is a literal constant that can be bound to an R-value reference

int &i2 = std::move(a); // Error: std::move converted lvalue a to lvalue and cannot be bound to lvalue reference
int &&r2 = std::move(a); // Correct: std::move converts the left value a to the right value, which can be bound to the right value reference

Note: you can bind a const reference (either const &, or const &) to an R-value

The left value is persistent and the right value is short

The most obvious difference between lvalues and lvalues is that lvalues have a persistent state and will not be destroyed immediately; The right value is either a literal constant or a temporary object created during the evaluation of the expression.

Therefore, you can know the right value reference:
1) The referenced object will be destroyed;
2) The object has no other users;

See the previous article for details C + + > the difference between right value reference and left value reference

The variable is an lvalue

A variable is an lvalue. You cannot bind an R-value reference directly to a variable, even if the variable is an R-value reference type.

int a = 42;
int &&rr1 = 42; // Correct: literal constants are right-hand values
int &&rr2 = a;  // Error: variable a is an lvalue
int &&rr3 = rr1; // Error: the R-value reference rr1 is an l-value

std::move function

Header file
You cannot bind an R-value reference to an l-value, but you can convert the l-value to the corresponding R-value reference type by calling the std::move function.

int &&rr1 = 42;
int &&rr4 = rr1; // Error: cannot bind an R-value reference to another R-value reference
int &&rr5 = std::move(rr1); // OK

The move function tells the compiler that we have an lvalue, but we want to treat it like an lvalue. The call to move means a promise that it can no longer be used except to assign to rr1 or destroy it. After you call move, you cannot make any assumptions about the value of the source object after the move.

int *p = new int(42);
int &&r = std::move(*p);
cout << r << endl;
r = 1;
*p = 3; // The compiler does not report an error or prevent the modification of the source object value, but it is not recommended
cout << r << endl;
cout << *p << endl;

Note: unlike most standard library names, using is not provided for move. It is recommended to call std::move directly instead of move. Because the move name is common, applications often define this function. To avoid conflicts with the move function defined by the application, please use std::move.
[======]

Move constructor and move assignment operator

The move constructor (also known as move constructor) and the move assignment operator (also known as move assignment operator) are similar to the copy function (copy constructor, copy assignment operator), but the first two functions "steal" resources from a given object rather than copy resources.

In addition to completing the resource movement, the move constructor must also ensure that the source object is in such a state after the movement: it is harmless to destroy the source object.
Once the resource is moved, the resource no longer belongs to the source object, but to the newly created object. The source object must no longer point to the moved resource.

For example, define a move constructor for the StrVec class to realize the element move from one StrVec to another instead of copy:

class StrVec
{
public:
	StrVec(const StrVec &s); // copy constructor
	StrVec(StrVec &&s) noexcept; // move constructor
	...
private:
	string *elements;
	string *first_free;
	string *cap;
};

StrVec::StrVec(StrVec &&s) noexcept // The move operation should not throw any exceptions
// The member initializer takes over the resources in s
	: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
	// Put s into this state -- it's safe to run destructors aligned
	s.elements = s.first_free = s.cap = nullptr;
}

In the move constructor, the newly created object member initializer takes over the resources in the source object and sets the pointers of the source object to the resources to null to complete the resource movement operation. When the source object is destructed, the resource will not be released, so it is safe for new objects to use the resource.
noexcept indicates that the function does not throw any exceptions.

Move operations, standard library containers, and exceptions

Because the move operation "steals" resources, no resources are usually allocated. Therefore, the move operation usually does not throw an exception. In that case, why do you need to specify noexcept?
This is because unless the compiler knows that our move constructor will not throw an exception, it will think that moving our class object may throw an exception, and do some extra work to deal with this possibility. Therefore, if the confirmation does not throw an exception, it is explicitly indicated with noexcept.

TIPS:
Move constructors and move assignment operators that do not throw exceptions must be marked noexcept.

The move operation usually does not throw exceptions, but it does not mean that exceptions cannot be thrown, and the standard library container can guarantee its own behavior when exceptions occur. For example, vector guarantees that you can call in push_ If an exception occurs in the back (such as insufficient memory), the vector itself will not change.

To avoid this potential problem, unless the vector knows that the move constructor of the element type will not throw an exception, the copy constructor must be used instead of the move constructor during memory reallocation.
If you want to move rather than copy our custom type objects in the case of vector reallocation of memory, you must explicitly tell the standard library that our move constructor can be used safely.

In short: if the move constructor throws an exception, it uses the copy constructor to construct the object. If the move constructor does not throw an exception, it is explicitly declared with noexcept.

move assignment

Move assignment performs the same work as the destructor and move constructor. If our move assignment operator does not throw any exceptions, it should be marked noexcept.
Three steps to define move assignment:

  1. Release the existing resources of the current object;
  2. Take over the resources of the source object;
  3. Set the source object to destructable state;
class StrVec
{
public:
	...
	StrVec& operator=(StrVec &&rhs) noexcept; // move assignment
	...
private:
	string *elements;
	string *first_free;
	string *cap;
};

StrVec& StrVec::operator=(StrVec &&rhs) noexcept
{
	if (this != &rhs) {// Avoid self moving, because the result returned by move may be the object itself
		// Releasing the existing element of this object is equivalent to calling this - > ~ strvec
		free(); 
		// Take over resources from rhs
		elements = rhs.elements;
		first_free = rhs.first_free;
		cap = rhs.cap;

		// Placing rhs in a destructable state
		elements = first_free = cap = nullptr;
	}
	return *this;
}

After moving, the source object must be destructable

Moving resources from one object to another does not destroy the source object, but sometimes the source object will be destroyed after the move operation is completed. Therefore, when writing a move operation, you must ensure that the source object enters a destructable state after the move. Otherwise, destructing the source object may cause resource release, or modify the resource state, resulting in exceptions in the takeover object.

After moving resources, the program should not rely on the data in the source object. Although it is possible to access the data in the source object, the result is uncertain.

TIPS: after moving, the source object must remain valid and destructable, but the user cannot make any assumptions about it. When can I destruct? Depending on the user, it can usually be destructed immediately.

Composite move operation

If we do not declare our copy function, the compiler will synthesize the default version for us when necessary. Similarly, the compiler will synthesize the move function (move constructor and move assignment operator, i.e. move operation) for us.
The copy operation can be defined in three cases:
1) Bit wise copy member;
2) Assign values to objects;
3) Deleted functions;

When does the compiler not synthesize move operations?
Unlike the copy function, the compiler does not synthesize the move operation for some classes. In particular, if a class defines three member functions:
1) copy constructor;
2) copy assignment operator;
3) Destructor;

When does the compiler synthesize the move operation?
Only when a class does not define any copy control member of its own version (i.e. copy function), and each non static data member of the class can be moved (usually an object of built-in type and supporting move operation), the compiler will synthesize move operation for it.

What happens when a class has no move operation?
When a class has no move operation, the normal function matches, and the class will use the copy operation instead.

Example of composite move operation:

struct X
{
	int i; // Built in type
	string s; // string defines its own move operation
};
struct hasX
{
	X mem; // X has a composite move operation
};

int main()
{
	X x, x2 = std::move(x); // Use the composite move constructor
	hasX hx, hx2 = std::move(hx); // Use the composite move constructor
	return 0;
}

Thinking: when does the compiler use the composite copy operation and the composite move operation when we do not define either the copy function or the move function?
My understanding: look at function matching. If the argument used for construction or assignment is an lvalue, use the synthetic copy operation; If it is an R-value, use the composite move operation.

Define default, delete move operations
Unlike the copy operation, the move operation is not implicitly defined as delete. On the contrary, if we explicitly require to compile the composite move operation with = default, but the compiler cannot move all members, the compiler will define the move operation as delete.

So, when does the compiler define the move operation as delete?
The principles are:

  • Different from the copy constructor, the move constructor is defined as delete on the condition that there is a data member, there is a copy constructor, there is no move constructor, or there is no copy constructor, but the move constructor cannot be synthesized. The situation of move assignment is similar.

  • If the move operation of a data member is defined as delete or inaccessible, the move operation of a class is defined as delete.

  • Similar to the copy constructor, if the destructor of the class is defined as delete or inaccessible, the move constructor of the class is defined as delete.

  • Similar to the copy assignment operator, if a class data member is const or referenced, the move assignment of the class is defined as deleted.

For example, suppose Y is a class with a copy constructor defined but no move constructor defined:

class Y {
public:
	Y() = default;
	Y(const Y&) { cout << "Y copy constructor invoked" << endl; }
	Y& operator=(const Y&) { cout << "Y copy assignment invoked" << endl; return *this; }
};

// Since the data member Y does not have a move function (move constructor and move assignment operator), the compiler will not synthesize the move function for hasY. Instead, it will synthesize the copy function
struct hasY {
	hasY() = default;
	hasY(hasY &&) = default; // The compiler does not synthesize the move constructor
	hasY& operator=(hasY&&) = default; // The compiler does not synthesize the move assignment operator

	Y mem; // There will be a delete move constructor and a move assignment operator
};

int main()
{
	hasY hy, hy2 = std::move(hy); // In fact, the move constructor of hasY is not called, but the copy constructor is called
	hasY h3, h4;
	h4 = std::move(h3); // The copy assignment operator is actually called
	return 0;
}

Operation results:
You can see that even if the move function is indicated as default, the move function is not actually called, but the copy function is called.

Y copy constructor invoked
Y copy assignment invoked

There is also interaction between the move operation and the synthesized copy control members: if a class defines the move function, the copy function corresponding to the class synthesis will be defined as delete.

Move the right value and copy the left value

------------Restore content start------------

background

The new feature of C++ 11 is object movement. Objects can be moved instead of copied. In some cases, objects are destroyed immediately after being copied, such as value passing parameters, objects are returned by value passing, and temporary objects construct another object. In these cases, using moving objects instead of copying objects can significantly improve performance.

string s1(string("hello")); // The nameless object string("hello") is a temporary object that will be destroyed immediately after the construction s1 is copied

[======]

rvalue reference

When it comes to object movement, you have to mention two elements: the right value reference and the std::move library function.

An rvalue reference is a reference that must be bound to an rvalue. It mainly includes nameless objects, expressions, and literals. Use & & to get a reference to the right value.
In short, an R-value reference is a reference to a temporary object, but the temporary object is not necessarily an R-value, but the temporary object to be destroyed immediately is an R-value object. For example, a named object defined in a function is a temporary object and is not destroyed immediately.

Right value reference property

1) An R-value reference can only be bound to one object to be destroyed. You can freely "move" a resource referenced by an R-value to another object;
2) Similar to an lvalue reference, an lvalue reference is also an alias of an object;

The difference between right value reference and left value reference

Lvalue reference is a familiar general reference. It is proposed to distinguish right value references. The feature is that the lvalue reference cannot be bound to 1) the expression requiring conversion; 2) Literal constant; 3) An expression that returns an R-value;

For example,

int a = 2;
int &i = a * 2; // Error: the temporary calculation result a * 2 is an R-value and cannot be bound to an l-value reference
const int& ii = a * 2; // Correct: you can bind a const reference to an rvalue
int &&r = a * 2; // Correct: std::move converts the left value a to the right value, which can be bound to the right value reference

int &i1 = 42; // Error: 42 is a literal constant and cannot be bound to an lvalue reference
int &&r1 = 42; // Correct: 42 is a literal constant that can be bound to an R-value reference

int &i2 = std::move(a); // Error: std::move converted lvalue a to lvalue and cannot be bound to lvalue reference
int &&r2 = std::move(a); // Correct: std::move converts the left value a to the right value, which can be bound to the right value reference

Note: you can bind a const reference (either const &, or const &) to an R-value

The left value is persistent and the right value is short

The most obvious difference between lvalues and lvalues is that lvalues have a persistent state and will not be destroyed immediately; The right value is either a literal constant or a temporary object created during the evaluation of the expression.

Therefore, you can know the right value reference:
1) The referenced object will be destroyed;
2) The object has no other users;

See the previous article for details C + + > the difference between right value reference and left value reference

The variable is an lvalue

A variable is an lvalue. You cannot bind an R-value reference directly to a variable, even if the variable is an R-value reference type.

int a = 42;
int &&rr1 = 42; // Correct: literal constants are right-hand values
int &&rr2 = a;  // Error: variable a is an lvalue
int &&rr3 = rr1; // Error: the R-value reference rr1 is an l-value

std::move function

Header file
You cannot bind an R-value reference to an l-value, but you can convert the l-value to the corresponding R-value reference type by calling the std::move function.

int &&rr1 = 42;
int &&rr4 = rr1; // Error: cannot bind an R-value reference to another R-value reference
int &&rr5 = std::move(rr1); // OK

The move function tells the compiler that we have an lvalue, but we want to treat it like an lvalue. The call to move means a promise that it can no longer be used except to assign to rr1 or destroy it. After you call move, you cannot make any assumptions about the value of the source object after the move.

int *p = new int(42);
int &&r = std::move(*p);
cout << r << endl;
r = 1;
*p = 3; // The compiler does not report an error or prevent the modification of the source object value, but it is not recommended
cout << r << endl;
cout << *p << endl;

Note: unlike most standard library names, using is not provided for move. It is recommended to call std::move directly instead of move. Because the move name is common, applications often define this function. To avoid conflicts with the move function defined by the application, please use std::move.
[======]

Move constructor and move assignment operator

The move constructor (also known as move constructor) and the move assignment operator (also known as move assignment operator) are similar to the copy function (copy constructor, copy assignment operator), but the first two functions "steal" resources from a given object rather than copy resources.

In addition to completing the resource movement, the move constructor must also ensure that the source object is in such a state after the movement: it is harmless to destroy the source object.
Once the resource is moved, the resource no longer belongs to the source object, but to the newly created object. The source object must no longer point to the moved resource.

For example, define a move constructor for the StrVec class to realize the element move from one StrVec to another instead of copy:

class StrVec
{
public:
	StrVec(const StrVec &s); // copy constructor
	StrVec(StrVec &&s) noexcept; // move constructor
	...
private:
	string *elements;
	string *first_free;
	string *cap;
};

StrVec::StrVec(StrVec &&s) noexcept // The move operation should not throw any exceptions
// The member initializer takes over the resources in s
	: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
	// Put s into this state -- it's safe to run destructors aligned
	s.elements = s.first_free = s.cap = nullptr;
}

In the move constructor, the newly created object member initializer takes over the resources in the source object and sets the pointers of the source object to the resources to null to complete the resource movement operation. When the source object is destructed, the resource will not be released, so it is safe for new objects to use the resource.
noexcept indicates that the function does not throw any exceptions.

Move operations, standard library containers, and exceptions

Because the move operation "steals" resources, no resources are usually allocated. Therefore, the move operation usually does not throw an exception. In that case, why do you need to specify noexcept?
This is because unless the compiler knows that our move constructor will not throw an exception, it will think that moving our class object may throw an exception, and do some extra work to deal with this possibility. Therefore, if the confirmation does not throw an exception, it is explicitly indicated with noexcept.

TIPS:
Move constructors and move assignment operators that do not throw exceptions must be marked noexcept.

The move operation usually does not throw exceptions, but it does not mean that exceptions cannot be thrown, and the standard library container can guarantee its own behavior when exceptions occur. For example, vector guarantees that you can call in push_ If an exception occurs in the back (such as insufficient memory), the vector itself will not change.

To avoid this potential problem, unless the vector knows that the move constructor of the element type will not throw an exception, the copy constructor must be used instead of the move constructor during memory reallocation.
If you want to move rather than copy our custom type objects in the case of vector reallocation of memory, you must explicitly tell the standard library that our move constructor can be used safely.

In short: if the move constructor throws an exception, it uses the copy constructor to construct the object. If the move constructor does not throw an exception, it is explicitly declared with noexcept.

move assignment

Move assignment performs the same work as the destructor and move constructor. If our move assignment operator does not throw any exceptions, it should be marked noexcept.
Three steps to define move assignment:

  1. Release the existing resources of the current object;
  2. Take over the resources of the source object;
  3. Set the source object to destructable state;
class StrVec
{
public:
	...
	StrVec& operator=(StrVec &&rhs) noexcept; // move assignment
	...
private:
	string *elements;
	string *first_free;
	string *cap;
};

StrVec& StrVec::operator=(StrVec &&rhs) noexcept
{
	if (this != &rhs) {// Avoid self moving, because the result returned by move may be the object itself
		// Releasing the existing element of this object is equivalent to calling this - > ~ strvec
		free(); 
		// Take over resources from rhs
		elements = rhs.elements;
		first_free = rhs.first_free;
		cap = rhs.cap;

		// Placing rhs in a destructable state
		elements = first_free = cap = nullptr;
	}
	return *this;
}

After moving, the source object must be destructable

Moving resources from one object to another does not destroy the source object, but sometimes the source object will be destroyed after the move operation is completed. Therefore, when writing a move operation, you must ensure that the source object enters a destructable state after the move. Otherwise, destructing the source object may cause resource release, or modify the resource state, resulting in exceptions in the takeover object.

After moving resources, the program should not rely on the data in the source object. Although it is possible to access the data in the source object, the result is uncertain.

TIPS: after moving, the source object must remain valid and destructable, but the user cannot make any assumptions about it. When can I destruct? Depending on the user, it can usually be destructed immediately.

Composite move operation

If we do not declare our copy function, the compiler will synthesize the default version for us when necessary. Similarly, the compiler will synthesize the move function (move constructor and move assignment operator, i.e. move operation) for us.
The copy operation can be defined in three cases:
1) Bit wise copy member;
2) Assign values to objects;
3) Deleted functions;

When does the compiler not synthesize move operations?
Unlike the copy function, the compiler does not synthesize the move operation for some classes. In particular, if a class defines three member functions:
1) copy constructor;
2) copy assignment operator;
3) Destructor;

When does the compiler synthesize the move operation?
Only when a class does not define any copy control member of its own version (i.e. copy function), and each non static data member of the class can be moved (usually an object of built-in type and supporting move operation), the compiler will synthesize move operation for it.

What happens when a class has no move operation?
When a class has no move operation, the normal function matches, and the class will use the copy operation instead.

Example of composite move operation:

struct X
{
	int i; // Built in type
	string s; // string defines its own move operation
};
struct hasX
{
	X mem; // X has a composite move operation
};

int main()
{
	X x, x2 = std::move(x); // Use the composite move constructor
	hasX hx, hx2 = std::move(hx); // Use the composite move constructor
	return 0;
}

Thinking: when does the compiler use the composite copy operation and the composite move operation when we do not define either the copy function or the move function?
My understanding: look at function matching. If the argument used for construction or assignment is an lvalue, use the synthetic copy operation; If it is an R-value, use the composite move operation.

Define default, delete move operations
Unlike the copy operation, the move operation is not implicitly defined as delete. On the contrary, if we explicitly require to compile the composite move operation with = default, but the compiler cannot move all members, the compiler will define the move operation as delete.

So, when does the compiler define the move operation as delete?
The principles are:

  • Different from the copy constructor, the move constructor is defined as delete on the condition that there is a data member, there is a copy constructor, there is no move constructor, or there is no copy constructor, but the move constructor cannot be synthesized. The situation of move assignment is similar.

  • If the move operation of a data member is defined as delete or inaccessible, the move operation of a class is defined as delete.

  • Similar to the copy constructor, if the destructor of the class is defined as delete or inaccessible, the move constructor of the class is defined as delete.

  • Similar to the copy assignment operator, if a class data member is const or referenced, the move assignment of the class is defined as deleted.

For example, suppose Y is a class with a copy constructor defined but no move constructor defined:

class Y {
public:
	Y() = default;
	Y(const Y&) { cout << "Y copy constructor invoked" << endl; }
	Y& operator=(const Y&) { cout << "Y copy assignment invoked" << endl; return *this; }
};

// Since the data member Y does not have a move function (move constructor and move assignment operator), the compiler will not synthesize the move function for hasY. Instead, it will synthesize the copy function
struct hasY {
	hasY() = default;
	hasY(hasY &&) = default; // The compiler does not synthesize the move constructor
	hasY& operator=(hasY&&) = default; // The compiler does not synthesize the move assignment operator

	Y mem; // There will be a delete move constructor and a move assignment operator
};

int main()
{
	hasY hy, hy2 = std::move(hy); // In fact, the move constructor of hasY is not called, but the copy constructor is called
	hasY h3, h4;
	h4 = std::move(h3); // The copy assignment operator is actually called
	return 0;
}

Operation results:
You can see that even if the move function is indicated as default, the move function is not actually called, but the copy function is called.

Y copy constructor invoked
Y copy assignment invoked

There is also interaction between the move operation and the synthesized copy control members: if a class defines the move function, the copy function corresponding to the class synthesis will be defined as delete.

move right value, copy left value

If a class has both a move function and a copy function, the compiler uses ordinary function matching rules to determine which function to use: the left value uses the copy function and the right value uses the move function.

StrVec v1, v2; // v1, v2 are lvalues
v1 = v2; // v2 is an lvalue. Use the copy assignment operator
StrVec getVec(istream &); // getVec returns the right value (temporary object to be destroyed)
v2 = getVec(cin); // getVec returns the right value and uses the move assignment operator

If there is no move function, the corresponding copy function is used, even the right value

When there is only copy function (including composite function) and no move function (including compiler without composite function and user setting function to delete), even the right value, use copy function instead of move function (because there is no).

class Foo {
public:
	Foo() = default; // default constructor
	Foo(const Foo&); // copy constructor
	... // Other functions, but no move constructor
};

Foo x; // Use default constructor to build object x
Foo y(x); // Building objects using cop y constructor
Foo z(std::move(x)); // Use copy constructor instead of move constructor because there is no move constructor

std::move(x) returns a foo & & (R-value) bound to X. since there is no move constructor and only the copy constructor, even if the R-value is used when building object z, foo & & will actually be converted to const foo &, thus calling the copy constructor to construct z.

Copy and swap assignment operators and move operations

std::swap can move resources.

For example, we define class HashPtr:

// Define default constructor and move constructor, and do not define copy constructor and move assignment operator
// It can be inferred that the compiler will not synthesize the copy constructor and the copy assignment operator (implicit delete)
class HashPtr {
public:
	HashPtr() : ps(nullptr), i(0) { } // default constructor
	HashPtr(HashPtr &&p) noexcept : ps(p.ps), i(p.i) { p.ps = 0; } // Move constructor. Since the move constructor is synthesized, the copy operation will not be synthesized
	// assignment(operator =) is both a move assignment operator and a copy assignment operator
	// Note the difference between hashptr & operator = (const hashptr & RHS) {} and hashptr & operator = (const hashptr & & RHS) {}
	HashPtr& operator=(HashPtr rhs) { swap(*this, rhs); return *this; } // assignment operator
	// ... 
private:
	string *ps;
	int i;
};

The parameter of operator = is HashPtr rhs, which means that a copy construct is required when passing parameters. However, we have defined the move construct, and the compiler will not synthesize the copy construct for us, that is, the copy construct is an implicit delete.

Suppose hp and hp2 are HashPtr objects:

HashPtr hp;
HashPtr hp2 = hp; // Error: because the move constructor has been defined, the compiler will not synthesize the copy constructor (delete), but hp is an R-value. It is impossible to call the move constructor to construct hp2
HashPtr hp3 = std::move(hp); // OK: std::move converts hp to an R-value and calls the move constructor to construct hp3
HashPtr hp4, hp5;
hp4 = hp; // Error: because hp is an lvalue, the copy operator uses the copy constructor to construct the formal parameter rhs, but the copy constructor is implicitly delete
hp5 = std::move(hp); // OK:: std::move convert hp to an R-value and call the move constructor to construct the operator = parameter rhs

Suggestion: update the three / five rule

5 copy control members:
1) 1 destructor;
2) Two copy functions: copy constructor and copy assignment operator;
3) Two move functions: move constructor and moveassignment operator;

It should be regarded as a whole. If a class defines any copy operation, it should define all five operations.

  • If a class has a custom copy constructor, it is likely to need to define copy assignment (the same applies to the move function);
  • If a class is a custom destructor, it is likely to need to define a copy function, because some custom object members need a custom copy function to copy;
  • If a class is a custom destructor, it may need to define a move function to reduce the unnecessary overhead caused by copy resources;
  • If a class defines a pointer type data member, it may need to define a destructor to release dynamically applied resources;

Move iterator move iterator

Generally, an iterator dereference (for example, * it, it is an iterator) returns an lvalue pointing to an element, while a move iterator returns an lvalue reference pointing to an element.

Standard library function make_ move_ The iterator can convert a normal iterator to a move iterator.
For example, if you want to expand the size of StrVec (custom dynamic string array) without using the move iterator, you can do this:

void StrVec::reallocate()
{
	// Allocate 2 times the current size
	auto newcapacity = size() ? 2 * size() : 1;
	// Assign raw memory
	auto newdata = alloc.allocate(newcapacity);
	// Move data from old memory to new memory
 	auto dest = newdata; // Point to new array free location
	auto elem = elements; // Points to the next element of the old array

	// On the memory allocated by allocator, call the move constructor of class string one by one
	for (size_t i = 0; i != size(); i++) {
		alloc.construct(dest++, std::move(*elem++));
	}

	free(); // Move complete, free old memory
	// Update pointer
 	elements = newdata;
	first_free = dest;
	cap = elements + newcapacity;
}

Use the move iterator:

void StrVec::reallocate()
{
	// Allocate memory space twice the current size
	auto newcapacity = size() ? 2 * size() : 1;
	auto first = alloc.allocate(newcapacity);
	// Move element
	auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first);

	free(); // Free up old space
	// Update pointer
	elements = first; 
	first_free = last;
	cap = elements + newcapacity;
}

class StrVec complete source code

Click to view the code
class StrVec {
public:
	StrVec() :  // The allocator member is initialized by default
		elements(nullptr), first_free(nullptr), cap(nullptr) { }
	StrVec(const StrVec&);
	StrVec& operator=(const StrVec&);
	~StrVec();
	void push_back(const string&);

	size_t size() const { return first_free - elements; }
	size_t capacity() const { return cap - elements; }
	string *begin() const { return elements; }
	string *end() const { return first_free; }

private:
	static allocator<string> alloc;
	void chk_n_alloc() { if (size() == capacity()) reallocate(); }
	pair<string*, string*> alloc_n_copy(const string*, const string*);
	void free();
	void reallocate();

	string *elements; // Pointer to the first element of the array
	string *first_free; // Pointer to the first free element of the array
	string *cap; // Pointer to the end of the array
};

allocator<string> StrVec::alloc;

StrVec::StrVec(const StrVec& s)
{
	auto newdata = alloc_n_copy(s.begin(), s.end());
	elements = newdata.first;
	first_free = cap = newdata.second;
}

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;
}

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

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

pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e)
{
	auto data = alloc.allocate(e - b);
	return { data, 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);
	}
}

void StrVec::reallocate()
{
	// Allocate memory space twice the current size
	auto newcapacity = size() ? 2 * size() : 1;
	auto first = alloc.allocate(newcapacity);
	// Move element
	auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first);

	free(); // Free up old space
	// Update pointer
	elements = first; 
	first_free = last;
	cap = elements + newcapacity;
}

// Client test code
int main()
{
	StrVec s;
	stringstream stream;
	string str;

	for (int i = 0; i < 50; i++) {
		stream << i + 1;
		stream >> str;
		s.push_back(str);
	}
	cout << s.size() << endl;

	StrVec s2;
	s2 = s;
	cout << s2.size() << endl;
	return 0;
}

Note: it is not recommended to use the move operation arbitrarily.
1) The standard library does not guarantee which algorithms are applicable to the move iterator and which are not.
2) After the move, the source object has an uncertain state. The source object may or may not be destroyed. Calling std::move on it is very dangerous.

Therefore, if you want to use the move operation, you must ensure that there are no other users of the source object after the move, and you must be sure that the move operation is safe. This is not a C + + syntax requirement, but a specification that should be followed when using the move operation.

Posted by jamessw on Sun, 05 Dec 2021 15:47:44 -0800