7

What are some of C++11 std::unique_ptr uses and gotchas?

Can I use std::unique_ptr also to store dynamically allocated arrays?

Can I use std::unique_ptr also with resources using a custom deletion mechanism?

Mr.C64
  • 41,637
  • 14
  • 86
  • 162

1 Answers1

17

Let's organize some uses and gotchas using a Q&A format.


Q1: I'd like to store a pointer to a class Component inside my class X.
I don't want the "container" class X to be copyable; X is the only owner of Component instances.
I know that owning raw pointers are a bad thing and potential sources of "leaktrocities" (instead observing raw pointers are fine). What smart pointer could I use for this purpose?

A1: C++11's std::unique_ptr is certainly a good option.

It's fine in cases of unique (non-shared) ownership, and doesn't have the overhead of std::shared_ptr.
It's an excellent replacement for previous C++98/03 boost::scoped_ptr.
In fact, in addition, std::unique_ptr provides move semantics.
So, if class X contains unique_ptr<Component> data members (and other movable data members), the whole class X will be automatically movable.

An example use is showed below:

#include <memory> // for std::unique_ptr

class X
{
    std::unique_ptr<Component> m_pComponent;
    ....

public:
    X()
        : m_pComponent( new Component() )
    {
        ....
    }
}

(Of course, being a smart pointer, there's no need to explicitly delete it in the containing class destructor.)


Q2: That's great! No need for explicit destructor, no std::shared_ptr overhead (typical C++ philosophy: "We don't pay for things we don't use"), move semantics machinery already implemented!
However, I have a problem: my class Component has a constructor overload which takes some parameter that I need to calculate in constructor code before creating Component instances. I tried using ordinary operator= assigment in constructor to assign the newly created Component to the unique_ptr, but I got an error message:

X::X()
{
    ....

    const int param = CalculateCoolParameter();

    // This assignment fails:
    m_pComponent = new Component(param); // <---- Error pointing to '=' here
                 ^--- error
}

A2: OK, probably you had expected an operator= overload releasing the previous owned pointer (if any) and assigning to the newly created one.
Unfortunately, there isn't such an overload.
However, the std::unique_ptr::reset() method will do!

m_pComponent.reset( new Component(param) );

Q3: Hey! This unique_ptr is really cool! I like the fact that it's smart, it's automatically movable, and doesn't bring overhead.
So, I'd like to use it to store a dynamically allocated array of some constant size (calculated at run-time) instead of using std::vector (in this portion of code I'm highly constrained and I don't want to pay for the std:vector overhead, since I don't want all the std::vector dynamically resizing features, deep-copy, etc.).

I tried something like this:

const size_t count = GetComponentsCount();
unique_ptr<Component> components( new Component[count] );

It compiles fine, but I noted that ~Component destructor is called only once, instead I expected count destructor calls! What's going wrong here?

A3: The problem is that, with the above syntax, std::unique_ptr uses delete to release the allocated objects. But since those were allocated using new[], the proper cleanup call is delete[] (not the simple delete without brackets).

To fix that and to instruct unique_ptr to properly use delete[] for releasing resources, the following syntax must be used:

unique_ptr<Component[]> components( new Components[count] ); 
//                  ^^
//
// Note brackets "[]" after the first occurrence of "Component" 
// in unique_ptr template argument.
//

Q4: That's great! But can I use unique_ptr also in cases in which the resource release code is not performed using ordinary C++ delete (or delete[]), but instead using some custom cleanup function, like fclose() for C <stdio.h> files (opened with fopen()), or CloseHandle() for Win32 file HANDLEs (created using CreateFile())?

A4: That's definitely possible: you can specify a custom deleter for std::unique_ptr.

e.g.:

// 
// Custom deleter function for FILE*: fclose().
//
std::unique_ptr<FILE,          // <-- the wrapped raw pointer type: FILE*
                int(*)(FILE*)> // <-- the custom deleter type: fclose() prototype
myFile( fopen("myfile", "rb"), // <-- resource (FILE*) is returned by fopen()
        fclose );              // <-- the deleter function: fclose()



//
// Custom deleter functor for Win32 HANDLE: calls CloseHandle().
//
struct CloseHandleDeleter
{
    // The following pointer typedef is required, since
    // the raw resource is HANDLE (not HANDLE*).
    typedef HANDLE pointer;

    // Custom deleter: calls CloseHandle().
    void operator()(HANDLE handle) const
    {
        CloseHandle(handle);
    }
};

std::unique_ptr<HANDLE, CloseHandleDeleter> myFile( CreateFile(....) );  
Mr.C64
  • 41,637
  • 14
  • 86
  • 162
  • FWIW, what do you pay for the vector features you don't use? (hint: nothing) – R. Martinho Fernandes Jul 30 '13 at 10:55
  • At least from a memory footprint, `std::vector` can use 3 pointers, instead `unique_ptr` just one. – Mr.C64 Jul 30 '13 at 10:56
  • A2: a nicer solution, if possible, is to have a method doing the caluclation and returning the std::unique_ptr , then use that right in the initializer list. – stijn Jul 30 '13 at 10:57
  • @Mr.C64 But `unique_ptr` is not usable by itself (note how you need to carry around the size on your own). – R. Martinho Fernandes Jul 30 '13 at 10:58
  • @R.MartinhoFernandes: You can have _N_ arrays of the same size, and just store size once and having _N_ `unique_ptr`. – Mr.C64 Jul 30 '13 at 11:00
  • 3
    I'm still not sold :( I can't envision a scenario where having a couple extra pointers would not be fine, but allocating all those arrays would. – R. Martinho Fernandes Jul 30 '13 at 11:01
  • 2
    If you have a matrix 10,000x10,000 with each element being a dynamically allocated array, each `vector` has 8 bytes overhead (2 additional pointers if compared to `unique_ptr`), so total overhead is 800,000,000 bytes, i.e. circa 760MB. – Mr.C64 Jul 30 '13 at 22:02