2013-06-17

C++11 “smart” pointers need “careful” programmers

I’m taking a deeper look into the new C++11 features. One of the most useful features turned out to be shared_ptr, a smart pointer that, along with unique_ptr and weak_ptr, superseded the almost useless auto_ptr, which is now deprecated.

Informally speaking, shared_ptr wraps a pointer to an object, and has an internal counter taking track of how many copies of this shared_ptr are currently managing that object. When the counter eventually reaches 0, the object is deleted.

This can be very convenient, especially when memory management is an issue since the ownership relationships between objects are not clearly evident — and it’s hard to find out when and how an object should be deleted.

Still, there is a subtle issue that one should take care of when using shared_ptr. I bet it is quite obvious for most experienced C++ programmers, but I think everyone can do a mistake, and this post may actually save you from a big headache, sometime in the future.

Let’s introduce the issue with a simple example.

Take a dummy struct like this one:

struct foo {
    int x;
    foo(int x) : x(x) {
    }
};

Imagine that, for some reason, you want to manage the instances of foo using shared_ptr.
So, for example, you have three instances of foo in this way:

std::shared_ptr<foo> f0 = std::make_shared<foo>(0);
std::shared_ptr<foo> f1 = std::make_shared<foo>(1);
std::shared_ptr<foo> f2 = std::make_shared<foo>(2);

Now suppose that you have to call a function, bar1, that returns an element from a vector of foo* (not shared_ptrs!) and you cannot change it.

foo* bar1(const std::vector<foo*>& v) {
    // Just an example: imagine a more complex algorithm to extract
    // an element from the vector
    return v[0];
}

Therefore, you have to unbox all the shared_ptrs to get raw pointers to foo and pass them to bar1:

std::vector<foo*> v = { f0.get(), f1.get(), f2.get() };
foo* f = bar1(v);

So far so good. Now suppose you have another function, bar2, that processes a shared_ptr<foo> and no, you can’t change its signature to make it accept raw pointers.

void bar2(std::shared_ptr<foo> f) {
    // Just an example
    f->x++;
}

The first, obvious idea is to “smartly” wrap f with a brand new shared_ptr.

std::shared_ptr<foo> f_ptr(f);
bar2(f_ptr);

So simple, so disastrous. When both the shared_ptr instances you created to manage that particular foo instance (namely f0 and f_ptr) will be destroyed, you will get a double free() on the managed object.

Here is the full code. It compiles with gcc 4.7 with the -std=c++11 command line switch. It crashes miserably.

#include <memory>
#include <vector>
#include <iostream>

struct foo {
    int x;
    foo(int x) : x(x) {
    }
};

foo* bar1(const std::vector<foo*>& v);
void bar2(std::shared_ptr<foo> f);

int main(void) {

    // The foo objects are managed with shared_ptr
    std::shared_ptr<foo> f0 = std::make_shared<foo>(0);
    std::shared_ptr<foo> f1 = std::make_shared<foo>(1);
    std::shared_ptr<foo> f2 = std::make_shared<foo>(2);

    // Call bar, that returns an element from a vector of foo*
    // (not shared_ptr's!).
    // We have to unbox all the shared_ptr's to get raw pointers
    std::vector<foo*> v = { f0.get(), f1.get(), f2.get() };
    foo* f = bar1(v);

    // Now we have f. Let's give it to bar2 that, unfortunately,
    // accepts only a shared_ptr.
    // We "smartly" wrap it with a BRAND NEW shared_ptr.
    std::shared_ptr<foo> f_ptr(f);
    bar2(f_ptr);

    // Crash for free() of an invalid pointer!

    return 0;

}

// This function accepts only a vector of foo* (and you can't change it)
foo* bar1(const std::vector<foo*>& v) {
    // Just an example: imagine a more complex algorithm to
    // extract an element from the vector
    return v[0];
}

void bar2(std::shared_ptr<foo> f) {
    // Just an example
    f->x++;
}

If you are thinking “no wonder at all...” then there’s nothing more to say. If you are a bit confused or surprised, then keep on reading!

Have a look at the definition of shared_ptr you find on cppreference.com:

std::shared_ptr is a smart pointer that retains shared ownership of an object through a pointer. Several shared_ptr objects may own the same object. The object is destroyed and its memory deallocated when either of the following happens:
  • the last remaining shared_ptr owning the object is destroyed.
  • the last remaining shared_ptr owning the object is assigned another pointer via operator=() or reset().

Now let’s get back to the second paragraph of this post:

Informally speaking, shared_ptr wraps a pointer to an object, and has an internal counter taking track of how many copies of this shared_ptr are currently managing that object. When the counter eventually reaches 0, the object is deleted.

IMHO, my pseudo-definition is quite informal and imprecise but provides a good insight on how this class actually works. Independently created shared_ptr instances managing the same object don’t share any information, and know nothing about each other. They have independent counters, that will all reach 0, causing multiple free()s on that object.

We are free to create as many shared_ptrs managing the same object as we need, provided we instantiate them by copying already existing shared_ptrs to that object — excluding their “common ancestor” which has to be created with make_shared or by directly calling the shared_ptr constructor.

Now, how can we solve a situation like the one of the example above, supposing we cannot change neither the definition of foo, nor the signatures of bar1 and bar2? The following piece of code is the only hack that comes up to my mind (feel free to improve it).

#include <memory>
#include <vector>
#include <iostream>

// We cannot change the definition of foo
struct foo {
    int x;
    foo(int x) : x(x) {
    }
};

// We cannot change the signatures of the functions
foo* bar1(const std::vector<foo*>& v);
void bar2(std::shared_ptr<foo> f);

// We define a foo_wrapper inheriting from foo and from a template
// class of the standard library named enable_shared_from_this
struct foo_wrapper : foo, std::enable_shared_from_this<foo_wrapper> {
    foo_wrapper(int x) : foo(x) {
    };
};

int main(void) {

    // Instead of instantiating foo's, we instantiate foo_wrapper's.
    std::shared_ptr<foo_wrapper> f0 = std::make_shared<foo_wrapper>(0);
    std::shared_ptr<foo_wrapper> f1 = std::make_shared<foo_wrapper>(1);
    std::shared_ptr<foo_wrapper> f2 = std::make_shared<foo_wrapper>(2);

    // We have to unbox all the shared_ptr's to have plain foo_wrapper*'s
    // (which behave as foo* thanks to pointers covariance)
    std::vector<foo*> v = { f0.get(), f1.get(), f2.get() };

    // We downcast foo* to foo_wrapper* (we know it's safe)
    foo_wrapper* f = (foo_wrapper*) bar1(v);

    // We use the enable_shared_from_this::shared_from_this() method
    // to grab a shared_ptr managing this foo_wrapper that "shares"
    // its internal counter with the pre-existing shared_ptr's managing
    // the same object
    std::shared_ptr<foo_wrapper> f_ptr = f->shared_from_this();

    // Even if bar2 accepts a shared_ptr<foo>, we can pass a
    // shared_ptr<foo_wrapper> to it: the shared_ptr<T>
    // "smart" pointer class is designed so that it can mimic
    // pointers covariance.
    bar2(f_ptr);

    std::cout << f->x << std::endl; // prints 1

    return 0;

}

foo* bar1(const std::vector<foo*>& v) {
    // Just an example: imagine a more complex algorithm to
    // extract an element from the vector
    return v[0];
}

void bar2(std::shared_ptr<foo> f) {
    // Just an example
    f->x++;
}

I suggest you to read the documentation of std::enable_shared_from_this to better understand how it works. The key idea is the following:

enable_shared_from_this provides an alternative to an expression like std::shared_ptr<T>(this), which is likely to result in this being destructed more than once by multiple owners that are unaware of each other.

The obvious point of this post is that, even if smart pointers are there to make our life easier, everytime we lose some control over what’s actually happening we take the risk of complicate things even further.
A good idea is to plan for the use of smart pointers in advance, when design decisions can be made without screwing anything up.

Feel free to post a comment to pinpoint something I didn’t mention, or just to share your thoughts. I’d appreciate your feedback.

Tags: cpp, cpp11
comments powered by Disqus
Fork me on GitHub