- Modern C++:Efficient and Scalable Application Development
- Richard Grimes Marius Bancila
- 707字
- 2021-06-10 18:28:17
Managing exclusive ownership
The unique_ptr class is constructed with a pointer to the object it will maintain. This class provides the operator * to give access to the object, dereferencing the wrapped pointer. It also provides the -> operator, so that if the pointer is for a class, you can access the members through the wrapped pointer.
The following allocates an object on the free store and manually maintains its lifetime:
void f1()
{
int* p = new int;
*p = 42;
cout << *p << endl;
delete p;
}
In this case, you get a pointer to the memory on the free store allocated for an int. To access the memory--either to write to it or read from it--you dereference the pointer with the * operator. When you are finished with the pointer, you must call delete to deallocate the memory and return it to the free store. Now consider the same code, but with a smart pointer:
void f2()
{
unique_ptr<int> p(new int);
*p = 42;
cout << *p << endl;
delete p.release();
}
The two main differences are that the smart pointer object is constructed explicitly by calling the constructor that takes a pointer of the type that is used as the template parameter. This pattern reinforces the idea that the resource should only be managed by the smart pointer.
The second change is that the memory is deallocated by calling the release method on the smart pointer object to take ownership of the wrapped pointer, so that we can delete the pointer explicitly.
Think of the release method releasing the pointer from the ownership of the smart pointer. After this call, the smart pointer no longer wraps the resource. The unique_ptr class also has a method get that will give access to the wrapped pointer, but the smart pointer object will still retain ownership; do not delete the pointer obtained this way!
Note that a unique_ptr object wraps a pointer, and just the pointer. This means that the object is the same size in memory as the pointer it wraps. So far, the smart pointer has added very little, so let's look at another way to deallocate the resource:
void f3()
{
unique_ptr<int> p(new int);
*p = 42;
cout << *p << endl;
p.reset();
}
This is deterministic releasing of the resource, and means that the resource is released just when you want it to happen, which is similar to the situation with the pointer. The code here is not releasing the resource itself; it is allowing the smart pointer to do it, using a deleter. The default deleter for unique_ptr is a functor class called default_delete, which calls the delete operator on the wrapped pointer.
If you intend to use deterministic destruction, reset is the preferred method. You can provide your own deleter by passing the type of a custom functor class as the second parameter to the unique_ptr template:
template<typename T> struct my_deleter
{
void operator()(T* ptr)
{
cout << "deleted the object!" << endl;
delete ptr;
}
};
In your code, you will specify that you want the custom deleter, like this:
unique_ptr<int, my_deleter<int> > p(new int);
You may need to carry out an additional clean up before deleting the pointer, or the pointer could be a obtained by a mechanism other than new, so you can use a custom deleter to ensure that the appropriate releasing function is called. Note that the deleter is part of the smart pointer class, so if you have two different smart pointers using two different deleter this way, the smart pointer types are different even if they wrap the same type of resource.
Of course, you are most likely to allow the smart pointer to manage the resource lifetime for you, and to do this you simply allow the smart pointer object to go out of scope:
void f4()
{
unique_ptr<int> p(new int);
*p = 42;
cout << *p << endl;
} // memory is deleted
Since the pointer created is a single object, it means that you can call the new operator on an appropriate constructor to pass in initialization parameters. The constructor of unique_ptr is passed a pointer to an already constructed object, and the class manages the lifetime of the object after that. Although a unique_ptr object can be created directly by calling its constructor, you cannot call the copy constructor, so you cannot use initialization syntax during construction. Instead, the Standard Library provides a function called make_unique.
This has several overloads, and for this reason it is the preferred way to create smart pointers based on this class:
void f5()
{
unique_ptr<int> p = make_unique<int>();
*p = 42;
cout << *p << endl;
} // memory is deleted
This code will call the default constructor on the wrapped type (int), but you can provide parameters that will be passed to the appropriate constructor of the type. For example, for a struct that has a constructor with two parameters, the following may be used:
void f6()
{
unique_ptr<point> p = make_unique<point>(1.0, 1.0);
p->x = 42;
cout << p->x << "," << p->y << endl;
} // memory is deleted
The make_unique function calls the constructor that assigns the members with non-default values. The -> operator returns a pointer and the compiler will access the object members through this pointer.
There is also a specialization of unique_ptr and make_unique for arrays. The default deleter for this version of unique_ptr will call delete[] on the pointer, and thus it will delete every object in the array (and call each object's destructor). The class implements an indexer operator ([]) so you can access each item in the array. However, note that there are no range checks, so, like a built-in array variable, you can access beyond the end of the array. There are no dereferencing operators (* or ->), so a unique_ptr object based on an array can only be accessed with array syntax.
The make_unique function has an overload that allows you to pass the size of the array to create, but you have to initialize each object individually:
unique_ptr<point[]> points = make_unique<point[]>(4);
points[1].x = 10.0;
points[1].y = -10.0;
This creates an array with four point objects initially set to the default value, and the following lines initialize the second point to a value of (10.0, -10.0). It is almost always better to use vector or array than unique_ptr to manage arrays of objects.
The important point about the unique_ptr class is that it ensures that there is a single copy of the pointer. This is important because the class destructor will release the resource, so if you could copy a unique_ptr object it would mean more than one destructor will attempt to release the resource. Objects of unique_ptr have exclusive ownership; an instance always owns what it points to.
You cannot copy assign unique_ptr smart pointers (the copy assignment operator and copy constructor are deleted), but you can move them by transferring ownership of the resource from the source pointer to the destination pointer. So, a function can return a unique_ptr because the ownership is transferred through move semantics to the variable being assigned to the value of the function. If the smart pointer is put into a container, there is another move.