Probably the best feature introduced by C++11 is std::unique_ptr. It will automagically make sure your dynamically allocated objects are deleted when you don’t use them anymore.
In previous versions of C++, you needed to rely exclusively on documentation and conventions to ensure dynamically allocated memory was handled properly. With C++11, you can ask the compiler to help you out and enforce ownership semantics. Take the following code for example, you need to ensure you call delete when you are done with the object created by function factory(). The compiler will not complain if you forget and you end up with a memory leak.
1 2 3 4 5 6 7 8 |
struct A { int field; }; A* factory() { return new A(); } void useFactory() { A* ptr = factory(); std::cout << ptr->field << std::endl; // you must remember to call delete delete ptr; } |
What can std::unique_ptr do for you ?
With std::unique_ptr, you cannot casually forget to delete the pointer. std::unique_ptr owns the allocated object unless this ownership is explicitly transferred to another entity (eg. another unique_ptr, a std::shared_ptr). When a std::unique_ptr goes out of scope, it will delete any object it owns.
1 2 3 4 5 6 7 |
struct A { int field; }; std::unique_ptr<A> factory() { return std::unique_ptr<A>(new A()); } void useFactory() { std::unique_ptr<A> ptr = factory(); std::cout << ptr->field << std::endl; // ptr will delete allocated A automatically } |
The most explicit way of transferring ownership is via function std::move(). Using the same above example, we can see how it works.
1 2 3 4 5 6 7 8 |
struct A { int field; }; std::unique_ptr<A> factory() { return std::unique_ptr<A>(new A()); } void useFactory() { std::unique_ptr<A> ptr1 = factory(); std::unique_ptr<A> ptr2 = std::move(ptr1); // now ptr2 owns the instance of A // and ptr1 is holding a null pointer } |
You cannot directly assign an instance of std::unique_ptr to another. This rule ensures that no two unique_ptr can claim ownership of the same dynamically allocated object. It prevents deleting an object multiple times.
This is the magic of std::unique_ptr, you always know who owns the pointed value. By consistently using unique_ptr, you know at a glance when you own a heap object and when you yield ownership to another. Moreover, this rule is enforced by the compiler — the less you need to worry about, the better.
How do you use std::unique_ptr ?
std::unique_ptr helps you express your intent with regards to ownership transfer. Transfer of ownership occurs in the following general cases :
- Returning a dynamically allocated object from a function
- Passing a such an object to a function
Returning a std::unique_ptr
Returning a heap-allocated object from a function requires that the caller delete it when it is done with.
By returning a unique_ptr, you ensure the caller takes ownership of the pointed object. In the example below, the signature of createVector() ensures the caller takes the responsibility for releasing the created vector. Normal flow of execution guarantees memory will be released whenever the returned unique_ptr is destroyed.
1 2 3 4 5 6 7 8 9 10 11 12 |
typedef std::vector<int> Vector; std::unique_ptr<Vector> createVector() { return std::unique_ptr<Vector>(new Vector()); } void caller() { std::unique_ptr<Vector> ptr = factory(); // use the returned vector // nothing needs to be done here // the Vector will be deleted } |
Passing a std::unique_ptr to a function
A function taking a unique_ptr takes definitive ownership of the pointed object. The caller will not even have the ability to access the pointed object after the function call.
Notice the call to function std::move(), it is required and helps you actually see that ownership is transferred to the function.
1 2 3 4 5 6 7 8 9 10 11 |
void takePtr(std::unique_ptr<int> ptr) { /* ... */ } void caller() { std::unique_ptr<int> ptr = new int(5); // explicitly transfer ownership with std::move takePtr(std::move(ptr)); // now ptr holds a null pointer. takePtr() has effectively taken ownership. // a temporary (rvalue reference) is implicitly moved takePtr(std::unique_ptr<int>(new int(6))); } |
Passing a const reference to a unique_ptr
The called function can use the pointed object but may not interfere with its lifetime. The caller function keeps ownership of the pointed object.
In my opinion, there is no real benefit to using a const reference to a unique_ptr from simply using a raw pointer to the object. On the contrary, it introduces a constraint on the caller that it must manage the pointed object through a unique_ptr. But what if your caller context requires you have shared ownership (std::shared_ptr)? Since we mostly care about whether ownership is transfered (not how ownership is cared for), a raw pointer is perfect.
1 2 3 4 5 6 7 8 9 10 |
void useUniquePtr(const std::unique_ptr<int> & ptr) { /* ... */ } void useRawPtr(int * ptr) { /* ... */ } void caller() { std::unique_ptr<int> ptr = new int(5); // useUniquePtr() can use but does not own the pointed object useUniquePtr(ptr); // same semantics useRawPtr(ptr.get()); } |
Passing a non-const reference to a unique_ptr
This is, in my opinion, the most complex situation. The called function may or may not take ownership of the passed pointer. “may or may not” is really part of the functions contract. If the called function uses std::move() on the passed unique_ptr, it effectively takes onwnership and once it gives control back to the caller, the caller does not own the pointer anymore, it doesn’t even know the value of the pointer anymore. On the other hand, if the callee merely uses the provided pointer without moving it, the caller still owns the pointed object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void useUniquePtr(std::unique_ptr<int> & ptr) { /* ... */ } void caller() { std::unique_ptr<int> ptr = new int(5); useUniquePtr(ptr); // if useUniquePtr() used std::move() on ptr, it took ownership // of the pointed int, so ptr is now holding a nullptr // otherwise it still owns the int and will delete it when the // current scope ends if (ptr.get() == nullptr) std::cout << "ptr has been moved away" << std::endl; else std::cout << "ptr is still ours" << std::endl; } |
Conclusion
C++11 provides us with a great tool for managing the lifetime of dynamically allocated object. When used consistently, std::unique_ptr lets your code really express when ownership of a dynamic object is transferred and in which direction directly. The compiler will even see that the semantics are respected at compile-time.
However, they are of no use when you don’t intend to express ownership semantics. Pass raw pointers whenever you can but make sure that you use std::unique_ptr or another similar smart pointer when you mean to transfer ownership.