- •Содержание
- •Благодарности
- •Как читать эту книгу
- •Несколько слов о стиле программирования
- •Переменные и константы
- •const
- •Стековые и динамические объекты
- •Области действия и функции
- •Области действия
- •Перегрузка
- •Видимость
- •Типы и операторы
- •Конструкторы
- •Деструкторы
- •Присваивание
- •Перегрузка операторов
- •Что такое шаблоны и зачем они нужны?
- •Проблемы
- •Обходные решения
- •Синтаксис шаблонов
- •Параметризованные типы
- •Параметризованные функции
- •Параметризованные функции классов
- •Передача параметра
- •Шаблоны с несколькими параметрами
- •Долой вложенные параметризованные типы!
- •Наследование
- •Комбинации простых и параметризованных типов
- •Небезопасные типы в открытых базовых классах
- •Небезопасные типы в закрытых базовых классах
- •Небезопасные типы в переменных класса
- •Глава 4. Исключения
- •Обработка исключений в стандарте ANSI
- •Синтаксис инициирования исключений
- •Синтаксис перехвата исключений
- •Конструкторы и деструкторы
- •Нестандартная обработка исключений
- •Условные обозначения
- •Глава 5. Умные указатели
- •Глупые указатели
- •Умные указатели как идиома
- •Оператор ->
- •Параметризованные умные указатели
- •Иерархия умных указателей
- •Арифметические операции с указателями
- •Во что обходится умный указатель?
- •Применения
- •Разыменование значения NULL
- •Отладка и трассировка
- •Кэширование
- •Семантика ведущих указателей
- •Конструирование
- •Уничтожение
- •Копирование
- •Присваивание
- •Прототип шаблона ведущего указателя
- •Дескрипторы в C++
- •Что же получается?
- •Подсчет объектов
- •Указатели только для чтения
- •Указатели для чтения/записи
- •Интерфейсные указатели
- •Дублирование интерфейса
- •Маскировка указываемого объекта
- •Изменение интерфейса
- •Грани
- •Преобразование указываемого объекта в грань
- •Кристаллы
- •Вариации на тему граней
- •Инкапсуляция указываемого объекта
- •Проверка граней
- •Обеспечение согласованности
- •Грани и ведущие указатели
- •Переходные типы
- •Полиморфные указываемые объекты
- •Выбор типа указываемого объекта во время конструирования
- •Изменение указываемого объекта во время выполнения программы
- •Посредники
- •Функторы
- •Массивы и оператор []
- •Проверка границ и присваивание
- •Оператор [] с нецелыми аргументами
- •Имитация многомерных массивов
- •Множественные перегрузки оператора []
- •Виртуальный оператор []
- •Курсоры
- •Простой класс разреженного массива
- •Курсоры и разреженные массивы
- •Операторы преобразования и оператор ->
- •Итераторы
- •Активные итераторы
- •Пассивные итераторы
- •Что лучше?
- •Убогие, но распространенные варианты
- •Лучшие варианты
- •Итератор абстрактного массива
- •Операторы коллекций
- •Мудрые курсоры и надежность итераторов
- •Частные копии коллекций
- •Внутренние и внешние итераторы
- •Временная пометка
- •Пример
- •Тернистые пути дизайна
- •Транзакции
- •Отмена
- •Хватит?
- •Образы и указатели
- •Простой указатель образов
- •Стеки образов
- •Образы автоматических объектов
- •Образы указателей
- •Комбинации и вариации
- •Транзакции и отмена
- •Транзакции и блокировки
- •Класс ConstPtr
- •Класс LockPtr
- •Создание и уничтожение объектов
- •Упрощенное создание объектов
- •Отмена
- •Варианты
- •Вложенные блокировки
- •Взаимные блокировки и очереди
- •Многоуровневая отмена
- •Оптимизация объема
- •Несколько прощальных слов
- •Часть 3. Снова о типах
- •Гомоморфные иерархии классов
- •Взаимозаменяемость производных классов
- •Нормальное наследование
- •Инкапсуляция производных классов
- •Множественная передача
- •Двойная передача
- •Гетероморфная двойная передача
- •Передача более высокого порядка
- •Группировка передач и преобразования
- •Производящие функции
- •make-функции
- •Символические классы и перегруженные make-функции
- •Оптимизация с применением производящих функций
- •Локализованное использование производящих функций
- •Уничтожающие функции
- •Снова о двойной передаче: промежуточные базовые классы
- •Объекты классов
- •Информация о классе
- •Еще несколько слов об уничтожающих функциях
- •Определение класса по объекту
- •Представители
- •Основные концепции
- •Инкапсуляция указателей и указываемых объектов
- •Производящие функции
- •Ссылки на указатели
- •Неведущие указатели
- •Ведущие указатели
- •Снова о двойной передаче
- •Удвоенная двойная передача
- •Самомодификация и переходимость
- •Множественная двойная передача
- •Применение невидимых указателей
- •Кэширование
- •Распределенные объекты и посредники
- •Нетривиальные распределенные архитектуры
- •Часть 4. Управление памятью
- •Перегрузка операторов new и delete
- •Простой список свободной памяти
- •Наследование операторов new и delete
- •Аргументы оператора new
- •Конструирование с разделением фаз
- •Уничтожение с разделением фаз
- •Кто управляет выделением памяти?
- •Глобальное управление
- •Выделение и освобождение памяти в классах
- •Объекты классов и производящие функции
- •Управление памятью под руководством клиента
- •Управление памятью с применением ведущих указателей
- •Перспективы
- •Строительные блоки
- •Поблочное освобождение памяти
- •Скрытая информация
- •Подсчет ссылок
- •Базовый класс с подсчетом ссылок
- •Ведущие указатели с подсчетом ссылок
- •Дескрипторы с подсчетом ссылок
- •Трудности подсчета ссылок
- •Подсчет ссылок и ведущие указатели
- •Деление по классам
- •Деление по размеру
- •Деление по средствам доступа
- •Пространства стека и кучи
- •Поиск указателей
- •Мама, откуда берутся указатели?
- •Поиск указателей
- •Дескрипторы, повсюду дескрипторы
- •Общее описание архитектуры
- •Ведущие указатели
- •Вариации
- •Оптимизация в особых ситуациях
- •Алгоритм Бейкера
- •Пространства объектов
- •Последовательное копирование
- •Внешние объекты
- •Алгоритм Бейкера: уход и кормление в C++
- •Уплотнение на месте
- •Базовый класс VoidPtr
- •Пул ведущих указателей
- •Итератор ведущих указателей
- •Алгоритм уплотнения
- •Оптимизация
- •Перспективы
- •Глава 16. Сборка мусора
- •Доступность
- •Периметр
- •Внутри периметра
- •Анализ экземпляров
- •Перебор графа объектов
- •Сборка мусора по алгоритму Бейкера
- •Шаблон слабого дескриптора
- •Шаблон сильного дескриптора
- •Итераторы ведущих указателей
- •Перебор указателей
- •Оптимизация
- •Внешние объекты
- •Множественные пространства
- •Сборка мусора и уплотнение на месте
- •Нужно ли вызывать деструкторы?
- •Только для профессиональных каскадеров
- •Организация памяти
- •Поиск периметра
- •Перебор внутри периметра
- •Сборка мусора
- •Последовательная сборка мусора
- •Итоговые перспективы
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 с аргументами на базе ведущих указателей.
Существует немало причин для деления памяти на пространства. Ниже описаны некоторые распространенные стратегии выбора объектов, которые должны находиться в одном пространстве памяти.
Деление по классам
В предыдущих главах мы говорили об объектах классов, но так и не ответили напрямую на вопрос: как определить класс объекта для имеющегося объекта? Простейшее решение — добавить переменную,
215
которая ссылается на объект класса. К сожалению, оно связано с заметными затратами как по памяти, так и кода конструктора. Однако существуют два других варианта:
1. Хранить указатель на объект класса в ведущем указателе. Получаем те же затраты, но с улучшенной инкапсуляцией.
2.Выделить все экземпляры некоторого класса в одно пространство памяти и хранить указатель на объект класса в начале пространства (см. рисунок).
Класс
Объект
Пространство
памяти
Второй вариант существенно снижает затраты при условии, что по адресу объекта можно эффективно определить начало адресного пространства памяти, которому он принадлежит (возможно, с помощью упомянутой выше коллекции пространств памяти).
На первый взгляд кажется, что устраивать такую суету вокруг простейшей проблемы глупо. Почему бы просто не добавить дополнительную переменную, ссылающуюся на объект класса? Приведу по крайней мере две причины:
1. Класс, с которым вы работаете, может входить в коммерческую библиотеку, для которой у вас нет исходных текстов, или его модификация нежелательна по другим причинам.
2.Класс может представлять собой тривиальную объектно-ориентированную оболочку для примитивного типа данных (скажем, int). Лишние байты для указателя на объект класса (не говоря уже о v-таблице, которая неизбежно потребуется для виртуальных функций доступа к нему) могут оказаться весьма существенными.
Деление по размеру
Вполне разумно объединить все объекты одинакового размера (или принадлежащие одному диапазону размеров) в одно пространство памяти для оптимизации создания и уничтожения. Многие стратегии управления памятью работают для одних диапазонов лучше, чем для других. Например, при создании множества больших, сильно различающихся по размерам объектов схема со степенями 2 наверняка оставит много неиспользованных фрагментированных блоков. Однако для объектов относительно малых (или близких по размеру к степеням 2) такая схема работает просто замечательно. В крайних случаях все пространство памяти может заполняться объектами одинакового размера. Такие пространства обладают чрезвычайно эффективным представлением и легко управляются.
Деление по способу использования
Еще один возможный вариант — делить объекты в соответствии со способом их использования. Например, для многопоточного приложения-сервера объекты можно разделить по клиентам. Редко используемые объекты могут находиться в одном пространстве, а часто используемые — в другом. Объекты, кэшируемые на диске, могут отделяться от объектов, постоянно находящихся в памяти. Деление может осуществляться как на уровне классов, так и на уровне отдельных объектов.
Деление по средствам доступа
Другая важная причина для разделения по пространствам памяти заключается в том, чтобы хранить по отдельности объекты, доступ к которым осуществляется из стека, из других процессов или чисто
216
внутренние из кучи. Как будет показано в последних главах, это играет важную роль в схемах уплотнения и сборки мусора.
В двух следующих главах эта методика будет применяться довольно часто. Перемещаемые объекты отделяются от объектов, остающихся на одном месте. Ведущие указатели, доступные из стека, находятся в одном пространстве памяти, а доступные из других процессов — в другом.
Пространства стека и кучи
Наконец, сам стек тоже можно считать разновидностью пространства памяти, а выделяемые в стеке пулы — их частным случаем. Этот подход применялся ранее в этой главе для пулов, локальных по отношению к области действия конкретной функции. Эта особая интерпретация стека упоминается и в двух последующих главах.