![]() |
![]() |
|
![]() |
![]() |
Home Contact Links Deutsch |
|
C++: Memory management with smart- and weak-pointersThe problemWithin a multithreaded environment it is difficult to guarantee, that an object exists, if you want to access it. Another problem is to guarantee, that an object is destructed, if it is not used anymore. Other programming languages such as Java provide handles to objects and a Garbage Collector (GC) to match these requirements. But this solution comes with some disadvantages. For example Java makes no predication of when an object will be destructed and even cannot guarantee, if this happens. Another problem is, that GCs cannot be used in realtime environments, because the timing of a GC-ed language isn't predictable. Within C++ you’re be used to control the time of destruction exactly for objects that lie on the stack. Unfortunately the language doesn't provide any solution for objects that are on the heap. So a technique is needed that offers this feature for all objects. Reference counting is a good candidate for this. It offers a predictive timing and so it's a good choice for realtime programs. Unfortunately it's slower and implies the problem of circular references. But there's no advantage without any disadvantage. Smart-pointersA basic approach is to build a base class for reference counted objects and to provide a so called smart-pointer class. When you create an instance of a reference counted class, you will get a smart-pointer for it that acts like a handle. You can create copies of a smart-pointer value (meaning the reference) and you can assign new values to it. It should also be allowed to transfer smart-pointer values between threads. The result of these actions are copies of the references, not copies of the referenced objects. The count of references is stored in the referenced object. If the last reference is deleted, then the reference count reaches 0. This will lead to an immediate destruction of the referenced object. But what happens, if two objects reference each other? Then the reference count will never reach 0 and the technique fails (see picture below). The solution of this problem is to cut this strong circular linkage by a weak linkage. To do so, one of the smart-pointers in a circular linkage must be replaced by a weak-pointer. Weak-pointersA weak-pointer doesn't prevent the destruction of the referenced object, if the last smart-pointer reference is deleted. Instead it only offers a way to create a smart-pointer that references the appropriate object, as long as it's available. For this aim, the weak-pointer must be able to check, if the referenced object is still available. At least there are two possible ways in implementing this. The first one is to insert a weak reference count in the referenced class additively to the smart reference count. If the smart reference count reaches 0, then a special finalize-method is to call on the object by the last referencing smart-pointer. In the finalize-method, the object has to set all its references to null and so all dependent objects would be finalized, too. This would lead to a destruction of all remaining weak pointers and as soon as the weak reference count also reaches 0, the object could be destructed. The picture below gives you a more detailed example of that. Two objects are referenced, each by a single smart pointer and the second object has a weak reference to the first. If smart-pointer 1 should be set to null or should be deleted, then smart reference count of object 1 would drop to 0. This is detected by smart-pointer 1, but it's very important, that it first calls finalize-method of object 1, before it decrements the reference count. Now in the finalize-method all the pointers of the object 1 are set to null. As a consequence the smart reference count of object 2 must be decremented by smart-pointer 2 and would reach 0, too. Smart-pointer 2 detects that (before it decrements the reference count) and calls finalize-method of object 2. In finalize-method of object 2 all the pointers are set to null - also the weak ones. As a consequence the weak reference count of object 1 must be decremented by weak-pointer 1 and would reach 0, too. Weak-pointer 1 detects that (before it decrements the reference count), but mustn't destruct object 1, because there's still a smart reference count of 1 left (you remember, that the reference count is decremented after calling finalize by the smart-pointers and we're still in a finalize-call, right?). Now the call of finalize in object 2 returns and smart-pointer 2 decrements the smart reference count of this object. It detects that not only the smart reference count reached 0, but also the weak reference count has dropped to 0. So it calls delete for object 2 which causes the destruction of object 2 and frees the memory allocated for it. Now the call of finalize in object 1 also returns and smart-pointer 1 decrements the smart reference count of object 1. It calls delete for object 1, because the weak reference count of object 1 dropped to 0. And so all the memory is given free, although we had circularly referenced objects.
This technique I already used with good results, but this solution has a drawback ... the memory of the referenced object stays allocated as long as the weak reference count doesn't reach 0. The reason is that a smart-pointer isn't able to reset all the weak-pointers that are referencing an object. If big objects are used, this isn't optimal. The solution is to doubly link all weak-pointers of a single referenced object. This would triple memory usage for weak-pointers, but per definition, weak-pointers should be used rarely. Of course there's another disadvantage - the stack size for destructing big object graphs is high. But if this becomes a problem keep in mind that every recursively working algorithm can be transformed into a non-recursively working algorithm. Copyright (c) 2016: Juergen Luers |