Log in

std::unique_ptr - Ben FrantzDale [entries|archive|friends|userinfo]
Ben FrantzDale

[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

std::unique_ptr [Mar. 29th, 2011|11:15 pm]
Ben FrantzDale
[Tags|, , , , ]
[Current Location |Home]

Yesterday I posted about rvalue references and move semantics. What's really impressive to me is the range of problems solved by the introduction of rvalue references. An interesting example of this is removing sharp edges from my old friend std::auto_ptr, allowing it to be deprecated in favor of the new std::unique_ptr.

std::auto_ptr was introduced to provide a RAII wrapper to pointers. This means you can put the logic of "I want to allocate memory here that I'll clean up at the end of the function" all in one place rather than scattering that logic around a function. It also means a factory can return an auto_ptr and not worry about leaking memory. That is, if create() allocates an object of type T and returns a pointer to it, the caller can't really know (short of documentation) if the object returned needs to be deleted later or if it's just a pointer to an existing object. With auto_ptr, you can do this:
std::auto_ptr<T> create();

create(); // OK, creates a T that 
          // gets deleted because the temporary auto_ptr goes out of scope.
T* it = create().release(); // Safe -- calling code is knowingly taking 
              // responsibility for the memory.
std::auto_ptr<T> it = create(); // Safe

This is all well and good. It gets murky, though. As described, auto_ptr has very odd copy semantics. In particular, copying modifies the source of the copy(!) This means that you can't copy a const auto_ptr<T> (which has its own interesting ramifications—see Herb Sutter link below), but it also means you can't expect auto_ptr to play nice with containers or algorithms that expect assignment to just—I don't know—assign.

That murkyness has a silver lining: It allows for an idiom like the factories-return-by-auto_ptr usage above but for consumers: consumers take auto_ptr by value (not reference). This makes consumers relatively safe:
std::auto_ptr<T> produce();
void consume(std::auto_ptr<T> x);

std::auto_ptr<T> it = produce();
consume(it); // Safe (but quietly leaves it set to NULL).
T* baz = new T();

consume(baz); // Safe -- Won't compile -- you'll be 
              // told it wants an auto_ptr by value.
consume(std::auto_ptr<T>(baz)); // Safeish -- you will 
           // still have baz after this, but you should 
           // know better than to use it.

The danger is that anyone who takes an auto_ptr could slurp it up (even if they don't now, they could change to take it by value some time down the road, and your code wouldn't change!).
void looks_innocent_0(const std::auto_ptr<T>& x);
void looks_innocent_1(const std::auto_ptr<T> x);

std::auto_ptr<T> it = produce();

looks_innocent_0(it); // Looks like it's just looking at it.
looks_innocent_1(it); // Looks the same but actually steals it.
consume(it); // Screwed: it is NULL.

For more on auto_ptr, see Herb Sutter's Using auto_ptr Effectively.

Enter unique_ptr.

unique_ptr is almost exactly like auto_ptr except that it doesn't have the weird move-on-copy semantics. Instead it has no copy operation at all, just std::swap(•,•) and construction and assignment from an rvalue reference; that is, explicit move-assignment otherwise using std::move. The only time the cooky move-assignment happens implicitly is when nobody is looking when the rvalue is the unnamed temporary as it is returned from a function. With that, the above examples Just Work except for where implicit moves were happening, there you have to make the move explicit:
std::unique_ptr<T> create();

create(); // OK, as above.
T* it = create().release(); // Safe, as above.
unique_ptr<T> it = create(); // Safe -- move-copy from rvalue reference.

std::unique_ptr<T> produce();
void consume(std::unique_ptr<T> x);

std::unique_ptr<T> it = produce();
consume(it); // Very safe -- no funny business -- will not compile!
consume(std::move(it)); // Safe -- We say what we mean and won't get a leak.

T* baz = new T();

consume(baz); // Safe -- Won't compile -- you'll be 
              // told it wants a unique_ptr by value.

consume(std::move(std::unique_ptr<T>(baz))); // Safe -- you will 
           // still have baz after this, but you should 
           // know better than to use it.

void looks_innocent_0(const std::unique_ptr<T>& x);
void looks_innocent_1(const std::unique_ptr<T> x);

std::unique_ptr<T> it = produce();
looks_innocent_0(it); // Safe
looks_innocent_1(it); // Safe -- won't compile
consume(std::move(it)); // Safe!

So far I'm a fan. See also: Who'se the smartest of 'em all? Get to know std::unique_ptr.

Update Comments locked due to spam.