Поводок

Думаю, не стоит в который раз упоминать, о том, как может быть полезен сбор мусора, и о том, что в C++ её порой недостаёт. Так что, немного пораздумав, я решил сделать может быть не самую эффективную, но простую реализацию, о чём я и хочу поведать.

Постановка задачи, или как это сделать

Существует несколько способов реализации мусоросборщиков, и нам предстоит выбрать ту, которая бы нас устроила. Для начала стоит сформулировать требования и пожелания. Я выбрал такие:

  • простота реализации;
  • слабая зависимость (в идеале — независимость) от программной платформы;
  • прозрачность, то есть скрытость механизма сборки от пользователя (в идеале — пользователь работает со сборщиком так же, как он это делает с обычными указателями на статические объекты).

Требование простоты и независимости от платформы побудило меня отбросить все возможные варианты асинхронных сборщиков (работающих отдельной низкоприоритетной задачей), так как это привело бы к необходимости использовать синхронизацию и блокировку ресурсов, а также протребовало бы вытесняющей многозадачности. Поэтому все действия по сборке придётся производить вместе с основными действиями. Также судя по всему весьма сложно и неэффективно использовать сканирующие методы (если вообще возможно), ведь в C++ весьма ограниченные возможности получения информации о типах и их структуре во время исполнения. Поэтому пришлось взять метод подсчёта в процессе их создания и уничтожения: у каждого объекта заводится счётчик ссылающихся на него, который увеличивается при копировании ссылки и уменьшается при уничтожении, после чего, если его значение достигло нуля (уничтожены все ссылки) объект уничтожается.

Реализация

Для этого необходимо два класса: первый из них — пристройка к объекту, содержащая счётчик и следящая за ним, второй представляет собой «умный» указатель, который ведёт себя как обычный указатель, но при этом сигналил счётчику об операциях над собой.

Соответственно счётчик должен иметь методы получения сигнала о появлении новой ссылки, её исчезновения и уметь уничтожать объект, как только пропадут все связи на него. Умный указатель должен имитировать работу обычного, то есть повторять стандартные действия над указателями — разыменование (*, →), присвоение (=) (для простоты не будем рассматривать операции над массивами: –, ++, []). Но помимо этого в обязаности умного указателя входит сигнализировать о всех операциях над собой счётчику.

Ну и так как сборщику мусора предстоит работать с объектами разного типа, то наилучшим вариантом будет использовать шаблоны C++ (template).

Здесь я сделаю лирическое отступление по теме на тему названий. Мне не нравятся монстроидальные названия, вроде ReferenceCountingPointer, поэтому, если для сущности, которую я описываю нет простого общепринятого названия всегда стараюсь подобрать простую метафору. На этот раз в процессе размышления неожиданно возникла идея о поводке и ошейнике.

  • Поводок (leash) — это больше чем указатель, он не просто позволяет найти существо, но и удерживает его при себе, не давая убежать.
  • Ошейник (collar) — то, что постоянно находится на существе, позволяя прикрепить поводок. Конечно, к нему можно прикрепить несколько поводков, но если будет отцеплен последний, существо убежит.

Так что предстоит иметь два класса, а точнее шаблона класса: 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;
}

Итоги

Что у нас получилось? Рассмотрим плюсы и минусы.

Плюсы
  • простая реализация;
  • малый дополнительный расход памяти;
  • переносимость;
  • соответствует стандарту c++;
  • не требует особых средств программного окружения.
Минусы
  • не поддерживает преобразование между дочерними и родительскими классами;
  • не отлавливает сложные ситуации, такие как кольцевые ссылки;
  • нестандартный метод аллокации объектов не позволяет использовать произвольные конструкторы;
  • не обеспечивает слабые ссылки.

Может ещё есть, но эти, как мне кажется, наиболее существенные.

Можно ли это улучшить?

cifra/povodok.txt · Последние изменения: 2011/02/23 00:59 — vovanium
За исключением случаев, когда указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: CC Attribution 3.0 Unported