When we talk about std::unique_ptr we must mention the idea of explicit resource ownership as well as the concept of a resource source and sink. By wrapping a pointer inside std::unique_ptr we state that whoever holds the std::unique_ptr owns the resource explicitly: has complete control over its lifetime. Prior to C++11 this was expressed using std::auto_ptr. Modern C++ deprecated std::auto_ptr and addressed its shortcomings by introducing the std::unique_ptr.
A source is a function that creates a resource then relinquishes its ownership; in other words, it gives the ownership to whoever called it, and from then on, the caller is responsible for releasing that resource.
A sink is a function which accepts a resource as a parameter and assumes ownership of it; in other words, it promises to release said resource once it’s done executing.
Transfer of ownership must be stated explicitly for named instances (variables of type std::unique_ptr) with std::move. Unnamed temporaries can be passed into a variable or as a function parameter without invoking std::move (this will be clearly illustrated in the code sample below).
Explicit ownership of a resource can be “upgraded” to shared ownership by std::move’ing the std::unique_ptr into a std::shared_ptr. It resets the std::unique_ptr to point back at nullptr and initializes the std::shared_ptr with reference count of 1 (also illustrated in the code below).
Worth mentioning is the fact that std::unique_ptr can own arrays of objects allocated with operator new. A partial specialization of std::unique_ptr template for array types also overloads operator[] for easy array element access; it mirrors the syntax of native arrays. However there are limitations of storing arrays inside std::unique_ptr: when creating an array of primitive types an initial value for each element cannot be specified (and each element ends up with an undefined value); when creating an array of user-defined types (classes and structs) said type must have a default constructor. Moreover the size of the array is lost: there is no way to query the std::unique_ptr and find out how many elements the array holds. For those reasons I recommend you stick with std::vector: it’s elements can be initialized with a default or custom value and you can always call size() on it.
The example program below, using inline comments, further details how to transfer the resource ownership and how to express the idea of source and sink funtions.
Complete listing on GitHub: unique.cpp and T.hpp. Merry Christmas and Happy New Year!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
#include <memory> #include <vector> #include <cassert> #include "T.hpp" using namespace std; using T_u_ptr = unique_ptr<T>; using T_s_ptr = shared_ptr<T>; // Creates and gives away explicit ownership... T_u_ptr source() { return make_unique<T>(); } // Assumes explicit ownership, "t" gets deallocated at the end of this function void sink(T_u_ptr t) { t->foo(); } // Does NOT assume explicit ownership because we're taking by reference void NOT_sink(T_u_ptr& t) { t->foo(); } // Assumes ownership, but then hands it back if the caller captures the return value, // otherwise releases the resource T_u_ptr sink_or_pass_thru(T_u_ptr t) { t->foo(); return t; } // Just a function that takes a shared_ptr... void shared(T_s_ptr t) { t->foo(); } int main() { auto t1 = source(); sink(move(t1)); // We have to std::move it, because copy-constructor of unique_ptr = delete, // by using std::move we're forcing the use of the move constructor (if one exists), // this would have worked without std::move if using std::auto_ptr (now deprecated) // and it would have stole the ownership without warning us!!! assert(!t1); // "t1" is now pointing to null because of the std::move above auto t2 = source(); NOT_sink(t2); // "t2" still pointing to resource after this call assert(t2); sink(move(t2)); // and now "t2" is gone... assert(!t2); sink(source()); // No need for explicit std::move, temporary is captured as r-value reference // so the unique_ptr's move constructor is automatically invoked auto t3 = source(); auto t4 = sink_or_pass_thru(move(t3)); // Effectively moves the ownership from "t3" to "t4" assert(!t3 && t4); sink_or_pass_thru(source()); // Takes ownership, but deletes the resource since nobody captures the return value T_s_ptr t5 = source(); // Create and "upgrade" from unique to shared ownership T_s_ptr t6 = move(t4); // unique_ptr's must be explicitly std::move'ed to shared_ptr's assert(!t4 && t5 && t6); shared(t6); // No transfer of ownership, just using a shared resource here... // PRIMITIVE ARRAYS... constexpr const int N = 3; auto a1 = make_unique<int[]>(N); // Allocates N int's, size of array is lost, values are undefined auto a2 = vector<int>(N, int{42}); // Allocates and value-initializes N int's, size is known, values are well defined auto a3 = move(a1); // Transfer ownership of from "a1" to "a3" assert(!a1 && a3); a3[N - 1] = 1; // Access the last int of the array // ARRAYS... auto a4 = make_unique<T[]>(N); // Create an array of N T's, size is lost, T must have a default constructor auto a5 = vector<T>(N, T{42}); // Create a vector of N T's, size is known, initialize with custom T auto a6 = move(a4); // Transfer ownership from "a4" to "a6" assert(!a4 && a6); a6[N - 1].foo(); // Access the last T of the array } |