Думаю, не стоит в который раз упоминать, о том, как может быть полезен сбор мусора, и о том, что в C++ её порой недостаёт. Так что, немного пораздумав, я решил сделать может быть не самую эффективную, но простую реализацию, о чём я и хочу поведать.
Существует несколько способов реализации мусоросборщиков, и нам предстоит выбрать ту, которая бы нас устроила. Для начала стоит сформулировать требования и пожелания. Я выбрал такие:
Требование простоты и независимости от платформы побудило меня отбросить все возможные варианты асинхронных сборщиков (работающих отдельной низкоприоритетной задачей), так как это привело бы к необходимости использовать синхронизацию и блокировку ресурсов, а также протребовало бы вытесняющей многозадачности. Поэтому все действия по сборке придётся производить вместе с основными действиями. Также судя по всему весьма сложно и неэффективно использовать сканирующие методы (если вообще возможно), ведь в C++ весьма ограниченные возможности получения информации о типах и их структуре во время исполнения. Поэтому пришлось взять метод подсчёта в процессе их создания и уничтожения: у каждого объекта заводится счётчик ссылающихся на него, который увеличивается при копировании ссылки и уменьшается при уничтожении, после чего, если его значение достигло нуля (уничтожены все ссылки) объект уничтожается.
Для этого необходимо два класса: первый из них — пристройка к объекту, содержащая счётчик и следящая за ним, второй представляет собой «умный» указатель, который ведёт себя как обычный указатель, но при этом сигналил счётчику об операциях над собой.
Соответственно счётчик должен иметь методы получения сигнала о появлении новой ссылки, её исчезновения и уметь уничтожать объект, как только пропадут все связи на него. Умный указатель должен имитировать работу обычного, то есть повторять стандартные действия над указателями — разыменование (*, →), присвоение ( (для простоты не будем рассматривать операции над массивами: –, ++, []). Но помимо этого в обязаности умного указателя входит сигнализировать о всех операциях над собой счётчику.
Ну и так как сборщику мусора предстоит работать с объектами разного типа, то наилучшим вариантом будет использовать шаблоны C++ (template
).
Здесь я сделаю лирическое отступление по теме на тему названий. Мне не нравятся монстроидальные названия, вроде ReferenceCountingPointer, поэтому, если для сущности, которую я описываю нет простого общепринятого названия всегда стараюсь подобрать простую метафору. На этот раз в процессе размышления неожиданно возникла идея о поводке и ошейнике.
Так что предстоит иметь два класса, а точнее шаблона класса: leash_t
и collar_t
(да, я использую сишный суффикс _t для типов, так как часто перемешиваю чистый C и C++).
Для начала разберём collar_t.
Его описание выдглядит так:
template <class T> class collar_t:public T { private: int links; public: collar_t(): T() { links = 0; } collar_t(const collar_t& v): T(v) { links = 0; } collar_t(const T& v): T(v) { links = 0; } void link() { if(this)links++; } void unlink() { if(this) { if(!--links)delete this; } } }
Всё, что выдаёт в нём ошейник — методы link()
и unlink()
, первый увеличивает число связей, второй уменьшает, и если оно обнуляется, то уничтожает объект. Пожалуй, неожиданной здесь является проверка this
. Её можно не ставить, но, так как, бывает, указателям присваивают 0 (NULL), эту проверку придётся делать во всех операциях над указателями. Причина расположения её здесь — чтобы не писать её по многу раз в классе leash_t
.
Три конструктора, это классическая парочка — конструктор по умолчанию и конструктор копирования и конструктор копирования объекта без поводка, выполняют утилитарные функции — очищают счётчик. Не стоит бояться того, что при создании он равен нулю, следом всегда будет идти операция получения поводка.
Ах да, так и не решив, куда деть то, что должно быть в ошейнике, я решил его сделать ошейнику родителем. Таким образом с точки зрения пользователя становится неважно, общаемся мы с объектом в ошейнике или без, впрочем всегда можно сделать преобразование типа.
А теперь самое интересное — реализация поводка. Здесь сплошь и рядом языковые хитрости. Для начала всё незамысловато — объявляем шаблон класса и собственно скрытый настоящий указатель:
template <class T> class leash_t { private: collar_t<T> *p;
Далее делаем разнообразные конструкторы:
public: leash_t<T>() { p = 0; } leash_t<T>(const leash_t<T> &v) { p = v.p; p->link(); } leash_t<T>(collar_t<T> *v) { p = v; p->link(); }
p→link()
).
new
возвращает обычный указатель.
Вот здесь становится видно, что если бы не сделали проверку внутри collar_t::link
, её пришлось бы сделать уже дважды, но ведь описание класса ещё не кончилось.
И деструктор класса:
~leash_t<T>() { p->unlink(); }
Следующая цель — поводок должен вести себя как указатель. Поэтому надо реализовать свойственные ему операторы. Во первых, присвоение:
leash_t<T>& operator=(const leash_t<T> &v) { p->unlink(); p = v.p; p->link(); } leash_t<T>& operator=(const collar_t<T> *v) { p->unlink(); p = v; p->link(); }
Фактически это аналоги двух конструкторов, копии и обычного указателя. В отличие от обычных операторов присвоения, в задачи этих входит также сообщить старому ошейнику об отцеплении и новому — о прицеплении. Что и делается операциями p→unlink();
и p→link();
.
Во вторых, разыменования:
collar_t<T>& operator *() { return *p; } T * operator ->() { return p; }
Здесь я пошёл на сознательную хитрость. Пусть стандартный оператор разыменования (*) возвращает (по ссылке) объект в ошайние, но второй оператор (→) у меня возвращает объект типа без ошейника, а значит, обратившить к члену динамического класса пользователь не наткнётся на члены ошейника, если они вдруг совпадут.
Так сказать до кучи, можно добавить приведение типа:
operator T*() { return p; }
Если вдруг оно понадобится.
Но это ещё не всё. Мы научились работать с указателями, но ещё не с динамическими объектами. В принципе, мы можем создавать динамические объекты обычным образом — через new
, но особенность new
в том, что возвращает оно обычный указатель, а нам бы хотелось, чтобы пользователю при создании динамического объекта вручался поводок. Для этого нам придётся создать один метод (но аж в 4 вариантах), который является обёрткой для new
:
static leash_t<T> alloc() { return leash_t<T>(new collar_t<T>); } static leash_t<T> alloc(const T *v) { return leash_t<T>(new collar_t<T>(*v)); } static leash_t<T> alloc(const collar_t *v) { return leash_t<T>(new collar_t<T>(*v)); } static leash_t<T> alloc(const leash_t<T> &v) { return leash_t<T>(new collar_t<T>(*(v.p))); }
Вот собственно и всё.
Осталось рассказать о ещё одной хитрости. Так как ошейник — штука для пользователя сама по себе бесполезная, то лучше всего её спрятать. И я не нашёл ничего лучше, кроме как сделать класс ошейника членом класса поводка:
template <class T> class leash_t { public: class collar_t:public T { ...
При этом пришлось немного изменить код, так как теперь ошейник — не шаблон класса (collar_t<T>
), а чистый класс (collar_t
) внутри шаблона, а там он и используется. правда извне он будет уже называться как leash_t<T>::collar_t
. Результирующий код выходит такой:
1: template <class T> class leash_t { 2: public: 3: class collar_t:public T { 4: private: 5: int links; 6: public: 7: collar_t(): T() { 8: links = 0; 9: } 10: collar_t(const collar_t& v): T(v) { 11: links = 0; 12: } 13: collar_t(const T& v): T(v) { 14: links = 0; 15: } 16: void link() { 17: if(this) links++; 18: } 19: void unlink() { 20: if(this) { 21: if(!--links)delete this; 22: } 23: } 24: }; 25: private: 26: collar_t *p; 27: public: 28: leash_t<T>() { 29: p = 0; 30: } 31: leash_t<T>(collar_t *v) { 32: p = v; 33: p->link(); 34: } 35: leash_t<T>(const leash_t<T> &v) { 36: p = v.p; 37: p->link(); 38: } 39: ~leash_t<T>() { 40: p->unlink(); 41: } 42: leash_t<T>& operator=(const leash_t<T> &v) { 43: p->unlink(); 44: p = v.p; 45: p->link(); 46: } 47: leash_t<T>& operator=(const collar_t *v) { 48: p->unlink(); 49: p = v; 50: p->link(); 51: } 52: operator T*() { 53: return p; 54: } 55: collar_t& operator *() { 56: return *p; 57: } 58: T * operator ->() { 59: return p; 60: } 61: static leash_t<T> alloc() { 62: return leash_t<T>(new collar_t); 63: } 64: static leash_t<T> alloc(const T *v) { 65: return leash_t<T>(new collar_t(*v)); 66: } 67: static leash_t<T> alloc(const collar_t *v) { 68: return leash_t<T>(new collar_t(*v)); 69: } 70: static leash_t<T> alloc(const leash_t<T> &v) { 71: return leash_t<T>(new collar_t(*(v.p))); 72: } 73: };
Вот тестовый код, в котором видно, как этот сборщик используется:
class dog{ public: void woof() { printf("woof!\n"); } }; leash_t<dog> inner() { leash_t<dog>p = leash_t<dog>::alloc(); leash_t<dog>q = p; leash_t<dog>s = leash_t<dog>::alloc(p); s->woof(); q->woof(); return p; } int main() { leash_t<dog>p = inner(); p->woof(); return 0; }
Что у нас получилось? Рассмотрим плюсы и минусы.
Может ещё есть, но эти, как мне кажется, наиболее существенные.
Можно ли это улучшить?