When is std::weak_ptr useful?

Viewed 176625

I started studying smart pointers of C++11 and I don't see any useful use of std::weak_ptr. Can someone tell me when std::weak_ptr is useful/necessary?

15 Answers

shared_ptr : holds the real object.

weak_ptr : uses lock to connect to the real owner or returns a NULL shared_ptr otherwise.

weak ptr

Roughly speaking, weak_ptr role is similar to the role of housing agency. Without agents, to get a house on rent we may have to check random houses in the city. The agents make sure that we visit only those houses which are still accessible and available for rent.

Apart from the other already mentioned valid use cases std::weak_ptr is an awesome tool in a multithreaded environment, because

  • It doesn't own the object and so can't hinder deletion in a different thread
  • std::shared_ptr in conjunction with std::weak_ptr is safe against dangling pointers - in opposite to std::unique_ptr in conjunction with raw pointers
  • std::weak_ptr::lock() is an atomic operation (see also About thread-safety of weak_ptr)

Consider a task to load all images of a directory (~10.000) simultaneously into memory (e.g. as a thumbnail cache). Obviously the best way to do this is a control thread, which handles and manages the images, and multiple worker threads, which load the images. Now this is an easy task. Here's a very simplified implementation (join() etc is omitted, the threads would have to be handled differently in a real implementation etc)

// a simplified class to hold the thumbnail and data
struct ImageData {
  std::string path;
  std::unique_ptr<YourFavoriteImageLibData> image;
};

// a simplified reader fn
void read( std::vector<std::shared_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageData : imagesToLoad )
     imageData->image = YourFavoriteImageLib::load( imageData->path );
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::shared_ptr<ImageData>>> splitDatas = 
        splitImageDatas( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

But it becomes much more complicated, if you want to interrupt the loading of the images, e.g. because the user has chosen a different directory. Or even if you want to destroy the manager.

You'd need thread communication and have to stop all loader threads, before you may change your m_imageDatas field. Otherwise the loaders would carry on loading until all images are done - even if they are already obsolete. In the simplified example, that wouldn't be too hard, but in a real environment things can be much more complicated.

The threads would probably be part of a thread pool used by multiple managers, of which some are being stopped, and some aren't etc. The simple parameter imagesToLoad would be a locked queue, into which those managers push their image requests from different control threads with the readers popping the requests - in an arbitrary order - at the other end. And so the communication becomes difficult, slow and error-prone. A very elegant way to avoid any additional communication in such cases is to use std::shared_ptr in conjunction with std::weak_ptr.

// a simplified reader fn
void read( std::vector<std::weak_ptr<ImageData>> imagesToLoad ) {
   for( auto& imageDataWeak : imagesToLoad ) {
     std::shared_ptr<ImageData> imageData = imageDataWeak.lock();
     if( !imageData )
        continue;
     imageData->image = YourFavoriteImageLib::load( imageData->path );
   }
}

// a simplified manager
class Manager {
   std::vector<std::shared_ptr<ImageData>> m_imageDatas;
   std::vector<std::unique_ptr<std::thread>> m_threads;
public:
   void load( const std::string& folderPath ) {
      std::vector<std::string> imagePaths = readFolder( folderPath );
      m_imageDatas = createImageDatas( imagePaths );
      const unsigned numThreads = std::thread::hardware_concurrency();
      std::vector<std::vector<std::weak_ptr<ImageData>>> splitDatas = 
        splitImageDatasToWeak( m_imageDatas, numThreads );
      for( auto& dataRangeToLoad : splitDatas )
        m_threads.push_back( std::make_unique<std::thread>(read, dataRangeToLoad) );
   }
};

This implementation is nearly as easy as the first one, doesn't need any additional thread communication, and could be part of a thread pool/queue in a real implementation. Since the expired images are skipped, and non-expired images are processed, the threads never would have to be stopped during normal operation. You could always safely change the path or destroy your managers, since the reader fn checks, if the owning pointer isn't expired.

I see std::weak_ptr<T> as a handle to a std::shared_ptr<T>: It allows me to get the std::shared_ptr<T> if it still exists, but it will not extend its lifetime. There are several scenarios when such point of view is useful:

// Some sort of image; very expensive to create.
std::shared_ptr< Texture > texture;

// A Widget should be able to quickly get a handle to a Texture. On the
// other hand, I don't want to keep Textures around just because a widget
// may need it.

struct Widget {
    std::weak_ptr< Texture > texture_handle;
    void render() {
        if (auto texture = texture_handle.get(); texture) {
            // do stuff with texture. Warning: `texture`
            // is now extending the lifetime because it
            // is a std::shared_ptr< Texture >.
        } else {
            // gracefully degrade; there's no texture.
        }
    }
};

Another important scenario is to break cycles in data structures.

// Asking for trouble because a node owns the next node, and the next node owns
// the previous node: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > next;
    std::shared_ptr< Node > prev;
};

// Asking for trouble because a parent owns its children and children own their
// parents: memory leak; no destructors automatically called.
struct Node {
    std::shared_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::shared_ptr< Node > next;
    std::weak_ptr< Node > prev;
};

// Better: break dependencies using a std::weak_ptr (but not best way to do it;
// see Herb Sutter's talk).
struct Node {
    std::weak_ptr< Node > parent;
    std::shared_ptr< Node > left_child;
    std::shared_ptr< Node > right_child;
};

Herb Sutter has an excellent talk that explains the best use of language features (in this case smart pointers) to ensure Leak Freedom by Default (meaning: everything clicks in place by construction; you can hardly screw it up). It is a must watch.

I see a lot of interesting answers that explain reference counting etc., but I am missing a simple example that demonstrates how you prevent memory leak using weak_ptr. In first example I use shared_ptr in cyclically referenced classes. When the classes go out of scope they are NOT destroyed.

#include<iostream>
#include<memory>
using namespace std;

class B;

class A
{
public:
    shared_ptr<B>bptr;
    A() {
        cout << "A created" << endl;
    }
    ~A() {
        cout << "A destroyed" << endl;
    }
};

class B
{
public:
    shared_ptr<A>aptr;
    B() {
        cout << "B created" << endl;
    }
    ~B() {
        cout << "B destroyed" << endl;
    }
};

int main()
{
    {
        shared_ptr<A> a = make_shared<A>();
        shared_ptr<B> b = make_shared<B>();
        a->bptr = b;
        b->aptr = a;
    }
  // put breakpoint here
}

If you run the code snippet you will see as classes are created, but not destroyed:

A created
B created

Now we change shared_ptr's to weak_ptr:

class B;
class A
{
public:
    weak_ptr<B>bptr;

    A() {
        cout << "A created" << endl;
    }
    ~A() {
        cout << "A destroyed" << endl;
    }
};

class B
{
public:
    weak_ptr<A>aptr;

    B() {
        cout << "B created" << endl;
    }
    ~B() {
        cout << "B destroyed" << endl;
    }
};

    int main()
    {
        {
            shared_ptr<A> a = make_shared<A>();
            shared_ptr<B> b = make_shared<B>();
            a->bptr = b;
            b->aptr = a;
        }
      // put breakpoint here
    }

This time, when using weak_ptr we see proper class destruction:

A created
B created
B destroyed
A destroyed

When we does not want to own the object:

Ex:

class A
{
    shared_ptr<int> sPtr1;
    weak_ptr<int> wPtr1;
}

In the above class wPtr1 does not own the resource pointed by wPtr1. If the resource is got deleted then wPtr1 is expired.

To avoid circular dependency:

shard_ptr<A> <----| shared_ptr<B> <------
    ^             |          ^          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
    |             |          |          |
class A           |     class B         |
    |             |          |          |
    |             ------------          |
    |                                   |
    -------------------------------------

Now if we make the shared_ptr of the class B and A, the use_count of the both pointer is two.

When the shared_ptr goes out od scope the count still remains 1 and hence the A and B object does not gets deleted.

class B;

class A
{
    shared_ptr<B> sP1; // use weak_ptr instead to avoid CD

public:
    A() {  cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }

    void setShared(shared_ptr<B>& p)
    {
        sP1 = p;
    }
};

class B
{
    shared_ptr<A> sP1;

public:
    B() {  cout << "B()" << endl; }
    ~B() { cout << "~B()" << endl; }

    void setShared(shared_ptr<A>& p)
    {
        sP1 = p;
    }
};

int main()
{
    shared_ptr<A> aPtr(new A);
    shared_ptr<B> bPtr(new B);

    aPtr->setShared(bPtr);
    bPtr->setShared(aPtr);

    return 0;  
}

output:

A()
B()

As we can see from the output that A and B pointer are never deleted and hence memory leak.

To avoid such issue just use weak_ptr in class A instead of shared_ptr which makes more sense.

Inspired by @offirmo's response I wrote this code and then ran the visual studio diagnostic tool:

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

using namespace std;

struct Member;
struct Team;

struct Member {
    int x = 0;

    Member(int xArg) {
        x = xArg;
    }

    shared_ptr<Team> teamPointer;
};

struct Team {
    vector<shared_ptr<Member>> members;
};

void foo() {
    auto t1 = make_shared<Team>();
    for (int i = 0; i < 1000000; i++) {
        t1->members.push_back(make_shared<Member>(i));
        t1->members.back()->teamPointer = t1;
    }
}

int main() {
    foo();

    while (1);

    return 0;
}

When the member pointer to the team is shared_ptr teamPointer the memory is not free after foo() is done, i.e. it stays at around 150 MB.

But if it's changed to weak_ptr teamPointer in the diagnostic tool you'll see a peak and then memory usage returns to about 2MB.

Related