Article 18 Using std::unique_ptr manages resources with exclusive ownership

Using std::unique_ptr manages resources with exclusive ownership

Whenever you need to use a smart pointer, std::unique_ptr is basically the most appropriate. You can think that by default, std::unique_ptr is equivalent to the original pointer, and for most operations (including dereference), they execute exactly the same instructions. This means you can even use it when memory and time are tight. If the original pointer is small enough and fast enough, std::unique_ptr can almost certainly meet your requirements.

std::unique_ptr embodies the semantics of proprietary ownership. A non empty std::unique_ptr always has the resource it points to. Move an std::unique_ptr moves ownership from the source pointer to the target pointer (the original pointer is set to null). Copy operation is not allowed, because if you can copy an std::unique_ptr, you will get two STD:: unique pointing to the same content_ PTR, each thinks it has its own resources, and there will be repeated destruction during destruction. Therefore, std::unique_ptr only supports mobile operations. When STD:: unique_ When PTR is destroyed, the resource it points to also executes the destructor. The original pointer needs to explicitly call delete to destroy the resource pointed to by the pointer.

std::unique_ptr is commonly used as the return type of a factory function of an object in an inheritance hierarchy. Suppose we have an inheritance structure of the base class Investmen.

class Investment{ ... };
class Sock : public Investment { ... };
clsas Bond : public Investment { ... };
class RealEstate : public Investment { ... };

The factory function of this inheritance relationship allocates an object on the heap and then returns a pointer. The caller destroys the object when it is not needed, which perfectly matches std::unique_ptr, because the caller is responsible for the resource returned by the factory (that is, the exclusive ownership of the resource), and std::unique_ptr will automatically destroy the content pointed to. You can say so

template<typename... Ts>
std::unique_ptr<Investment>
makeInvestment(Ts&&... params);

The caller should use the returned STD:: unique in a separate scope_ PTR smart pointer:

{
    ...
    auto pInvestment = makeInvestment(arguments);
    ...
}	//destroy *pInvestment

If the ownership chain is interrupted due to exceptions or other atypical control flows (such as early return or break in the loop), STD:: unique of the managed resource is owned_ PTR will ensure that the destructor of the pointed content is called to destroy the corresponding resources.

By default, destruction will be done through delete, but STD:: unique can be customized during construction_ PTR refers to the destructor of the object: any function (or function object, including lambda). If the object created through makeInvestment cannot be deleted directly, a log should be written first, which can be realized as follows:

auto delInvmt = [](Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&& params)
{
	std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt);
    if(/*a stock object should be created*/)
    {
        pInv.reset(new Stock(std::forward<Ts>(params)...));
    }
    else if(/*a bond object should be created*/)
    {
		 pInv.reset(new Bond(std::forward<Ts>(params)...));
    }
    else if(/*a RealEstate object should be created*/)
    {
        pInv.reset(new RealEstate(std::forward<Ts>(params)...));
    }
    return pInv;
}

This implementation is really great, if you understand:

  • delInvmt is a custom destructor returned from makeInvestment. All custom destructors accept the original pointer of the object to be destroyed, and then execute the destruction operation.
  • When using a custom remover, you must pass it as the second parameter to std::unique_ptr.
  • Create an empty STD:: unique when implementing the basic policy of makeInvestment_ PTR, then point to an object of an appropriate type, and then return as the second parameter of the constructor in order to associate a custom remover with pInv.
  • Try to assign the original pointer (such as new creation) to std::unique_ptr cannot be compiled because there is no implicit conversion from the original pointer to the smart pointer. This implicit conversion is a problem, so it is prohibited. This is why the new pointer is passed through reset.
  • The parameter type of the custom remover is Investment *. Although the real object type is created inside makeInvestment, it is finally deleted as an Investment * object in the lambda expression. This means that we delete the derived class instance through the base class pointer. To do this, the base class must be a virtual destructor.
  • When new is used, std::forward is used as a parameter to perfectly forward to makeInvestment, which makes all the information provided by the caller available to the constructor of the object being created.
class Investment{
public:
  	...
    virtual ~Investment();
    ...
};

When using the default delegator, it is reasonable to assume std::unique_ptr is the same size as the original pointer. This may no longer be the case when customizing the remover. The delegator is a function pointer, which usually makes STD:: unique_ The bytes of PTR are increased from one to two. For the function object of the delegator, the size depends on the number of states stored in the function object. Stateless function objects (such as non captured lambda expressions) have no effect on the size, which means that when the user-defined delegator can be implemented by lambda, try to use lambda.

auto delInvmt = [](Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
};
template<typename... Ts>
std::unique_ptr<Investment,decltype(delInvmt)>
makeInvestment(Ts&& params);		//The size of the return value is the same as that of Investment *

void delInvmt2(Investment* pInvestment)
{
    makeLogEntry(pInvestment);
    delete pInvestment;
}
template<typename... Ts>
std::unique_ptr<Investment, void(*)(Investment*)>
makeInvestment(Ts&&... params);		//The size of the return value is equal to the size of Investment * plus at least the size of the function pointer

std::unique_ There are two forms of PTR, one for a single object (std::unique_ptr < T >) and one for an array (std::unique_ptr < T [] >).

STD:: unique of array_ The existence of PTR should not be used because better data containers such as std::array, std::vector and std::string should replace the original array.

std::unique_ptr is a method to express proprietary ownership in C++11, but one of its most attractive functions is that it can be easily and efficiently converted to std::shared_ptr

std::shared_ptr<Investment> sp = makeInvestment(arguments);

That's why std::unique_ptr is well suited for key parts of the return type of factory functions. Factory functions cannot determine whether callers want to use proprietary semantics for the objects they return, or share ownership STD:: shared_ Whether PTR is more appropriate. By returning std::unique_ptr, the factory provides the caller with the most effective smart pointer, but they do not prevent the caller from replacing it with a more flexible brother.

Key points shorthand

  • std::unique_ptr is lightweight and fast, with smart pointer of shift only type, and implements exclusive ownership definition for managed resources
  • By default, the resource destructor is implemented by the delete operator, but a user-defined delector can be specified. Stateful delectors and delectors implemented with function pointers will increase STD:: unique_ Object size of PTR type
  • Set STD:: unique_ Convert PTR to std::shared_ptr is easy to implement.

Added by aviavi on Thu, 10 Feb 2022 09:20:31 +0200