Встречайте Deterministic Garbage Pointer Collector

от автора

Память, в С++ с ней было всегда сложно работать (горькое наследство C)… Тут нам на помощь приходит C++11 со своими std::shared_ptr.

Как вы догадались, если бы у этих примитивов не было бы проблем, то не было бы этой статьи 🙂

Давайте рассмотри следующий пример классической утечки памяти на 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; } 

Очевидно, что мы не увидим вызов деструкторов объектов. Как с этим бороться? std::weak_ptr приходит нам на помощь:

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

Да, это помогает решить проблему. Но вот если у вас более сложная иерархия объектов и очень сложно понять, кого следует сделать std::weak_ptr, а кого std::shared_ptr? Или вы не хотите вообще замарачиваться с слабыми связями?

Garbage Collector — наше все !!

Нет, конечно же нет. В С++ нету нативной поддержки Garbage Collector-а, а даже если ее добавят мы получаем накладные рассходы на работу Garbage Collector-а, плюс ломается RAII.

Что же нам делать?

Deterministic Garbage Pointer Collector — это поинтер, который трекает все ссылки от root-объектов и как только никто из root-объектов не ссылается на наш объект, он сразу же удаляется.

Принцип его работы подобен std::shared_ptr (он трекает scope), но также объекты, которые ссылаются на него.

Давайте рассмотрим принцип его работы на предудущем примере:

#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(void * rootPtr) {     child_ptr_.disconnectFromRoot(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(void * rootPtr) {     parent_ptr_.disconnectFromRoot(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; }  

Как видим, кода стало немного больше, но это та цена, которую необходимо заплатить за полностью автоматическое удаление объектов. Видно, что добавились дополнительные методы connectToRoot и disconnectFromRoot. Конечно же, писать их руками все время будет довольно сложно, поэтому я намереваюсь сделать небольшой генератор этих методов в классах, которые используют gc_ptr (как видим эти мы придерживаемся принципа Zero-Overhead, мы не платим за то что не используем, а если используем-то расходы не больше, чем если бы мы написали это руками).

Библиотека gc_ptr.hpp потоко-безопасная, она не создает никаких дополнительных потоков для сборки мусора, все выполняется в конструкторе, деструкторе и операторах присваивания, так что если мы затираем наш объект и на него больше не ссылается ни один root-объект, то ты возвращаем память, выделенную под наш объект.

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/post/462583/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *