Meet the Deterministic Garbage Collector Pointer

Memory, in C ++ it was always difficult to work with it (a bitter legacy of C ) ... Here C ++ 11 with its std :: shared_ptr comes to our aid.









As you might have guessed, if these primitives had no problems, then this article would not have been :)



Let's look at the following example of a classic memory leak on std :: shared_ptr :



#include <iostream> #include <memory> class Child; class Parent { public: Parent() { std::cout << "Parent()" << std::endl; } ~Parent() { std::cout << "~Parent()" << std::endl; } void createChild() { child_ptr_ = std::make_shared<Child>(); } std::shared_ptr<Child> getChild() { return child_ptr_; } private: std::shared_ptr<Child> child_ptr_; }; class Child { public: Child() { std::cout << "Child()" << std::endl; } ~Child() { std::cout << "~Child()" << std::endl; } void setParent(std::shared_ptr<Parent> parentPtr) { parent_ptr_ = parentPtr; } private: std::shared_ptr<Parent> parent_ptr_; }; int main() { auto parent = std::make_shared<Parent>(); parent->createChild(); parent->getChild()->setParent(parent); return 0; }
      
      





Obviously, we will not see the call of object destructors. How to deal with this? std :: weak_ptr comes to our aid:



 ... class Child { ... void setParent(std::shared_ptr<Parent> parentPtr) { parent_ptr_ = parentPtr; } private: std::weak_ptr<Parent> parent_ptr_; }; ...
      
      





Yes, it helps solve the problem. But if you have a more complex hierarchy of objects and it is very difficult to understand who should make std :: weak_ptr and who should be std :: shared_ptr ? Or do you not want to mess with weak ties at all?



Garbage Collector is our everything !!



No, of course not. In C ++, there is no native support for the Garbage Collector, and even if it is added, we get overhead costs for the Garbage Collector, plus RAII breaks.



What do we do?



Deterministic Garbage Collector Pointer is a Pointer that tracks all links from root objects and as soon as none of the root objects refers to our object, it is immediately deleted.



The principle of its operation is similar to std :: shared_ptr (it tracks scope ), but also objects that reference it.



Let's look at the principle of its operation in the previous example:



 #include <iostream> #include "gc_ptr.hpp" class Child; class Parent { public: Parent() { std::cout << "Parent()" << std::endl; } ~Parent() { std::cout << "~Parent()" << std::endl; } void createChild() { child_ptr_.create_object(); } memory::gc_ptr<Child> getChild() { return child_ptr_; } void connectToRoot(void * rootPtr) { child_ptr_.connectToRoot(rootPtr); } void disconnectFromRoot(bool isRoot, void * rootPtr) { child_ptr_.disconnectFromRoot(isRoot, rootPtr); } private: memory::gc_ptr<Child> child_ptr_; }; class Child { public: Child() { std::cout << "Child()" << std::endl; } ~Child() { std::cout << "~Child()" << std::endl; } void setParent(memory::gc_ptr<Parent> parentPtr) { parent_ptr_ = parentPtr; } void connectToRoot(void * rootPtr) { parent_ptr_.connectToRoot(rootPtr); } void disconnectFromRoot(bool isRoot, void * rootPtr) { parent_ptr_.disconnectFromRoot(isRoot, rootPtr); } private: memory::gc_ptr<Parent> parent_ptr_; }; int main() { memory::gc_ptr<Parent> parent; parent.create_object(); parent->createChild(); parent->getChild()->setParent(parent); return 0; }
      
      





As you can see, the code has become a bit more, but this is the price you need to pay for fully automatic removal of objects. It can be seen that additional connectToRoot and disconnectFromRoot methods were added. Of course, writing with their hands all the time will be quite difficult, so I intend to make a small generator of these methods in classes that use gc_ptr (as we see these we adhere to the Zero-Overhead principle, we do not pay for what we do not use, and if we use- then the costs are not more than if we wrote it with our hands).



The gc_ptr.hpp library is thread-safe, it does not create any additional threads for garbage collection, everything is done in the constructor, destructor and assignment operators, so if we overwrite our object and no root object refers to it anymore, then we return memory allocated for our object.



Thanks for attention!



All Articles