- •Содержание
- •Благодарности
- •Как читать эту книгу
- •Несколько слов о стиле программирования
- •Переменные и константы
- •const
- •Стековые и динамические объекты
- •Области действия и функции
- •Области действия
- •Перегрузка
- •Видимость
- •Типы и операторы
- •Конструкторы
- •Деструкторы
- •Присваивание
- •Перегрузка операторов
- •Что такое шаблоны и зачем они нужны?
- •Проблемы
- •Обходные решения
- •Синтаксис шаблонов
- •Параметризованные типы
- •Параметризованные функции
- •Параметризованные функции классов
- •Передача параметра
- •Шаблоны с несколькими параметрами
- •Долой вложенные параметризованные типы!
- •Наследование
- •Комбинации простых и параметризованных типов
- •Небезопасные типы в открытых базовых классах
- •Небезопасные типы в закрытых базовых классах
- •Небезопасные типы в переменных класса
- •Глава 4. Исключения
- •Обработка исключений в стандарте ANSI
- •Синтаксис инициирования исключений
- •Синтаксис перехвата исключений
- •Конструкторы и деструкторы
- •Нестандартная обработка исключений
- •Условные обозначения
- •Глава 5. Умные указатели
- •Глупые указатели
- •Умные указатели как идиома
- •Оператор ->
- •Параметризованные умные указатели
- •Иерархия умных указателей
- •Арифметические операции с указателями
- •Во что обходится умный указатель?
- •Применения
- •Разыменование значения NULL
- •Отладка и трассировка
- •Кэширование
- •Семантика ведущих указателей
- •Конструирование
- •Уничтожение
- •Копирование
- •Присваивание
- •Прототип шаблона ведущего указателя
- •Дескрипторы в C++
- •Что же получается?
- •Подсчет объектов
- •Указатели только для чтения
- •Указатели для чтения/записи
- •Интерфейсные указатели
- •Дублирование интерфейса
- •Маскировка указываемого объекта
- •Изменение интерфейса
- •Грани
- •Преобразование указываемого объекта в грань
- •Кристаллы
- •Вариации на тему граней
- •Инкапсуляция указываемого объекта
- •Проверка граней
- •Обеспечение согласованности
- •Грани и ведущие указатели
- •Переходные типы
- •Полиморфные указываемые объекты
- •Выбор типа указываемого объекта во время конструирования
- •Изменение указываемого объекта во время выполнения программы
- •Посредники
- •Функторы
- •Массивы и оператор []
- •Проверка границ и присваивание
- •Оператор [] с нецелыми аргументами
- •Имитация многомерных массивов
- •Множественные перегрузки оператора []
- •Виртуальный оператор []
- •Курсоры
- •Простой класс разреженного массива
- •Курсоры и разреженные массивы
- •Операторы преобразования и оператор ->
- •Итераторы
- •Активные итераторы
- •Пассивные итераторы
- •Что лучше?
- •Убогие, но распространенные варианты
- •Лучшие варианты
- •Итератор абстрактного массива
- •Операторы коллекций
- •Мудрые курсоры и надежность итераторов
- •Частные копии коллекций
- •Внутренние и внешние итераторы
- •Временная пометка
- •Пример
- •Тернистые пути дизайна
- •Транзакции
- •Отмена
- •Хватит?
- •Образы и указатели
- •Простой указатель образов
- •Стеки образов
- •Образы автоматических объектов
- •Образы указателей
- •Комбинации и вариации
- •Транзакции и отмена
- •Транзакции и блокировки
- •Класс ConstPtr
- •Класс LockPtr
- •Создание и уничтожение объектов
- •Упрощенное создание объектов
- •Отмена
- •Варианты
- •Вложенные блокировки
- •Взаимные блокировки и очереди
- •Многоуровневая отмена
- •Оптимизация объема
- •Несколько прощальных слов
- •Часть 3. Снова о типах
- •Гомоморфные иерархии классов
- •Взаимозаменяемость производных классов
- •Нормальное наследование
- •Инкапсуляция производных классов
- •Множественная передача
- •Двойная передача
- •Гетероморфная двойная передача
- •Передача более высокого порядка
- •Группировка передач и преобразования
- •Производящие функции
- •make-функции
- •Символические классы и перегруженные make-функции
- •Оптимизация с применением производящих функций
- •Локализованное использование производящих функций
- •Уничтожающие функции
- •Снова о двойной передаче: промежуточные базовые классы
- •Объекты классов
- •Информация о классе
- •Еще несколько слов об уничтожающих функциях
- •Определение класса по объекту
- •Представители
- •Основные концепции
- •Инкапсуляция указателей и указываемых объектов
- •Производящие функции
- •Ссылки на указатели
- •Неведущие указатели
- •Ведущие указатели
- •Снова о двойной передаче
- •Удвоенная двойная передача
- •Самомодификация и переходимость
- •Множественная двойная передача
- •Применение невидимых указателей
- •Кэширование
- •Распределенные объекты и посредники
- •Нетривиальные распределенные архитектуры
- •Часть 4. Управление памятью
- •Перегрузка операторов new и delete
- •Простой список свободной памяти
- •Наследование операторов new и delete
- •Аргументы оператора new
- •Конструирование с разделением фаз
- •Уничтожение с разделением фаз
- •Кто управляет выделением памяти?
- •Глобальное управление
- •Выделение и освобождение памяти в классах
- •Объекты классов и производящие функции
- •Управление памятью под руководством клиента
- •Управление памятью с применением ведущих указателей
- •Перспективы
- •Строительные блоки
- •Поблочное освобождение памяти
- •Скрытая информация
- •Подсчет ссылок
- •Базовый класс с подсчетом ссылок
- •Ведущие указатели с подсчетом ссылок
- •Дескрипторы с подсчетом ссылок
- •Трудности подсчета ссылок
- •Подсчет ссылок и ведущие указатели
- •Деление по классам
- •Деление по размеру
- •Деление по средствам доступа
- •Пространства стека и кучи
- •Поиск указателей
- •Мама, откуда берутся указатели?
- •Поиск указателей
- •Дескрипторы, повсюду дескрипторы
- •Общее описание архитектуры
- •Ведущие указатели
- •Вариации
- •Оптимизация в особых ситуациях
- •Алгоритм Бейкера
- •Пространства объектов
- •Последовательное копирование
- •Внешние объекты
- •Алгоритм Бейкера: уход и кормление в C++
- •Уплотнение на месте
- •Базовый класс VoidPtr
- •Пул ведущих указателей
- •Итератор ведущих указателей
- •Алгоритм уплотнения
- •Оптимизация
- •Перспективы
- •Глава 16. Сборка мусора
- •Доступность
- •Периметр
- •Внутри периметра
- •Анализ экземпляров
- •Перебор графа объектов
- •Сборка мусора по алгоритму Бейкера
- •Шаблон слабого дескриптора
- •Шаблон сильного дескриптора
- •Итераторы ведущих указателей
- •Перебор указателей
- •Оптимизация
- •Внешние объекты
- •Множественные пространства
- •Сборка мусора и уплотнение на месте
- •Нужно ли вызывать деструкторы?
- •Только для профессиональных каскадеров
- •Организация памяти
- •Поиск периметра
- •Перебор внутри периметра
- •Сборка мусора
- •Последовательная сборка мусора
- •Итоговые перспективы
75
Умные указатели как идиома
Возникающие проблемы стоит разбирать последовательно. До арифметических операций с указателями мы доберемся позже, поэтому пока будем пользоваться ptr_diff.
Оператор ->
Теперь вы знаете, почему оператор -> был сделан перегружаемым. В полном соответствии с синтаксисом, описанным в главе 2, PFoo теперь обзаводится собственным оператором ->. Оператора преобразования хватает для вызова внешних функций. Приведенный ниже вызов функции f() работает, потому что у компилятора хватает ума поискать оператор преобразования, соответствующий сигнатуре функции, и в данном случае оператор Foo*() прекрасно подходит.
class PFoo { private:
Foo* foo; public:
PFoo() : foo(NULL) {} PFoo(Foo* f) : foo(f) {} operator Foo*() { return foo; }
Foo* operator->() { return foo; }
}; |
|
|
void f(Foo*); |
|
|
PFoo pf(new Foo); |
|
|
f(pf); |
// Работает |
благодаря функции operator Foo*() |
pf->MemberOfFoo(); |
// Работает |
благодаря функции operator->() |
Причина, по которой работает pf->MemberOfFoo(), менее очевидна. В левой части оператора -> указан пользовательский тип, поэтому компилятор ищет перегруженную версию оператора ->. Он находит ее, вычисляет и заменяет pf возвращаемым значением, которое превращается в новое левостороннее выражение оператора ->. Этот процесс рекурсивно продолжается до тех пор, пока левостороннее выражение не преобразуется к базовому типу. Если таким базовым типом является указатель на структуру, указатель на класс или указатель на объединение, компилятор обращается к указанному члену. Если это что-то иное (например, int), компилятор злорадно хохочет и выдает сообщение об ошибке. В нем он оценивает ваш интеллект и перспективы будущей работы на основании того факта, что вы пытаетесь обратиться к члену чего-то, вообще не имеющего членов. В любом случае поиск заканчивается при достижении базового типа. Для самых любопытных сообщаю, что большинство компиляторов, которыми я пользовался, не отслеживает истинной рекурсии вида:
PFoo operator->() { return *this; }
Здесь оператор -> пользовательского типа возвращает экземпляр этого типа в качестве своего значения. Компиляторы C++ обычно предпочитают помучить вас в бесконечном цикле.
Итак, у нас появился класс-указатель, который можно использовать везде, где используются указатели Foo*: в качестве аргументов функций, слева от оператора -> или при определении дополнительной семантики арифметических операций с указателями — всюду, где Foo* участвует в сложении или вычитании.
Параметризованные умные указатели
Один из очевидных подходов к созданию универсальных умных указателей — использование шаблонов.
template <class Type> class SP {
private:
Type* pointer; public:
76
SP() : pointer(NULL) {} SP(Type* p) : pointer(p) {}
operator Type*() { return pointer; } Type* operator->() { return pointer; }
}; |
|
|
void f(Foo*); |
|
|
Ptr<Foo> pf(new Foo); |
|
|
f(pf); |
// Работает |
благодаря функции operator Type*() |
pf->MemberOfFoo(); |
// Работает |
благодаря функции operator->() |
Этот шаблон подойдет для любого класса, не только для класса Foo. Перед вами — одна из базовых форм умных указателей. Она используется достаточно широко и даже может преобразовать указатель на производный класс к указателю на базовый класс при условии, что вы пользуетесь хорошим компилятором.
Хороший компилятор C++ правильно обрабатывает такие ситуации, руководствуясь следующей логикой:
1. Существует ли конструктор P<Foo>, который получает Р<Ваr>? Нет. Продолжаем поиски.
2.Существует ли в Р<Ваr> операторная функция operator P<Foo>()? Нет. Ищем дальше.
3.Существует ли пользовательское преобразование от Р<Ваr> к типу, который подходит под сигнатуру какого-либо конструктора P<Foo>? Да! Операторная функция operator Bar*() превращает Р<Ваr> в Bar*, который может быть преобразован компилятором в Foo*. Фактически выражение вычисляется как Ptr<Foo>pf2(Foo*(pb.operator Bar*())), где преобразование Bar* в Foo* выполняется так же, как для любого другого встроенного указателя.
Как я уже говорил, все должно работать именно так, но учтите — некоторые компиляторы обрабатывают эту ситуацию неправильно. Даже в хороших компиляторах результат вложения подставляемой (inline) операторной функции operator Bar*() во встроенный P<Foo>(Foo*) может быть совсем не тем, на который вы рассчитывали; многие компиляторы создают вынесенные (а следовательно, менее эффективные) копии встроенных функций классов вместо того, чтобы генерировать вложенный код подставляемой функции. Мораль: такой шаблон должен делать то, что вы хотите, но у компилятора на этот счет может быть другое мнение.
Иерархия умных указателей
Вместо использования шаблонов можно поддерживать параллельные иерархии указателей и объектов, на которые они указывают. Делать это следует лишь в том случае, если ваш компилятор не поддерживает шаблоны или плохо написан.
class PVoid { |
// Заменяет void* |
protected: |
|
void* addr; |
|
public: |
|
PVoid() : addr(NULL) {} PVoid(void* a) : addr(a) {} operator void*() { return addr; }
};
class Foo : public PVoid { public:
PFoo() : PVoid() {} PFoo(Foo* p) : PVoid(p) {}
operator Foo*() { return (Foo*)addr; } Foo* operator->() { return (Foo*)addr; }
77
};
class Pbar : public PFoo { public:
PBar() : PFoo() {} PBar(Bar* p) : PFoo(p) {}
operator Bar*() { return (Bar*)addr; } Bar* operator->() { return (Bar*)addr; }
}; |
|
pBar pb(new Bar); |
|
pFoo pf(pb); |
// Работает, потому что PBar является производным от PFoo |
pf->MemberOfFoo(); |
// Работает благодаря PFoo::operator-> |
Этот вариант будет работать, если вас не огорчают многочисленные копирования/вставки текста и (в зависимости от компилятора) предупреждения о том, что скрывает PFoo::operator->(). Конечно, такое решение не настолько элегантно, как встроенные типы указателей шаблона Ptr.
Арифметические операции с указателями
Ниже показан пример арифметических операторов, обеспечивающих работу операций сложения/вычитания для умных указателей. Для полной, абсолютно совместимой реализации к ним следует добавить операторы ++ и --.
template <class Type> class Ptr {
private:
Type* pointer; public:
Ptr() : pointer(NULL) {} Ptr(Type* p) : pointer(p) {}
operator Type*() { return pointer; }
ptr_diff operator-(Ptr<Type> p) { return pointer – p.pointer; } ptr_diff operator-(void* v) { return ((void*)pointer) – v; }
Ptr<Type> operator-(long index) { return Ptr<Type>(pointer – index); } Ptr<Type> operator-=(long index) { pointer -= index; return *this; } Ptr<Type> operator+(long index) { return Ptr<Type>(pointer + index); } Ptr<Type> operator+=(long index) { pointer += index; return *this; }
};
Важно понимать, чем ptr_diff отличается от целого индекса. При вычитании одного адреса из другого результатом является смещение, как правило, выраженное в байтах. В случае прибавления целого к указателю адрес изменяется на размер объекта, умноженный на целое. Помните: в C++, как и в С, указатель ссылается не на один объект, а на теоретический массив объектов. Индексы в описанных выше перегруженных операторах представляют собой индексы этого теоретического массива, а не количества байт.
После всего сказанного я бы не советовал пускаться на эти хлопоты для умных указателей — не из-за лени, а потому, что в этом случае вы обрекаете себя на решения, которые не всегда желательны. Если дать пользователю возможность складывать и вычитать указатели, вам неизбежно придется поддерживать идею, что указатель всегда индексирует воображаемый массив. Как выяснится в следующих главах, многие варианты применения умных указателей не должны или даже не могут правильно работать с парадигмой массива.