Недавно появилась заметка о простой и эффективной «ручной» сборке мусора в С++. Вполне допускаю, что локальная сборка мусора внутри какого-то сложного класса (где идёт активная низкоуровневая работа с указателями) может быть оправдана. Но в масштабах большой программы, существует более надёжный и простой метод избавления от утечек памяти. Не претендуя на «метод на все случаи жизни», очень надеюсь, что он сделает проще жизнь хотя бы некоторым из читателей.
Суть метода предельно проста: если каждый объект является переменной какой-либо области видимости или простым («стековым») членом другого объекта, то даже при аварийном закрытии программы от необработанного исключения, всегда будет происходить корректная очистка. Задача заключается в том, чтобы свести всё многообразие динамических сценариев к этой схеме.
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). Там по-своему объясняется эта методика, а также некоторые другие интересные возможности (быстрая компиляция, быстрое разрушающее копирование, ускорение массивов на порядок).
Некоторые теоретические аспекты подхода были изложены в одной из предыдущих статей.
Автор: mt_