📜 ⬆️ ⬇️

Five pitfalls when using shared_ptr

The shared_ptr class is a handy tool that can solve a lot of developer problems. However, in order not to make mistakes, you need to know his device perfectly. I hope my article will be useful to those who are just starting to work with this tool.

I will talk about the following:


The described problems take place for both boost :: shared_ptr and std :: shared_ptr. At the end of the article you will find an application with full texts of programs written to demonstrate the described features (using the boost library as an example).

Cross reference


This problem is the most known and is related to the fact that the pointer shared_ptr is based on reference counting. For an instance of an object that shared_ptr owns, a counter is created. This counter is common to all shared_ptr pointing to this object.
')


When constructing a new object, an object with a counter is created and the value 1 is placed in it. When copying, the counter is incremented by 1. When the destructor is called (or when the pointer is replaced by assignment or reset), the counter is decreased by 1.

Consider an example:
struct Widget { shared_ptr<Widget> otherWidget; }; void foo() { shared_ptr<Widget> a(new Widget); shared_ptr<Widget> b(new Widget); a->otherWidget = b; //         = 2 b->otherWidget = a; //         = 2 } 

What happens when objects a and b leave the domain of definition? In the destructor, references to objects will decrease. Each object will have a counter = 1 (after all, a still points to b, and b to a). Objects “hold” each other and our application does not have access to them - these objects are “lost”.
To solve this problem, there is a weak_ptr. One of the typical cases of creating cross-references is when one object owns a collection of other objects.

 struct RootWidget { list<shared_ptr<class Widget> > widgets; }; struct Widget { shared_ptr<class RootWidget> parent; }; 

With this device, each Widget will prevent the removal of the RootWidget and vice versa.



In this case, you need to answer the question: “Who owns whom?”. Obviously, it is RootWidget that owns Widget objects in this case, and not vice versa. Therefore, you need to modify the example as follows:

 struct Widget { weak_ptr<class RootWidget> parent; }; 

Weak links do not prevent the object from being deleted. They can be converted to strong in two ways:

1) shared_ptr constructor
 weak_ptr<Widget> w = …; //  ,    ,   shared_ptr    shared_ptr<Widget> p( w ); 

2) Lock method
 weak_ptr<Widget> w = …; //  ,    ,  p    if( shared_ptr<Widget> p = w.lock() ) { //     –     } 

Conclusion:
In the case of ring references in the code, use weak_ptr to solve problems.

Unnamed pointers


The problem of nameless pointers relates to the question of “sequence points” (sequence points)
 // shared_ptr,     foo -  foo( shared_ptr<Widget>(new Widget), bar() ); // shared_ptr,     foo   p shared_ptr<Widget> p(new Widget); foo( p, bar() ); 

Of these two options, the documentation recommends that you always use the second — give names to the pointers. Consider an example where the bar function is defined like this:
 int bar() { throw std::runtime_error(“Exception from bar()”); } 

The fact is that in the first case, the design order is not defined. It all depends on the specific compiler and compilation flags. For example, this might happen like this:
  1. new widget
  2. function call bar
  3. constructing shared_ptr
  4. function call foo

Surely, you can only be sure that the call to foo will be the last action, and the shared_ptr will be constructed after creating the object (new Widget). However, there are no guarantees that it will be constructed immediately after the creation of the object.

If an exception is generated during the second step (and in our example it will be generated), then Widget will be considered constructed, but shared_ptr will not own it yet. As a result, the link to this object will be lost. I checked this example on gcc 4.7.2. The order of the call was such that shared_ptr + new, regardless of the compilation options, were not separated by a call to bar. But relying on this behavior is not worth it - it is not guaranteed. I would be grateful if the compiler prompts me, its version and compilation options, for which such code will lead to an error.

Another way to bypass the anonymous problem shared_ptr is to use the functions make_shared or allocate_shared. For our example, it would look like this:

 foo( make_shared<Widget>(), bar() ); 

This example looks even more succinctly than the original one, and also has a number of advantages in terms of memory allocation (efficiency issues will be left outside the article). Let's call make_shared with any number of arguments. For example, the following code will return shared_ptr on a string created through a constructor with one parameter.

 make_shared<string>("shared string"); 

Conclusion:
Let's share the names of shared_ptr even if the code from this is less concise, or use to create objects
the make_shared and allocate_shared functions.

The problem of using in different threads


Reference counting in shared_ptr is built using an atomic counter. We safely use pointers to the same object from different streams. In any case, we are not accustomed to worrying about reference counting (the thread safety of the object itself is another problem).

Suppose we have a global shared_ptr:
 shared_ptr<Widget> globalSharedPtr(new Widget); void read() { shared_ptr<Widget> x = globalSharedPtr; //  -  Widget } 

Run the call to read from different threads and you will see that there are no problems in the code (as long as you are performing thread-safe operations for this class on Widget).

Suppose there is another function:
 void write() { globalSharedPtr.reset( new Widget ); } 

The shared_ptr device is quite difficult, so I’ll provide a code that will schematically help show the problem. Of course, the real code looks different.
 shared_ptr::shared_ptr(const shared_ptr<T>& x) { A1: pointer = x.pointer; A2: counter = x.counter; A3: atomic_increment( *counter ); } shared_ptr<T>::reset(T* newObject) { B1: if( atomic_decrement( *counter ) == 0 ) { B2: delete pointer; B3: delete counter; B4: } B5: pointer = newObject; B6: counter = new Counter; } 

Suppose the first thread began to copy globalSharedPtr (read), and the second thread calls reset for the same pointer instance (write). The result may be the following:
  1. Stream1 has just executed line A2, but has not yet moved to line A3 (atomic increment).
  2. Flow2 at this time reduced the counter on line B1, saw that after decreasing, the counter became zero and executed lines B2 and B3.
  3. Stream1 reaches line A3 and attempts to atomically increment a counter that is no longer there.

And it may be that stream1 on line A2 will have time to increase the counter before stream2 causes the objects to be deleted, but after stream2 has made the counter decrease. Then we get a new shared_ptr pointing to the remote counter and object.

You can write similar code:
 shared_ptr<Widget> globalSharedPtr(new Widget); mutex_t globalSharedPtrMutex; void resetGlobal(Widget* x) { write_lock_t l(globalSharedPtrMutex); globalSharedPtr.reset( x ); } shared_ptr<Widget> getGlobal() { read_lock_t l(globalSharedPtrMutex); return globalSharedPtr; } void read() { shared_ptr<Widget> x = getGlobal(); //    x    } void write() { resetGlobal( new Widget ); } 

Now, using such functions, you can safely work with this shared_ptr.

Conclusion: if some instance of shared_ptr is available to different threads and can be modified, then you need to take care of synchronization of access to this instance of shared_ptr.

Singularities of the time of the destruction of the liberating functor for shared_ptr


This problem can occur only if you use your own exempting functor in combination with weak pointers (weak_ptr). For example, you can create shared_ptr based on another shared_ptr by adding a new action before deleting (in fact, the “Decorator” template). So you could get a pointer to work with the database, removing it from the pool of connections, and at the end of the client’s work with the pointer, bring it back to the pool.
 typedef shared_ptr<Connection> ptr_t; class ConnectionReleaser { list<ptr_t>& whereToReturn; ptr_t connectionToRelease; public: ConnectionReleaser(list<ptr_t>& lst, const ptr_t& x):whereToReturn(lst), connectionToRelease(x) {} void operator()(Connection*) { whereToReturn.push_back( connectionToRelease ); //      connectionToRelease.reset(); } }; ptr_t getConnection() { ptr_t c( connectionList.back() ); connectionList.pop_back(); ptr_t r( c.get(), ConnectionReleaser( connectionList, c ) ); return r; } 



The problem is that the object passed as a liberating functor for shared_ptr will be destroyed only when all references to the object are destroyed - both strong (shared_ptr) and weak (weak_ptr). Thus, if ConnectionReleaser does not take care of “releasing” the pointer passed to it (connectionToRelease), it will hold a strong link as long as at least one weak_ptr from shared_ptr created by the getConnection function exists. This can lead to a rather unpleasant and unexpected behavior of your application.

It is also possible that you use bind to create a liberating functor. For example:
 void releaseConnection(std::list<ptr_t>& whereToReturn, ptr_t& connectionToRelease) { whereToReturn.push_back( connectionToRelease ); //      connectionToRelease.reset(); } ptr_t getConnection() { ptr_t c( connectionList.back() ); connectionList.pop_back(); ptr_t r( c.get(), boost::bind(&releaseConnection, boost::ref(connectionList), c) ); return r; } 


Remember that bind copies the arguments passed to it (except when using boost :: ref), and if there is shared_ptr among them, then it should also be cleared to avoid the problem already described.

Conclusion: Perform in the release function all actions that must be performed when the last strong link is broken. Drop all shared_ptr that are for some reason members of your functor. If you use bind, do not forget that it copies the arguments passed to it.

Features of working with the enable_shared_from_this template


Sometimes you need to get shared_ptr from the methods of the object itself. Attempting to create a new shared_ptr from this will lead to undefined behavior (most likely to crash the program), unlike intrusive_ptr, for which this is common practice. To solve this problem, a mix-up class of enable_shared_from_this was invented.

The enable_shared_from_this template is arranged as follows: inside the class there is a weak_ptr, which in the construction of shared_ptr contains a link to this most shared_ptr. When you call the shared_from_this method of an object, the weak_ptr is converted to shared_ptr through the constructor. Schematically, the template looks like this:
 template<class T> class enable_shared_from_this { weak_ptr<T> weak_this_; public: shared_ptr<T> shared_from_this() { //        shared_ptr shared_ptr<T> p( weak_this_ ); return p; } }; class Widget: public enable_shared_from_this<Widget> {}; 

The shared_ptr constructor for this case looks like this:
 shared_ptr::shared_ptr(T* object) { pointer = object; counter = new Counter; object->weak_this_ = *this; } 

It is important to understand that when constructing the weak_this_ object, it still does not indicate anything. The correct link in it will appear only after the constructed object is passed to the constructor shared_ptr. Any attempt to call shared_from_this from the constructor will result in a bad_weak_ptr exception.
 struct BadWidget: public enable_shared_from_this<BadWidget> { BadWidget() { //   shared_from_this()   bad_weak_ptr cout << shared_from_this() << endl; } }; 

The same consequences will result from an attempt to access the shared_from_this from the destructor, but for a different reason: at the time of the destruction of the object, it is already considered that it does not indicate any strong references (the counter is decremented).
 struct BadWidget: public enable_shared_from_this<BadWidget> { ~BadWidget() { //   shared_from_this()   bad_weak_ptr cout << shared_from_this() << endl; } }; 

With the second case (destructor) there is little that can be invented. The only option is to take care not to call shared_from_this and make sure that the functions that the destructor does not do.

With the first case, everything is a little easier. Surely you have already decided that the only way for the existence of your object is shared_ptr, then it would be appropriate to move the object's constructor to the private part of the class and create a static method for creating shared_ptr of the type you need. If, when initializing an object, you need to perform actions that require shared_from_this, then for this purpose you can select the logic in the init method.

 class GoodWidget: public enable_shared_from_this<GoodWidget> { void init() { cout << shared_from_this() << endl; } public: static shared_ptr<GoodWidget> create() { shared_ptr<GoodWidget> p(new GoodWidget); p->init(); return p; } }; 

Conclusion:
Avoid calling (direct or indirect) shared_from_this from constructors and destructors. In case for proper object initialization you need access to shared_from_this: create an init method, delegate object creation to a static method, and make it possible for objects to be created only using this method.

Conclusion


The article describes 5 features of using shared_ptr and provides general recommendations for avoiding potential problems.

Although shared_ptr removes many problems from the developer, knowledge of the internal structure (albeit approximately) is necessary for competent use of shared_ptr. I recommend carefully studying the shared_ptr device, as well as the classes associated with it. Compliance with a set of simple rules can save the developer from unwanted problems.

Literature




application


The appendix contains the full texts of programs to illustrate the cases described in the article.

Demonstration of the ring link problem
 #include <string> #include <iostream> #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> class BadWidget { std::string name; boost::shared_ptr<BadWidget> otherWidget; public: BadWidget(const std::string& n):name(n) { std::cout << "BadWidget " << name << std::endl; } ~BadWidget() { std::cout << "~BadWidget " << name << std::endl; } void setOther(const boost::shared_ptr<BadWidget>& x) { otherWidget = x; std::cout << name << " now points to " << x->name << std::endl; } }; class GoodWidget { std::string name; boost::weak_ptr<GoodWidget> otherWidget; public: GoodWidget(const std::string& n):name(n) { std::cout << "GoodWidget " << name << std::endl; } ~GoodWidget() { std::cout << "~GoodWidget " << name << std::endl; } void setOther(const boost::shared_ptr<GoodWidget>& x) { otherWidget = x; std::cout << name << " now points to " << x->name << std::endl; } }; int main() { { //       std::cout << "====== Example 3" << std::endl; boost::shared_ptr<BadWidget> w1(new BadWidget("3_First")); boost::shared_ptr<BadWidget> w2(new BadWidget("3_Second")); w1->setOther( w2 ); w2->setOther( w1 ); } { //      weak_ptr      std::cout << "====== Example 3" << std::endl; boost::shared_ptr<GoodWidget> w1(new GoodWidget("4_First")); boost::shared_ptr<GoodWidget> w2(new GoodWidget("4_Second")); w1->setOther( w2 ); w2->setOther( w1 ); } return 0; } 

Demonstrate the conversion of weak_ptr to shared_ptr
 #include <iostream> #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> class Widget {}; int main() { boost::weak_ptr<Widget> w; //    weak_ptr      //  lock    std::cout << __LINE__ << ": " << w.lock().get() << std::endl; //  shared_ptr       try { boost::shared_ptr<Widget> tmp ( w ); } catch (const boost::bad_weak_ptr&) { std::cout << __LINE__ << ": bad_weak_ptr" << std::endl; } boost::shared_ptr<Widget> p(new Widget); //   weak_ptr   w = p; //  lock    std::cout << __LINE__ << ": " << w.lock().get() << std::endl; //  shared_ptr       .    std::cout << __LINE__ << ": " << boost::shared_ptr<Widget>( w ).get() << std::endl; //   p.reset(); //    .  weak_ptr    //  lock     std::cout << __LINE__ << ": " << w.lock().get() << std::endl; //  shared_ptr        try { boost::shared_ptr<Widget> tmp ( w ); } catch (const boost::bad_weak_ptr&) { std::cout << __LINE__ << ": bad_weak_ptr" << std::endl; } return 0; } 

Demonstration of multithreading problems for shared_ptr
 #include <iostream> #include <boost/thread.hpp> #include <boost/shared_ptr.hpp> typedef boost::shared_mutex mutex_t; typedef boost::unique_lock<mutex_t> read_lock_t; typedef boost::shared_lock<mutex_t> write_lock_t; mutex_t globalMutex; boost::shared_ptr<int> globalPtr(new int(0)); const int readThreads = 10; const int maxOperations = 10000; boost::shared_ptr<int> getPtr() { //   ,     read_lock_t l(globalMutex); return globalPtr; } void resetPtr(const boost::shared_ptr<int>& x) { //   ,     write_lock_t l(globalMutex); globalPtr = x; } void myRead() { for(int i = 0; i < maxOperations; ++i) { boost::shared_ptr<int> p = getPtr(); } } void myWrite() { for(int i = 0; i < maxOperations; ++i) { resetPtr( boost::shared_ptr<int>( new int(i)) ); } } int main() { boost::thread_group tg; tg.create_thread( &myWrite ); for(int i = 0; i < readThreads; ++i) { tg.create_thread( &myRead ); } tg.join_all(); return 0; } 

Demonstration of deleter + weak_ptr problem
 #include <string> #include <list> #include <iostream> #include <stdexcept> #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> #include <boost/bind.hpp> class Connection { std::string name; public: const std::string& getName() const { return name; } explicit Connection(const std::string& n):name(n) { std::cout << "Connection " << name << std::endl; } ~Connection() { std::cout << "~Connection " << name << std::endl; } }; typedef boost::shared_ptr<Connection> ptr_t; class ConnectionPool { std::list<ptr_t> connections; //         deleter (get1) class ConnectionReleaser { std::list<ptr_t>& whereToReturn; ptr_t connectionToRelease; public: ConnectionReleaser(std::list<ptr_t>& lst, const ptr_t& x):whereToReturn(lst), connectionToRelease(x) {} void operator()(Connection*) { whereToReturn.push_back( connectionToRelease ); std::cout << "get1: Returned connection " << connectionToRelease->getName() << " to the list" << std::endl; //  .          connectionToRelease.reset(); } }; //         deleter (get2) static void releaseConnection(std::list<ptr_t>& whereToReturn, ptr_t& connectionToRelease) { whereToReturn.push_back( connectionToRelease ); std::cout << "get2: Returned connection " << connectionToRelease->getName() << " to the list" << std::endl; //            connectionToRelease.reset(); } ptr_t popConnection() { if( connections.empty() ) throw std::runtime_error("No connections left"); ptr_t w( connections.back() ); connections.pop_back(); return w; } public: ptr_t get1() { ptr_t w = popConnection(); std::cout << "get1: Taken connection " << w->getName() << " from list" << std::endl; ptr_t r( w.get(), ConnectionReleaser( connections, w ) ); return r; } ptr_t get2() { ptr_t w = popConnection(); std::cout << "get2: Taken connection " << w->getName() << " from list" << std::endl; ptr_t r( w.get(), boost::bind(&releaseConnection, boost::ref(connections), w )); return r; } void add(const std::string& name) { connections.push_back( ptr_t(new Connection(name)) ); } ConnectionPool() { std::cout << "ConnectionPool" << std::endl; } ~ConnectionPool() { std::cout << "~ConnectionPool" << std::endl; } }; int main() { boost::weak_ptr<Connection> weak1; boost::weak_ptr<Connection> weak2; { ConnectionPool cp; cp.add("One"); cp.add("Two"); ptr_t p1 = cp.get1(); weak1 = p1; ptr_t p2 = cp.get2(); weak2 = p2; } std::cout << "Here the ConnectionPool is out of scope, but weak_ptrs are not" << std::endl; return 0; } 

Demonstrate problems with enable_shared_from_this
 #include <iostream> #include <boost/shared_ptr.hpp> #include <boost/enable_shared_from_this.hpp> class BadWidget1: public boost::enable_shared_from_this<BadWidget1> { public: BadWidget1() { std::cout << "Constructor" << std::endl; std::cout << shared_from_this() << std::endl; } }; class BadWidget2: public boost::enable_shared_from_this<BadWidget2> { public: ~BadWidget2() { std::cout << "Destructor" << std::endl; std::cout << shared_from_this() << std::endl; } }; class GoodWidget: public boost::enable_shared_from_this<GoodWidget> { GoodWidget() {} void init() { std::cout << "init()" << std::endl; std::cout << shared_from_this() << std::endl; } public: static boost::shared_ptr<GoodWidget> create() { boost::shared_ptr<GoodWidget> p(new GoodWidget); p->init(); return p; } }; int main() { boost::shared_ptr<GoodWidget> good = GoodWidget::create(); try { boost::shared_ptr<BadWidget1> bad1(new BadWidget1); } catch( const boost::bad_weak_ptr&) { std::cout << "Caught bad_weak_ptr for BadWidget1" << std::endl; } try { boost::shared_ptr<BadWidget2> bad2(new BadWidget2); //        terminate // .. ,           } catch( const boost::bad_weak_ptr&) { std::cout << "Caught bad_weak_ptr for BadWidget2" << std::endl; } return 0; } 

Source: https://habr.com/ru/post/191018/


All Articles