Недавно появилась заметка о простой и эффективной «ручной» сборке мусора в С++. Вполне допускаю, что локальная сборка мусора внутри какого-то сложного класса (где идёт активная низкоуровневая работа с указателями) может быть оправдана. Но в масштабах большой программы, существует более надёжный и простой метод избавления от утечек памяти. Не претендуя на «метод на все случаи жизни», очень надеюсь, что он сделает проще жизнь хотя бы некоторым из читателей.
Суть метода предельно проста: если каждый объект является переменной какой-либо области видимости или простым («стековым») членом другого объекта, то даже при аварийном закрытии программы от необработанного исключения, всегда будет происходить корректная очистка. Задача заключается в том, чтобы свести всё многообразие динамических сценариев к этой схеме.
В простейшем, «статическом», случае, это означает просто включение объекта в класс-владелец обычным образом в качестве члена. Я противопоставляю ему более экзотические варианты включения объекта в класс его владельца через указатель или ссылку (заметьте, не в любой класс, а в класс-владелец).
«Корневные» объекты программы объявляются «стековыми» переменными в main(). Причём, лучше всё-таки в main(), чем в виде глобальных переменных, потому что в первом случае можно гарантировать порядок очистки (противопоставляю случай с набором глобальных объектов, разбросанных по единицам трансляции).
Размещая все объекты указанным образом, даже после выброса необработанного исключения, будет проведена корректная зачистка.
Интереснее, когда объект требуется создать по ходу выполнения:
Из подобных владеющих примитивов можно создавать довольно сложные модели. Следующий уровень сложности — передача объектов между владельцами:
Передачу объекта вместе с правом владения можно сделать, добавив в массивы и контейнеры разрушающее копирование внутреннего указателя:
В результате, если владелец вернул из своей функции-члена массив или контейнер, то он фактически передал право владения дочерними объектами вызывающему объекту. Вызывающий объект стал новым владельцем. И объекты не имеют никаких шансов стать утечками памяти, поскольку гарантированно будут кем-то зачищены.
Снова напоминаю, что это всё работает только в том случае, когда мы строго придерживаемся правила, что у любого объекта есть ровно один владелец.
А это значит, что даже если владелец передаёт «наружу» ссылку или указатель на объект, получатель может просить этот объект поучаствовать в каком-то функционале (путём вызова открытых функций-членов объекта). Но не может этот объект удалить, так как не является его владельцем:
На этом всё.
Может и не «серебряная пуля», но для абсолютного большинства применений, вполне достаточно.
Более сложные сценарии, при желании, можно будет разобрать в комментариях.
P.S. Заинтересовавшимся этой темой, рекомендую ознакомиться с библиотекой, где все эти концепции уже реализованы — пакетом Core (концепция, пример массива) фреймворка U++ (лицензия BSD). Там по-своему объясняется эта методика, а также некоторые другие интересные возможности (быстрая компиляция, быстрое разрушающее копирование, ускорение массивов на порядок).
Некоторые теоретические аспекты подхода были изложены в одной из предыдущих статей.
Суть метода предельно проста: если каждый объект является переменной какой-либо области видимости или простым («стековым») членом другого объекта, то даже при аварийном закрытии программы от необработанного исключения, всегда будет происходить корректная очистка. Задача заключается в том, чтобы свести всё многообразие динамических сценариев к этой схеме.
1. Каждый объект имеет ровно одного владельца.
Самый главный принцип. Прежде всего, он означает, что в ходе выполнения программы объект может быть удалён только единственным объектом-владельцем, и никем другим.В простейшем, «статическом», случае, это означает просто включение объекта в класс-владелец обычным образом в качестве члена. Я противопоставляю ему более экзотические варианты включения объекта в класс его владельца через указатель или ссылку (заметьте, не в любой класс, а в класс-владелец).
«Корневные» объекты программы объявляются «стековыми» переменными в main(). Причём, лучше всё-таки в main(), чем в виде глобальных переменных, потому что в первом случае можно гарантировать порядок очистки (противопоставляю случай с набором глобальных объектов, разбросанных по единицам трансляции).
Размещая все объекты указанным образом, даже после выброса необработанного исключения, будет проведена корректная зачистка.
Разумеется, это самый очевидный случай, не предполагающий никакой динамики.class SomeParent { Child1 child1; Child2 child2; }; class Root { public: void Run(); private: SomeParent entry; }; int main(int argc, char **argv, char **envp) { Root().Run(); //даже при выбросе исключения, не будет утечек }
Интереснее, когда объект требуется создать по ходу выполнения:
2. Владеющие контейнеры.
Для хранения динамически создаваемого объекта применять контейнер с автозачисткой. Сам контейнер при этом объявляется обычным, «стековым» членом класса. Это может быть какой-то из вариантов умного указателя, либо ваша собственная реализация контейнера:В этом случае можно сказать, что контейнер является владельцем объекта.template <T> class One { public: One(); //изначально пустой One(T *); //владение объектом переходит контейнеру void Clear(); //уничтожение объекта вручную T *operator->(); //доступ к указателю T *GetPtr(); ~One(); //автозачистка }; //--- использование: class Owner { One<Entity> dynamicEntity; };
3. Владеющие массивы.
Используются в случае, когда нужно оперировать коллекцией объектов, объединённых по какому-либо признаку. Особенность такого массива понятна: в деструкторе он корректно уничтожает все свои элементы. При добавлении в массив по указателю, объект становится собственностью массива и точно также уничтожается им в деструкторе.Понятно, что владельцем всей коллекции объектов является массив, являющийся обычным, «стековым» членом класса-владельца.template <T> class Array { public: T & Add(); T & Add(const T &); //копирование T & Add(T *); //владение переходит массиву ~Array(); //уничтожает входящие элементы }; //для ассоциативных массивов - аналогично: template <K,T> class ArrayMap { public: T & Add(const K &); T & Add(const K &, const T &); //копирование T & Add(const K &, T *); //владение переходит массиву ~ArrayMap(); //уничтожает входящие элементы }; //--- использование: class Owner { Array<String> stringsArray; ArrayMap<int,String> anotherStringsCollection; };
Из подобных владеющих примитивов можно создавать довольно сложные модели. Следующий уровень сложности — передача объектов между владельцами:
4. Передача объектов передаёт право владения.
У каждого объекта — ровно один владелец. Владелец может передать объект другому владельцу, но сам теряет доступ к объекту.Передачу объекта вместе с правом владения можно сделать, добавив в массивы и контейнеры разрушающее копирование внутреннего указателя:
template <T> class One { public: //... One(const One<T> &source); //ptr = source.ptr; source.ptr = NULL; void operator=(const One<T> &source); //ptr = source.ptr; source.ptr = NULL; bool IsEmpty(); //узнать, владеем ли мы объектом private: mutable T *ptr; }; //аналогичный функционал добавляется и для массивов
В результате, если владелец вернул из своей функции-члена массив или контейнер, то он фактически передал право владения дочерними объектами вызывающему объекту. Вызывающий объект стал новым владельцем. И объекты не имеют никаких шансов стать утечками памяти, поскольку гарантированно будут кем-то зачищены.
Снова напоминаю, что это всё работает только в том случае, когда мы строго придерживаемся правила, что у любого объекта есть ровно один владелец.
А это значит, что даже если владелец передаёт «наружу» ссылку или указатель на объект, получатель может просить этот объект поучаствовать в каком-то функционале (путём вызова открытых функций-членов объекта). Но не может этот объект удалить, так как не является его владельцем:
class CleverEntity { public: void UpdateUI(Window *window) //получая указатель, получатель соглашается на использование объекта, //но не будет влиять на его жизненный цикл { //window->... //запрещено: delete window и прочие попытки уничтожить // либо перехватить владение объектом } }; class WindowWorker { public: void UpdateUI() { entity.UpdateUI(window.GetPtr()); } private: CleverEntity entity; One<Window> window; };
На этом всё.
Может и не «серебряная пуля», но для абсолютного большинства применений, вполне достаточно.
Более сложные сценарии, при желании, можно будет разобрать в комментариях.
P.S. Заинтересовавшимся этой темой, рекомендую ознакомиться с библиотекой, где все эти концепции уже реализованы — пакетом Core (концепция, пример массива) фреймворка U++ (лицензия BSD). Там по-своему объясняется эта методика, а также некоторые другие интересные возможности (быстрая компиляция, быстрое разрушающее копирование, ускорение массивов на порядок).
Некоторые теоретические аспекты подхода были изложены в одной из предыдущих статей.
