C + + experience -- managing resources in the form of objects

Posted by swell on Sat, 19 Feb 2022 23:24:40 +0100

The so-called resources are things borrowed from the system during the operation of the program. Once used, when you don't use them, you must return them. The most common resource of C + + is dynamic memory allocation, that is, the memory we dynamically allocate from the system with new or malloc. Of course, resources also include other resources, such as file descriptor, socket, mutex lock, database connection, etc.

So why manage resources in the form of objects? Managing resources by objects is a trench we dug to fight against resource leakage.
Mainly because:

  • The concept of object-based resource management is often called "resource acquisition opportunity is initialization opportunity". (Resource Acquisition Is Initialization; RAII);
  • It can rely on the automatic call mechanism of C + + destructor to ensure that resources are recycled.

Remember we were talking about the example of factory model, KFC making chicken legs. We have a kind of drumstick (various flavors), and other kinds of drumsticks are inherited from this kind. There is a factory function that allows us to easily create chicken legs with specific flavors.

class Chicken 
{
public:
    virtual void show() = 0;
}

class ChickenA  public Chicken 
{
public:
    void show(){std::cout << "this chickenA..."; }
}

...

Chicken* createChicken();

In the above class, once we call createChicken and successfully return the function object, we have the responsibility to delete it.

Assumptions:

{
    auto pChicken = createChicken();
    ...
    delete pChicken;
}

The above call seems very appropriate, but who can guarantee The operation part of does not return in advance or some other operations, so that the control flow does not proceed as we expected.

Don't retort that the situation you said won't exist. You know, when code is maintained, you can't guarantee anything, especially when others are maintaining it.

If the control flow will never execute delete pChicken again; Then I'm sorry to tell you that the memory is leaking.

C + + smart pointer provides us with a convenient solution.

std::auto_ptr<Chicken> pChicken(createChicken());

In this way, when the object leaves the scope, its destructor will be called automatically. We don't have to worry about resource leakage. But Auto_ There is a problem with the resources managed by PTR, that is, there is absolutely no more than one auto_ptr pointer points to the managed object.

std::auto_ptr<A> pA1(A());

//std::auto_ptr<A> pA2 = pA;
std::auto_ptr<A> pA2(pA1);    //At this time, pA2 points to the object and pA1 is set to null

After C++11, reference the counting smart pointer shared_ptr can meet more of our needs, such as auto_ptr cannot be copied. And multiple shared_ptr objects can host a pointer object at the same time.

shared_ptr<Chicken> pChicken(createChicken());

It should be noted that the deconstruction of a single pointer and the deconstruction of an array. Because shared_ptr and auto_ptr cannot be used as a destructor array.

Simply put, many times we need to implement our own resource management classes. In this case, we should pay attention to their copy behavior, because their copy behavior may not give us a good goal and role.

Generally speaking, in this case, we will prohibit the default copy behavior and use the reference counting method instead. It should be noted that suppose we use shared at the bottom_ PTR, we should pay attention to some situations that do not need to be deleted during destructions.

Even if we have the fortress of resource management class, it is inevitable to access the original resources in some cases. Therefore, each RAII object should provide a way to obtain the resources it manages. In half cases, there are two ways, display conversion and implicit conversion.

int daysMaked(const Chicken* pi);

int dat = daysMaked(pChicken);

The above call method fails to pass the compilation, because the function daysMaked needs a Chicken * pointer, but we send in shared_ PTR < Chicken >.

Usually, there are two ways to achieve the goal, that is, the above-mentioned display conversion and implicit conversion.

int dat = daysMaked(pChicken.get());

Later, we will talk in detail about how to implement display conversion and implicit conversion in our own RAII class.

Like almost all pointers, shared_ptr and auto_ptr also overloads pointer values and operators (operator * and operator - >), which allow pointers to be implicitly converted to bottom pointers.

shared_ptr<Chicken> pChicken(createChicken());
pChicken->show();
(*pChicken).show();

Let's take a look at the following example. The following first provides two capis.

FontHandle getFont();
void releaseFont(FontHandle fn);

Then there is a resource management class Font.

class Font
{
public:
    explicit Font(FontHandle f) : m_f(f){}  
    ~Font() {releaseFont(m_f); };
private:
    FontHandle m_f;   //Actual underlying font resources
}

According to normal understanding, we have basically completed the preparation of resource management class. However, when there are a large number of requirements to deal with the FontHandle object, we will have the requirement to convert the Font object into the FontHandle object very frequently according to the above description. For example, the following call:

void changeFontWeight(FontHandle f, int weight);
void changeFontSize(FontHandle f, int size);
...

At this time, providing a display conversion method in the resource management class is the most common approach we can think of.

class Font
{
public:
    ...
    FontHandle& get(){ return m_f; }
private:
    FontHandle m_f; 
}

In this way, it will be easier for us to operate the underlying resources.

void changeFontWeight(FontHandle f, int weight);

Font f(getFont());
int weight = 20;
...
changeFontWeight(f.get(), weight);

Have you found that when we provide the display conversion method get in the resource class, it provides non negligible convenience for the need to convert a large number of Font objects into fonthandle objects. The only annoyance is that the get method is called every time. Therefore, there is an implicit conversion method.

class Font
{
public:
    ...
    operator FontHandle() const { return m_f; }
private:
    FontHandle m_f; 
}

The following example is an implicit conversion, which is really convenient.

void changeFontSize(FontHandle f, int size);

Font f(getFont());
int size= 20;
...
changeFontSize(f, size);

But inevitably, implicit transformation has a hidden danger that is not often noticed.

Font f1(getFont());
FontHandle f2 = f1;

In this way, we originally wanted to assign a value to the Font object f1, but the implicit conversion made us copy a Fonthandle object. If f1 is destroyed and the bottom Font resource FontHandlle is released, the object f2 will be suspended.

  • When it is required to access the original resources, RAII resource management class should provide a method to access the resource objects it manages;
  • Access to the original resources can be obtained through display conversion or implicit conversion. In half, display conversion is safer, but implicit conversion is more convenient for the caller.

The test cases of the above font resources are as follows:

#include<iostream>

class FontHandle
{
public:
	FontHandle(int a, int b) : m_size(a),m_weight(b) {}
	
	void changeWeight(int w) { m_weight = w; }
	void changeSize(int s) { m_size = s; }
	
	int weight() { return m_weight; }
	int size() { return m_size; }
	
private:
	int m_size{ 0 };
	int m_weight{ 0 };
};

class Font
{
public:
	Font(FontHandle f) : m_f(f) { }
	FontHandle& get() { return m_f; }	
	operator FontHandle() const { return m_f; }
	
private:
	FontHandle m_f;
};

void changeFontWeight(FontHandle& f, int weight)
{
	f.changeWeight(weight);
}

void changeFontSize(FontHandle f, int weight)
{
	f.changeSize(weight);
}

FontHandle getFont()
{
	FontHandle f(3, 10);
	return f;
}

int main()
{
	Font f(getFont());
	
	int weight = 20;
	changeFontWeight(f.get(), weight);	//Display conversion 
	std::cout << f.get().weight() << std::endl;
	
	changeFontSize(f, 40);	//Implicit transformation
	std::cout << f.get().weight() << std::endl;
	
	//Object virtual hanging 
	FontHandle f2 = f;
	delete &f;
	
	std::cout << f2.weight() << std::endl; //Abnormal operation
	
	return 0;
} 

Topics: C++ Back-end