213
CMP operator->() { return *pointee; }
};
Если использовать дескрипторы в сочетании с ведущими указателями, можно выбрать, для каких
экземпляров класса следует подсчитывать ссылки, а какие экземпляры должны управляться другим
способом.
Трудности подсчета ссылок
Все выглядит так просто; однако без ложки дегтя дело все же не обходится. Подсчет ссылок обладает
одним очень распространенным недостатком — зацикливанием. Представьте себе ситуацию: объект А
захватил объект В (то есть вызвал для него функцию
Grab()
), а объект В сделал то же самое для
объекта А. Ни на А, ни на В другие объекты не ссылаются. Здравый смысл подсказывает, что А следует
удалить вместе с В, но они продолжают существовать, поскольку их счетчики ссылок так и не
обнуляются. Обидно, да?
Подобное зацикливание возникает сплошь и рядом. Оно может относиться не только к парам объектов,
но и целым подграфам. A
->
B
->
C
->
D
->
A, но никто за пределами этой группы не ссылается ни на
один из этих объектов. Группа словно плывет на «Летучем Голландце», построенном в эпоху высоких
технологий и обреченном на вечные скитания в памяти. Существует несколько стратегий борьбы с
зацикливаниями. Все они не обладают особой универсальностью, и в вашей конкретной ситуации это
может привести к отказу от подсчета ссылок. Как правило, встречаясь с проблемой циклических
ссылок, стоит рассмотреть более хитроумные приемы, описанные в двух последних главах. Как видите,
мысль об отказе от подсчета ссылок приходит довольно быстро.
Декомпозиция
Предположим, А захватывает В, а затем В захватывает некоторый компонент А:
class A {
private:
Foo*
foo;
B*
b;
};
Если сделать так, чтобы В выполнял захват в функции
foo
, проблем не возникает. Когда последняя
ссылка на A ликвидируется, его счетчик становится равным 0, поскольку В его не увеличивает. Для
этого придется проявить некоторую изрядную изобретательность при кодировании, к тому же дизайн
сильно зависит от особенностей конкретных объектов, но на удивление часто он решает проблему
зацикливания.
Сильные и слабые дескрипторы
Предположим, ссылка А на В создавалась через
Grab()
, а ссылка В на А — нет. В тот момент, когда
исчезнет последняя ссылка на А из внешнего мира, подсчет ссылок для обоих объектов пары прекратит
их существование. На этой идее основано различие между сильными (strong) и слабыми (weak)
дескрипторами или указателями с подсчетом ссылок. Описанный выше шаблон
CH
будет относиться к
сильным дескрипторам, поскольку поддерживает счетчик ссылок. Обычный шаблон дескриптора (без
вызова
Grab
и
Release
) будет относиться к слабым. Если спроектировать архитектуру объектов так,
чтобы не существовало циклических подграфов, содержащих исключительно сильные дескрипторы, то
вся схема подсчета ссылок снова возвращается в игру. Самая распространенная ситуация с таким
решением — иерархия целое/часть, в которой пары удаляются при удалении целого. Целые
поддерживают сильные ссылки, части — слабые.
Подсчет ссылок и ведущие указатели
Одно из самых распространенных и полезных применений подсчета ссылок заключается в управлении
ведущими указателями. В предыдущих главах эта тема упоминалась неоднократно. Дескрипторы
живут в стеке и потому автоматически уничтожаются при сборке мусора, выполняемой компилятором.
Однако ведущие указатели (по тем же причинам, что и объекты) обычно приходится создавать в куче.
Как узнать, когда следует удалять ведущий указатель? Подсчет ссылок упрощает эту задачу.
214
Проблем с зацикливанием не будет: поскольку ведущий указатель не хранит ссылок на свои
дескрипторы, связь является односторонней. Копируемые и передаваемые дескрипторы сохраняют
длину в четыре байта без виртуальных функций, а лишь тривиальными подставляемыми функциями.
Grab
и
Release
съедают несколько дополнительных машинных тактов, но это мелочи по сравнению с
тем, что вам пришлось бы проделывать для управления ведущима указателями без них. Несколько
лишних байт для счетчика в ведущим указателе не играют особой роли; к тому же они выделяются в
куче, степень детализации которой обычно заметно превышает четыре байта.
Возможно, вам стоит вернуться к предыдущим главам и подумать, как использовать показанную схему
подсчета ссылок везде, где встечаются ведущие указатели. Это станет ключом к нетривиальному
управлению памятью в дальнейших главах.
Пространтсва памяти
Все эти фокусы образуют фундамент для дальнейшего строительства, но относить их к архитектуре
было бы неверно. Для действительно нетривиального управления памятью понадобятся нетривиальные
организационные концепции. В простейшем случае вся доступная память рассматривается как один
большой блок, из которого выделяются блоки меньшего размера. Для этого можно либо напрямую
обратиться к операционной системе с требованием выделить большой блок памяти при запуске, либо
косвенно, в конечном счете перепоручая работу операторным функциям
::operator new
и
::operator delete
.
В двух последних главах мы взглянем на проблему с нетривиальных позиций и поделим доступную
память на пространства (memory spaces). Пространства памяти — это концепция; ее можно
реализовать на основе практически любой описанной выше блочно-ориентированной схемы
управления памятью. Например, в одном пространстве памяти может использоваться система
напарников, а в другом — списки свободной памяти. Концепция представлена в следующем
абстрактном базовом классе:
class MemSpace {
public:
void* Allocate(size_t bytes) = 0;
void Deallocate(void* space, size_t bytes) = 0;
};
(Если ваш компилятор поддерживает обработку исключений, при объявлении обоих функций следует
указать возможность инициирования исключений.) Некоторым пространствам памяти можно не
сообщать в функции
Deallocate()
размер возвращаемых блоков; для конкретных схем могут
появиться другие функции, но минимальный интерфейс выглядит именно так. Возможно, также будет
поддерживаться глобальная структура данных — коллекция всех
MemSpace
(причины
рассматриваются ниже). Коллекция должна эффективно отвечать на вопрос: «Какому пространству
памяти принадлежит данный адрес?» По имеющемуся адресу объекта вы определяете пространство
памяти, в котором он живет.
В реализации пространств памяти могут быть использованы любые методики, описанные в
предыдущей главе:
•
Глобальная перегрузка операторов
new
и
delete
(обычно не рекомендуется).
•
Перегрузка операторов
new
и
delete
на уровне класса.
•
Использование оператора
new
с аргументами под руководством клиента.
•
Использование оператора
new
с аргументами на базе ведущих указателей.
Существует немало причин для деления памяти на пространства. Ниже описаны некоторые
распространенные стратегии выбора объектов, которые должны находиться в одном пространстве
памяти.
Деление по классам
В предыдущих главах мы говорили об объектах классов, но так и не ответили напрямую на вопрос: как
определить класс объекта для имеющегося объекта? Простейшее решение — добавить переменную,
Do'stlaringiz bilan baham: |