C++11′s take on retain cycles

I’d like to take a brief moment to discuss something rather programmy today.  Namely, the topic of retain cycles.  If you’ve ever programmed in Objective-C before (since the addition of ARC) or Swift, this concept will be very familiar to you.  But if you’re coming from other programming languages such as the older (pre-11) C++, This concept will be new and you might want to read on.

Now before I get started, let me say that this example may not be the most correct way to handle delegation via lambda (block) functions in C++11.  Many will tell you that shared ownership of objects in C++ can get you into trouble, and this is certainly a case of how that can happen.  However it’s the most conceptually compatible with other languages, which I value over “c++ correctness” due to the fact that I’m porting code from another language to start with, and shared ownership allows me to get work done without reasoning around the lifespan of my objects.

Now as many newcomers to C++11 know, concepts such as smart pointers and lambda callback delegation can really save you alot of time when developing new systems.  However, there’s ahidden evil in using these two concepts together, that must be uncovered.

Example 1 :: All is well

Consider this example:

and its output:

What we’ve essentially done here is created an object (via shared pointer), set a callback lambda, and issued a method call which at some point calls our callback.  We see that everything is working as desired.  Most importantly, we see that our object is properly deleted when we are finished our program (when the shared pointer goes out of scope).

Example 2 :: Not so much

Now lets look what happens when I add a slightly more complicated, second case.

Obj2 essentially is the same as obj1 right?  All we do is add a little printout of obj2′s someProperty value from within the lambda… and of course, C++11 dictates that we must explicitly declare all external objects that we access within our lambda (a capture list).

So what is the output then?

Note the alarming lack of “object freed” being displayed for obj2.  That’s right, the above code memory-leaks obj2.  You see, obj2 owns someCallback, and someCallback owns obj2 via a capture. This is a retain cycle and it permanently causes obj2 to be “forever retained” and therefore leaks.  Once a retain cycle is made, you can consider things to be more-or-less, too late.

This type of bug is a dangerous silent killer of the night.  Why?  Because it can be so easy to miss when developing.  Objective-C and swift have evolved over time to add compiler and IDE warnings about this kind of “retain cycle” problem.  C++11 assumes that you just wouldn’t write this type of code.  But when porting code over from Objective-C, this kind of code can pop up ALL the time.

So what do we do?

Solution 1 :: The ObjC way

Well Objective-C handles this problem by introducing the concept of “weak pointers”.  These are special pointers that ensure that they do not own their reference, they only keep track of whether or not it is allocated (if any shared pointers to it exist).  Luckily, C++11 brings the weak pointer to us as well.  A solution that uses them looks like so:

You’ll note now that running this code will solve the leak.  It’s also important to note that, while this is an ugly solution, this is/was the only way to solve a retain cycle of this nature in Objective-C.

However, for this explicitly unique case, in C++, there’s another way.

Solution 2 :: The tempting C++ way

What if I told you there was a way to solve the original problem by adding one character to the original code?  Here it is:

See that little ‘&’ added before obj2 in the capture?  This allows us to capture a reference to the smart pointer and not a value.  Why does this matter?  Well when a smart_pointer value is copied, it essentially increments its retain count of the object in question, and when it finally is destroyed by going out of scope, it’ll decrement the retain count, restoring the natural balance to the universe.  However, as I mentioned before, a retain cycle prevents the retain count from ever reaching zero in this case.  A reference to obj2′s smart pointer value allows us to forego the copy-increment mechanism and essentially stop obj2 from being over-retained.  Its essentially like taking a pointer to a pointer….

However, this solution can be dangerous, as it assumes the coder understands the rammifcations of doing things like capturing a reference to a stack variable, an argument, or anything that may be “dead” by the time the callback is ran.  Almost always, this “solution” may give unintended consequences.

The “correct” way

Here’s where things get somewhat opinionated. The “correct” way to handle this really depends on the situation at hand. I suggest that if you a working with a team, you might want to use the safer and more verbose weak pointer method since it is more explicit in its intentions and helps explain why strong ownership could be a problem.  Furthermore, if you are working in a smaller system where shared pointership is not necessary, you might want to avoid using shared pointers altogether (assuming you fully understand the ownership semantics of the objects being used in the captures).

Either way, the golden rule is to understand the side effects of the code you are writing and when using shared pointers, keep that little retain cycle demon in the back of your mind… always

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>