C + + | classes that behave like values, classes that behave like pointers, and swap functions handle self assignment

concept

The two statements that behavior is like a value class and behavior is like a pointer class are actually quite awkward, which is one of the shortcomings of the translation of C++Primer...

In fact, they mean:

  • Classes that behave like values: each class object has its own implementation
  • Classes that behave like pointers: objects of all classes share resources of the class (similar to shared_ptr smart pointers, reference count + 1 for each object holding the resource, reference count - 1 for each object releasing the resource, and memory is released when reference count is 0)

The content of this blog is similar to class and Smart pointer Two blogs are related. Students who don't know can look at these two blogs first.

Class of behavior image value

For class managed resources, each object should have its own copy (Implementation). For example, for the following pointer of string type, when using the copy constructor or assignment operator, each object copies the string pointed to by the pointer member ps rather than ps itself. In other words, each object has a ps instead of adding a reference count to the ps.

class A
{
	int i = 0;
    string* ps;
public:
    A(const string &s = string()): ps(new string(s)), i(0) {}
    A(const A &a): ps(new string(*a.ps)), i(a.i) {}
    A& operator=(const A&);
    ~A() { delete ps; }
};

A& A::operator=(const A& a)
{
    string* newps = new string(*a.ps); // Copy the value pointed to by a.ps to the local temporary object newps
    delete ps;  // Destroy the memory pointed to by ps to avoid old memory leakage
    ps = newps; 
    i = a.i;
    return *this; // Returns a reference to this object
}

Why implement the assignment operator like this?

A& A::operator=(const A& a)
{
	delete ps;  // Destroy the memory pointed to by ps to avoid memory leakage
    ps = new string(*(a.ps)); 
    i = a.i;
    return *this; // Returns a reference to this object
}

This is because if a and * this are the same object, delete ps will release the string pointed to by * this and a. Next, when we try to copy * (a.ps) in the new expression, we will access a pointer to invalid memory (i.e. null pointer), and its behavior and result are undefined.

Therefore, the first implementation method can ensure that the operation of destroying the existing members of * this is absolutely safe and will not produce null pointers.

Classes that behave like pointers

concept

For classes that behave like pointers, when using the copy constructor or assignment operator, each object copies the ps itself rather than the string pointed to by the pointer member ps. In other words, every object counts the ps that points to a string.

Therefore, the destructor cannot roughly release the string pointed to by ps. it can release the string only when the last class A object pointing to the string is destroyed. We will find that this feature is very consistent with shared_ptr function, so we can use shared_ptr to manage resources in classes like pointers.

However, sometimes we need programmers to manage resources directly, so we need to use reference count.

Reference count

operation mode:

  • Each constructor (except the copy constructor) creates a reference count to record how many objects share state with the object being created. When we create an object, only one object shares the state, so initialize the counter to 1.
  • The copy constructor does not assign new counters, but copies the data members of the given object, including counters. The copy constructor increments the shared counter to indicate that the state of a given object is shared by a new user.
  • The destructor decrements the counter to indicate that there is one less user in the shared state. If the counter changes to 0, the destructor releases the state.
  • The copy assignment operator increments the counter of the right operand and decrements the counter of the left operand. If the counter of the left operand changes to 0, it means that there is no user in its shared state, and the copy assignment operator must destroy the state.

The only challenge is to determine where to store the reference count. Counters cannot be directly members of an A object. for instance:

A a1("cmy");
A a2(a1); // a2 and a1 point to the same string
A a3(a2); // a1, a2 and a3 all point to the same string

If the counter is saved in each object, the counter of a1 can be incremented and copied to a2 when a2 is created. When a3 can be created, it is true that the counter of a1 can be updated, but how to find a2 and update its counter?

So what about counters?

Dynamic memory implementation counter

class A
{
	int i = 0;
    string *ps;
    size_t *use; // Record how many objects share * ps members
public:
    A(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) {}
    A(const A &a): ps(new string(*a.ps)), i(a.i), use(a.use) { ++*use; }
    A& operator=(const A&);
    ~A() {}
};
A::~A(){
	if(--*use == 0){ // The reference count becomes 0
		delete ps; // Free string memory
		delete use; // Free counter memory
	}
}
A& A::operator=(const A& a)
{
	++*(a.use); // The reason why the counter self increment operation is put in front of this
	// This is to prevent ps and use from being released directly due to the self decrement of the counter during self assignment
	if(--(*use) == 0){
		delete ps;
		delete use;
	}
    ps = a.ps;
    i = a.i;
    use = a.use;
    return *this; // Returns a reference to this object
}

Class swap

concept

When we design the swap of a class, although the logic is as follows:

A tmp = a1;
a1 = a2;
a2 = tmp;

However, if it is implemented in this way, a new object tmp needs to be created. The efficiency is very low, resulting in a waste of memory space. Therefore, what we actually want is such a logical implementation:

string *tmp = a1.ps;
a1.ps = a2.ps;
a2.ps = tmp;

Creating A string type always saves memory than creating A class A object. Specific implementation:

class A
{
	friend void swap(A&, A&);
};
inline void swap(A& a1, A& a2){
	using std::swap;
	swap(a1.ps, a2.ps);
	swap(a1.i, a2.i);
}

swap implements self assignment

Assignment operators using copy and exchange:

A& A::operator=(A a){ // Pass the value, and use the copy constructor to copy the actual parameter (right operand) to generate the temporary quantity a
	swap(*this, a); // a now points to the memory used by * this
	return *this; // The scope of a ends, is destroyed, and the ps in a is delete d
}

The above overloaded assignment operator parameter is not a reference, that is, a is a copy of the right operand.

In the function body, swap exchanges the data members in a and * this* ps of this points to a copy of the string in the operand on the right* The original ps of this is stored in a. However, after the function body is executed, a is destroyed as a local variable, and the ps in a is delete d, that is, the original memory of the left operand (* this) is released.

The interesting thing about this technique is that it automatically handles self assignment and is inherently abnormally safe.

Tags: C++ C#

Posted on Tue, 23 Nov 2021 21:04:46 -0500 by ronald29x