Мэтью Уилсон
РАСШИРЕНИЕ БИБЛИОТЕКИ STL ДЛЯ С++ Наборы и итераторы
Мэтью Уилсон
EXTENDED STL, VOLUME 1 Collections and Iterators
Upper Saddle River, NJ • Boston • Indianapolis • San Francisco New York • Toronto • Montreal • London • Munich • Paris • Madrid Capetown • Sydney • Tokyo • Singapore • Mexico City
Мэтью Уилсон
РАСШИРЕНИЕ БИБЛИОТЕКИ STL ДЛЯ С++
Наборы и итераторы
Москва, СанктПетербург, 2008
УДК ББК
681.3.068+800.92C++ 32.973.26-018.1 У35 Уилсон М. Расширение библиотеки STL для С++. Наборы и итераторы: Пер. с англ. Слинкина А. А. – М.: ДМК Пресс, СПб, БХВ-Петербург, 2008. – 608 с.: ил. + CD-ROM ISBN 978-5-94074-442-9 («ДМК Пресс») ISBN 978-5-9775-0196-5 («БХВ-Петербург») В книге известный специалист по языку C++ Мэтью Уилсон демонстрирует, как выйти за пределы стандарта C++ и расширить стандартную библиотеку шаблонов, применив лежащие в ее основе принципы к различным API и нестандартным наборам, чтобы получить более эффективные, выразительные, гибкие и надежные программы. Автор описывает передовые приемы, которые помогут вам в совершенстве овладеть двумя важными темами: адаптация API библиотек и операционной системы к STL-совместимым наборам и определение нетривиальных адаптеров итераторов. Это даст вам возможность в полной мере реализовать заложенные в STL возможности для написания эффективных и выразительных программ. На реальных примерах Уилсон иллюстрирует ряд важных концепций и технических приемов, позволяющих расширить библиотеку STL в таких направлениях, о которых ее создатели даже не думали, в том числе: наборы, категории ссылок на элементы, порча итераторов извне и выводимая адаптация интерфейса. Эта книга станет неоценимым подспорьем для любого программиста на C++, хотя бы в минимальной степени знакомого с STL. На прилагаемом компакт-диске находится обширная коллекция открытых библиотек, созданных автором. Также включено несколько тестовых проектов и три дополнительных главы. УДК 681.3.068+800.92С++ ББК 32.973.26-018.1 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. RUSSIAN language edition published by DMK PUBLISHERS, Copyright © 2007. Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Материал, изложенный в данной книге, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим издательство не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 978-0-321-30550-7 (англ.) ISBN 978-5-94074-442-9 («ДМК Пресс»)
Copyright © 2007, Pearson Education, Inc. © Перевод на русский язык, оформление ДМК Пресс, 2008 ISBN 978-5-9775-0196-5 («БХВ-Петербург») © Издание, БХВ-Петербург, 2008
Содержание
Предисловие ...................................................................................... 22 Цели ..................................................................................................... Предмет обсуждения ........................................................................... Организация книги ............................................................................... Дополнительные материалы ................................................................
22 23 24 25
Благодарности .................................................................................. 26 Об авторе ............................................................................................. 28 Пролог ................................................................................................... 29 Дихотомия объекта исследования ....................................................... Принципы программирования в системе UNIX ..................................... Семь признаков успешных библиотек на C++ ....................................... Эффективность ............................................................................... Понятность и прозрачность ............................................................. Выразительные возможности .......................................................... Надежность ..................................................................................... Гибкость .......................................................................................... Модульность ................................................................................... Переносимость ............................................................................... Поиск компромиссов: довольство тем, что имеешь, диалектизм и идиомы .......................................................................... Примеры библиотек ............................................................................. STLSoft ............................................................................................ Подпроекты STLSoft ........................................................................ Boost ............................................................................................... Open)RJ ........................................................................................... Pantheios ......................................................................................... recls .................................................................................................
29 30 31 31 33 34 36 37 38 38 39 40 41 41 43 43 43 44
Типографские соглашения ........................................................... 45 Шрифты ............................................................................................... . . . сравни . . . ....................................................................................... Предварительное вычисление концевого итератора ........................... Квалификация типа вложенного класса ................................................
45 45 46 46
6
Содержание
NULL ..................................................................................................... Имена параметров шаблона ................................................................ Имена типов)членов и типов в области видимости пространства имен ... Соглашения о вызове ........................................................................... Концевые итераторы ............................................................................ Пространство имен для имен из стандартной библиотеки C ................ Адаптеры классов и адаптеры экземпляров ......................................... Имена заголовочных файлов ................................................................
47 47 48 48 48 49 49 49
Часть I. Основы .................................................................................. 50 Глава 1. Стандартная библиотека шаблонов ........................ 52 1.1. Основные понятия ......................................................................... 1.2. Контейнеры ................................................................................... 1.2.1. Последовательные контейнеры ............................................. 1.2.2. Ассоциативные контейнеры ................................................... 1.2.3. Непрерывность памяти .......................................................... 1.2.4. swap ....................................................................................... 1.3. Итераторы ..................................................................................... 1.3.1. Итераторы ввода ................................................................... 1.3.2. Итераторы вывода ................................................................. 1.3.3. Однонаправленные итераторы ............................................... 1.3.4. Двунаправленные итераторы ................................................. 1.3.5. Итераторы с произвольным доступом ................................... 1.3.6. Оператор выбора члена ......................................................... 1.3.7. Предопределенные адаптеры итераторов ............................. 1.4. Алгоритмы ..................................................................................... 1.5. Объекты)функции .......................................................................... 1.6. Распределители ............................................................................
52 53 53 54 54 54 55 55 56 57 57 58 58 60 61 62 62
Глава 2. Концепции расширения STL, или Как STL ведет себя при встрече с реальным миром ........................... 63 2.1. Терминология ................................................................................ 2.2. Наборы .......................................................................................... 2.2.1. Изменчивость ........................................................................ 2.3. Итераторы ..................................................................................... 2.3.1. Изменчивость ........................................................................ 2.3.2. Обход ..................................................................................... 2.3.3. Определение характеристик на этапе компиляции ................ 2.3.4. Категория ссылок на элементы .............................................. 2.3.5. Общее и независимое состояние ........................................... 2.3.6. Не пересмотреть ли классификацию итераторов? .................
63 64 66 67 68 68 68 68 68 69
Содержание
7
Глава 3. Категории ссылок на элементы ................................ 71 3.1. Введение ....................................................................................... 3.2. Ссылки в C++ ................................................................................. 3.2.1. Ссылки на элементы STL)контейнеров ................................... 3.3. Классификация ссылок на элементы ............................................. 3.3.1. Перманентные ....................................................................... 3.3.2. Фиксированные ..................................................................... 3.3.3. Чувствительные ..................................................................... 3.3.4. Недолговечные ...................................................................... 3.3.5. Временные по значению ........................................................ 3.3.6. Отсутствующие ...................................................................... 3.4. Использование категорий ссылок на итераторы ............................ 3.4.1. Определение категории на этапе компиляции ....................... 3.4.2. Как компилятор может помочь избежать неопределенного поведения итератора ...................................................................... 3.5. Определение оператора operator )>() ........................................... 3.6. Еще о категориях ссылок на элементы ..........................................
71 71 72 73 73 74 74 76 78 79 79 79 80 81 82
Глава 4. Забавная безвременная ссылка ............................... 83 Глава 5. Принцип DRY SPOT ......................................................... 85 5.1. Принцип DRY SPOT в C++ ............................................................... 5.1.1. Константы .............................................................................. 5.1.2. Оператор dimensionof() .......................................................... 5.1.3. Порождающие функции ......................................................... 5.2. Когда в C++ приходится нарушать принцип DRY SPOT ................... 5.2.1. Родительские классы ............................................................. 5.2.2. Типы значений, возвращаемых функциями ............................ 5.3. Замкнутые пространства имен ......................................................
85 85 86 87 87 87 88 89
Глава 6. Закон дырявых абстракций ........................................ 91 Глава 7. Программирование по контракту ............................. 93 7.1. Виды контроля ............................................................................... 93 7.2. Механизмы контроля ..................................................................... 95
Глава 8. Ограничения ..................................................................... 96 8.1. Поддержка со стороны системы типов .......................................... 96 8.2. Статические утверждения ............................................................. 97
8
Содержание
Глава 9. Прокладки .......................................................................... 99 9.1. Введение ....................................................................................... 99 9.2. Основные прокладки ................................................................... 100 9.2.1. Атрибутные прокладки ......................................................... 100 9.2.2. Конвертирующие прокладки ................................................ 101 9.3. Составные прокладки .................................................................. 104 9.3.1. Прокладки строкового доступа ............................................ 104
Глава 10. Утка и гусь, или Занимательные основы частичного структурного соответствия ................................. 108 10.1. Соответствие ............................................................................. 10.1.1. Соответствие по имени ...................................................... 10.1.2. Структурное соответствие ................................................. 10.1.3. Утка и гусь .......................................................................... 10.2. Явное семантическое соответствие .......................................... 10.2.1. Концепции .......................................................................... 10.2.2. Пометка с помощью типов)членов ..................................... 10.2.3. Прокладки .......................................................................... 10.3. Пересекающееся соответствие .................................................
108 108 110 111 113 113 114 114 115
Глава 11. Идиома RAII .................................................................. 116 11.1. Изменчивость ............................................................................ 116 11.2. Источник ресурса ...................................................................... 116
Глава 12. Инструменты для работы с шаблонами ............ 118 12.1. Характеристические классы ...................................................... 12.1.1. Класс base_type_traits ......................................................... 12.1.2. Класс sign_traits .................................................................. 12.1.3. Свойства типа: мини)характеристики ................................ 12.1.4. Класс is_integral_type .......................................................... 12.1.5. Класс is_signed_type ........................................................... 12.1.6. Класс is_fundamental_type .................................................. 12.1.7. Класс is_same_type ............................................................. 12.2. Генераторы типов ...................................................................... 12.2.1. Класс stlsoft::allocator_selector ........................................... 12.3. Истинные typedef .......................................................................
118 120 121 122 122 124 124 125 126 126 127
Глава 13. Выводимая адаптация интерфейса: адаптации типов с неполными интерфейсами на этапе компиляции .................................................................... 128 13.1. Введение ................................................................................... 128
Содержание 13.2. Адаптация типов с неполными интерфейсами ........................... 13.3. Адаптация неизменяемых наборов ............................................ 13.4. Выводимая адаптация интерфейса ........................................... 13.4.1. Выбор типа ......................................................................... 13.4.2. Распознавание типа ........................................................... 13.4.3. Исправление типа .............................................................. 13.5. Применение IIA к диапазону .......................................................
9 129 130 131 132 133 134 136
Глава 14. Гипотеза Хенни, или Шаблоны атакуют! ........... 138 Глава 15. Независимые автономии друзей equal() ......... 140 15.1. Опасайтесь неправильного использования функций)друзей, не являющихся членами ......................................... 140 15.2. Наборы и их итераторы .............................................................. 143
Глава 16. Важнейшие компоненты .......................................... 144 16.1. Введение ................................................................................... 16.2. Класс auto_buffer ....................................................................... 16.2.1. Это не контейнер! .............................................................. 16.2.2. Интерфейс класса .............................................................. 16.2.3. Копирование ...................................................................... 16.2.4. Воспитанные распределители идут последними ................ 16.2.5. Метод swap() ...................................................................... 16.2.6. Производительность .......................................................... 16.3. Класс filesystem_traits ................................................................ 16.3.1. Типы)члены ........................................................................ 16.3.2. Работа со строками ............................................................ 16.3.3. Работа с именами из файловой системы ........................... 16.3.4. Операции с состоянием объектов файловой системы ....... 16.3.5. Операции управления файловой системой ........................ 16.3.6. Типы возвращаемых значений и обработка ошибок .............. 16.4. Класс file_path_buffer ................................................................. 16.4.1. Класс basic_?? .................................................................... 16.4.2. UNIX и PATH_MAX ................................................................ 16.4.3. Windows и MAX_PATH .......................................................... 16.4.4. Использование буферов .................................................... 16.5. Класс scoped_handle .................................................................. 16.6. Функция dl_call() ........................................................................
144 144 145 146 147 147 148 148 149 149 150 150 153 154 154 154 156 157 158 159 159 160
Часть II. Наборы .............................................................................. 163 Глава 17. Адаптация API glob ..................................................... 167 17.1. Введение ................................................................................... 167
10
Содержание
17.1.1. Мотивация ......................................................................... 17.1.2. API glob .............................................................................. 17.2. Анализ длинной версии ............................................................. 17.3. Класс unixstl::glob_sequence ...................................................... 17.3.1. Открытый интерфейс ......................................................... 17.3.2. Типы)члены ........................................................................ 17.3.3. Переменные)члены ............................................................ 17.3.4. Флаги ................................................................................. 17.3.5. Конструирование ............................................................... 17.3.6. Размер и доступ к элементам ............................................. 17.3.7. Итерация ............................................................................ 17.3.8. Метод init_glob_() ............................................................... 17.4. Анализ короткой версии ............................................................ 17.5. Резюме ......................................................................................
167 169 171 174 174 175 176 176 179 180 181 182 187 188
Глава 18. Интерлюдия: конфликты в конструкторах и дизайн, который не то чтобы плох, но мало подходит для беспрепятственного развития ...................... 190 Глава 19. Адаптация API opendir/readdir ............................... 193 19.1. Введение ................................................................................... 19.1.1. Мотивация ......................................................................... 19.1.2. API opendir/readdir .............................................................. 19.2. Анализ длинной версии ............................................................. 19.3. Класс unixstl::readdir_sequence .................................................. 19.3.1. Типы и константы)члены .................................................... 19.3.2. Конструирование ............................................................... 19.3.3. Методы, относящиеся к размеру и итерированию ............. 19.3.4. Методы доступа к атрибутам .............................................. 19.3.5. const_iterator, версия 1 ....................................................... 19.3.6. Использование версии 1 .................................................... 19.3.7. const_iterator, версия 2: семантика копирования ................ 19.3.8. operator ++() ....................................................................... 19.3.9. Категория итератора и адаптируемые типы)члены ............ 19.3.10. operator )>() ..................................................................... 19.3.11. Поддержка флагов fullPath и absolutePath ........................ 19.4. Альтернативные реализации ..................................................... 19.4.1. Хранение элементов в виде мгновенного снимка ............... 19.4.2. Хранение элементов в виде итератора ............................... 19.5. Резюме ......................................................................................
193 193 195 195 197 199 200 200 202 202 206 207 210 210 211 211 214 214 215 215
Глава 20. Адаптация API FindFirstFile/FindNextFile ............ 217 20.1. Введение ................................................................................... 217
Содержание 20.1.1. Мотивация ......................................................................... 20.1.2. API FindFirstFile/FindNextFile ............................................... 20.2. Анализ примеров ....................................................................... 20.2.1. Длинная версия .................................................................. 20.2.2. Короткая версия ................................................................. 20.2.3. Точки монтирования и бесконечная рекурсия .................... 20.3. Проектирование последовательности ....................................... 20.4. Класс winstl::basic_findfile_sequence .......................................... 20.4.1. Интерфейс класса .............................................................. 20.4.2. Конструирование ............................................................... 20.4.3. Итерация ............................................................................ 20.4.4. Обработка исключений ...................................................... 20.5. Класс winstl::basic_findfile_sequence_const_iterator .................... 20.5.1. Конструирование ............................................................... 20.5.2. Метод find_first_file_() ......................................................... 20.5.3. operator ++() ....................................................................... 20.6. Класс winstl::basic_findfile_sequence_value_type ......................... 20.7. Прокладки ................................................................................. 20.8. А где же шаблонные прокладки и конструкторы? ....................... 20.9. Резюме ...................................................................................... 20.10. Еще об обходе файловой системы с помощью recls .................
11 217 220 222 222 223 224 225 226 226 228 229 229 231 233 235 237 244 246 247 247 248
Глава 21. Интерлюдия: о компромиссе между эффективностью и удобством использования: обход каталогов на FTPZсервере .............................................. 249 21.1. Класс inetstl::basic_findfile_sequence .......................................... 250 21.2. Класс inetstl::basic_ftpdir_sequence ............................................ 251
Глава 22. Перебор процессов и модулей ............................. 254 22.1. Характеристики набора ............................................................. 22.2. Класс winstl::pid_sequence ......................................................... 22.2.1. Простые реализации на основе композиции ...................... 22.2.2. Получение идентификаторов процессов ............................ 22.2.3. Работа без поддержки исключений .................................... 22.3. Класс winstl::process_module_sequence ..................................... 22.4. Перебор всех модулей в системе .............................................. 22.5. Исключение системных псевдопроцессов ................................. 22.6. Когда заголовочные файлы API отсутствуют .............................. 22.7. Резюме ......................................................................................
255 255 256 257 258 259 260 261 263 264
Глава 23. Числа Фибоначчи ....................................................... 265 23.1. Введение ................................................................................... 265
12
Содержание
23.2. Последовательность чисел Фибоначчи ...................................... 23.3. Последовательность чисел Фибоначчи как STL)последовательность ................................................................... 23.3.1. Интерфейс бесконечной последовательности ................... 23.3.2. Заключим контракт ............................................................ 23.3.3. А не изменить ли тип значения? ......................................... 23.3.4. Ограничивающий тип ......................................................... 23.3.5. Возбуждать ли исключение std::overflow_error? .................. 23.4. Трудности понимания ................................................................ 23.5. Определение конечных границ .................................................. 23.5.1. Так все)таки итераторы? .................................................... 23.5.2. Диапазон, ограниченный конструктором ........................... 23.5.3. Истинные typedef’ы ............................................................ 23.6. Резюме ......................................................................................
265 266 268 269 269 270 270 271 272 272 273 276 279
Глава 24. Адаптация семейства MFCZконтейнеров CArray .................................................................................................. 280 24.1. Введение ................................................................................... 24.2. Мотивация ................................................................................. 24.3. Эмуляция std::vector .................................................................. 24.4. Размышления над проектом ...................................................... 24.4.1. Семейство контейнеров)массивов в MFC .......................... 24.4.2. Класс CArray_traits .............................................................. 24.4.3. Проектирование адаптеров массивов ................................ 24.4.4. Абстрактное манипулирование состоянием ....................... 24.4.5. Идиома копирования с обменом ........................................ 24.4.6. Композиция интерфейса набора ........................................ 24.4.7. Педагогический подход ...................................................... 24.5. Интерфейс класса mfcstl::CArray_adaptor_base ......................... 24.6. Класс mfcstl::CArray_cadaptor .................................................... 24.6.1. Объявление шаблона и наследование ................................ 24.6.2. Применение паттерна CRTP ............................................... 24.6.3. Конструирование ............................................................... 24.6.4. operator []() ........................................................................ 24.7. Класс mfcstl::CArray_iadaptor ..................................................... 24.8. Конструирование ....................................................................... 24.9. Распределитель памяти ............................................................. 24.10. Методы доступа к элементам .................................................. 24.11. Итерация ................................................................................. 24.11.1. Методы begin() и end() ...................................................... 24.11.2. Методы rbegin() and rend() ................................................ 24.12. Размер ..................................................................................... 24.12.1. Оптимизация выделения памяти ...................................... 24.13. Емкость....................................................................................
280 280 283 284 285 286 287 288 288 290 290 291 292 293 294 295 297 297 298 299 299 300 300 301 301 303 305
Содержание 24.14. Сравнение ............................................................................... 24.15. Модификаторы ........................................................................ 24.15.1. Метод push_back() ............................................................ 24.15.2. Метод assign() .................................................................. 24.15.3. Методы pop_back() и clear() .............................................. 24.15.4. Метод erase() ................................................................... 24.15.5. Метод insert() ................................................................... 24.16. Присваивание и метод swap() .................................................. 24.16.1. Метод swap() .................................................................... 24.17. Резюме .................................................................................... 24.18. На компакт)диске ....................................................................
13 307 310 310 311 312 313 314 316 316 318 319
Глава 25. Карта окружающей местности .............................. 320 25.1. Введение ................................................................................... 25.2. Мотивация ................................................................................. 25.3. getenv(), putenv(), setenv()/unsetenv() и environ .......................... 25.4. Класс platformstl::environment_variable_traits .............................. 25.5. Планирование интерфейса ........................................................ 25.6. Поиск по имени .......................................................................... 25.6.1. Вариант 1: возврат фиксированной/недолговечной ссылки на кэшированный объект с актуальным значением .............. 25.6.2. Вариант 2: возврат фиксированной ссылки на кэшированный объект, содержащий значение на момент снимка .......................................................................... 25.6.3. Вариант 3: возврат фиксированной ссылки на кэшированный объект с актуальным значением ........................ 25.6.4. Вариант 4: возврат временной по значению ссылки на актуальное значение ..................................................... 25.6.5. Еще раз о поиске по имени ................................................ 25.7. Вставка, изменение и удаление значений по имени .................. 25.8. Итерация ................................................................................... 25.8.1. Версия 1: непрерывный итератор ...................................... 25.8.2. Версия 2: двунаправленный итератор ................................ 25.8.3. Версия 3: мгновенный снимок ............................................ 25.8.4. Версия 4: снимок с подсчетом ссылок ................................ 25.9. Окончательная реализация итерации ........................................ 25.9.1. Изменяемый снимок? ......................................................... 25.9.2. Создание снимка ............................................................... 25.9.3. Вложенный класс const_iterator .......................................... 25.9.4. Метод insert() ..................................................................... 25.9.5. Метод erase() ..................................................................... 25.9.6. Методы operator []() и lookup() ........................................... 25.9.7. Вложенный класс snapshot ................................................. 25.10. Гетерогенные категории ссылок? ............................................
320 320 321 322 325 325 327
328 329 330 331 331 332 332 333 336 338 340 341 342 343 344 346 348 349 350
14
Содержание
25.11. Метод size() и индексирование числом .................................... 351 25.12. Резюме .................................................................................... 351 25.13. На компакт)диске .................................................................... 352
Глава 26. Путешествие по ZZплоскости – туда и обратно ........................................................................................... 353 26.1. Пролог ....................................................................................... 26.2. Введение ................................................................................... 26.3. Версия 1: однонаправленная итерация ..................................... 26.3.1. Класс zorder_iterator, версия 1 ............................................ 26.3.2. Класс window_peer_sequence, версия 1 .............................. 26.4. Версия 2: двунаправленная итерация ........................................ 26.5. Учет внешних изменений ........................................................... 26.5.1. Класс stlsoft::external_iterator_invalidation ........................... 26.6. Класс winstl::child_window_sequence .......................................... 26.7. Блюз, посвященный двунаправленным итераторам .................. 26.7.1. О стражах end() .................................................................. 26.7.2. Убийственное двойное разыменование ............................. 26.7.3. Когда двунаправленный итератор не является однонаправленным, но оказывается обратимым и клонируемым .. 26.8. winstl::zorder_iterator: итератор, обратный самому себе ............ 26.8.1. Характеристический класс для zorder_iterator .................... 26.8.2. Шаблон zorder_iterator_tmpl<> ........................................... 26.8.3. Семантика обратной итерации ........................................... 26.9. Завершающие штрихи в реализации последовательностей равноправных окон ............................................................................ 26.10. Резюме .................................................................................... 26.11. Еще о Z)плоскости ...................................................................
353 353 356 356 357 358 360 361 362 363 363 364 367 368 369 371 374 375 376 376
Глава 27. Разбиение строки ....................................................... 378 27.1. Введение ................................................................................... 27.2. Функция strtok() ......................................................................... 27.3. Класс SynesisSTL::StringTokeniser .............................................. 27.4. Случаи, когда применяется разбиение строки ........................... 27.5. Альтернативные средства разбиения строк ............................... 27.5.1. Функция strtok_r() ............................................................... 27.5.2. Библиотека IOStreams ........................................................ 27.5.3. Функция stlsoft::find_next_token() ....................................... 27.5.4. Класс boost::tokenizer ......................................................... 27.6. Класс stlsoft::string_tokeniser ..................................................... 27.6.1. Класс stlsoft::string_tokeniser::const_iterator ....................... 27.6.2. Выбор категории ................................................................ 27.6.3. Класс stlsoft::string_tokeniser_type_traits .............................
378 379 381 383 384 384 384 385 385 385 388 390 391
Содержание 27.6.4. Класс stlsoft::string_tokeniser_comparator ........................... 27.7. Тестирование ............................................................................ 27.7.1. Одиночный символ)разделитель ........................................ 27.7.2. Разделитель)строка ........................................................... 27.7.3. Сохранение пустых лексем ................................................. 27.7.4. Копировать или сослаться: поговорим о представлениях .. 27.8. Немного о политиках ................................................................. 27.8.1. Переработка параметров шаблона с помощью наследования ................................................................................ 27.8.2. Шаблоны)генераторы типов .............................................. 27.8.3. Как быть с гипотезой Хенни? .............................................. 27.9. Производительность ................................................................. 27.10. Резюме ....................................................................................
15 392 394 394 395 396 396 399 400 401 402 402 405
Глава 28. Адаптация энумераторов COM ............................. 406 28.1. Введение ................................................................................... 28.2. Мотивация ................................................................................. 28.2.1. Длинная версия .................................................................. 28.2.2. Короткая версия ................................................................. 28.3. Энумераторы COM .................................................................... 28.3.1. Метод IEnumXXXX::Next() .................................................... 28.3.2. Метод IEnumXXXX::Skip() .................................................... 28.3.3. Метод IEnumXXXX::Reset() .................................................. 28.3.4. Метод IEnumXXXX::Clone() .................................................. 28.3.5. Различные типы значений .................................................. 28.4. Анализ длинной версии ............................................................. 28.5. Класс comstl::enumerator_sequence ........................................... 28.5.1. Открытый интерфейс ......................................................... 28.5.2. Типы и константы)члены .................................................... 28.5.3. Политики значений ............................................................. 28.5.4. Переменные)члены ............................................................ 28.5.5. Конструирование ............................................................... 28.5.6. Методы итерации ............................................................... 28.5.7. Методы итератора и корректность относительно const ...... 28.5.8. Нарушена семантика значения? ......................................... 28.6. Класс comstl::enumerator_sequence::iterator .............................. 28.6.1. Конструирование ............................................................... 28.6.2. Методы итерации ............................................................... 28.6.3. Метод equal() ..................................................................... 28.7. Класс comstl::enumerator_sequence:: iterator:: enumeration_ context ................................................................................................ 28.7.1. Зачем нужен контекст обхода? ........................................... 28.7.2. Определение класса .......................................................... 28.7.3. Конструирование ...............................................................
406 406 407 408 409 409 409 409 410 410 411 412 413 414 414 417 417 419 420 421 421 423 424 424 426 426 427 428
16
Содержание
28.7.4. Вспомогательные методы для поддержки итераторов ....... 28.7.5. Инвариант .......................................................................... 28.8. Политики клонирования итераторов .......................................... 28.8.1. Класс comstl::input_cloning_policy ....................................... 28.8.2. Класс comstl::forward_cloning_policy ................................... 28.8.3. Класс comstl::cloneable_cloning_policy ................................ 28.9. Выбор политики клонирования по умолчанию: применение принципа наименьшего удивления ................................. 28.9.1. Метод empty() .................................................................... 28.10. Резюме .................................................................................... 28.10.1. Почему по умолчанию не указывать однонаправленные итераторы? ..................................................... 28.10.2. Почему по умолчанию не указывать итераторы ввода? .... 28.10.3. Почему не ограничиться порциями размером 1? ............. 28.10.4. Почему не воспользоваться стандартным контейнером? ... 28.11. Следующий шаг .......................................................................
432 433 434 435 437 438 438 443 443 444 444 444 445 445
Глава 29. Интерлюдия: исправление мелких упущений, касающихся выведения типаZчлена ................ 446 Глава 30. Адаптация наборов COM ......................................... 448 30.1. Введение ................................................................................... 30.2. Мотивация ................................................................................. 30.2.1. Длинная версия .................................................................. 30.2.2. Короткая версия ................................................................. 30.3. Класс comstl::collection_sequence .............................................. 30.3.1. Открытый интерфейс ......................................................... 30.3.2. Типы и константы)члены .................................................... 30.3.3. Конструирование ............................................................... 30.3.4. Итерация: чистое применение грязного трюка ................... 30.3.5. Замечание по поводу метода size() .................................... 30.4. Политики получения энумератора ............................................. 30.5. Резюме ......................................................................................
448 448 448 451 451 452 453 453 454 456 457 460
Глава 31. Ввод/вывод с разнесением и сбором ................ 461 31.1. Введение ................................................................................... 31.2. Ввод/вывод с разнесением и сбором ........................................ 31.3. API ввода/вывода с разнесением и сбором ............................... 31.3.1. Линеаризация с помощью COM)потоков ............................ 31.3.2. Класс platformstl::scatter_slice_sequence – рекламный трейлер ....................................................................... 31.4. Адаптация класса ACE_Message_Queue ..................................... 31.4.1. Класс acestl::message_queue_sequence, версия 1 ..............
461 461 463 463 465 468 469
Содержание 31.4.2. Класс acestl::message_queue_sequence::iterator................. 31.5. О том, как садиться на ежа ........................................................ 31.5.1. Кэп, эта посудина не может идти быстрее! ......................... 31.5.2. Класс acestl::message_queue_sequence, версия 2 .............. 31.5.3. Специализация стандартной библиотеки ........................... 31.5.4. Производительность .......................................................... 31.6. Резюме ......................................................................................
17 470 473 474 475 477 479 480
Глава 32. Изменение типа возвращаемого значения в зависимости от аргументов .................................................... 481 32.1. Введение ................................................................................... 32.2. Одолжим рубин у Ruby ............................................................... 32.3. Двойственная семантика индексирования на C++ ..................... 32.4. Достижение обобщенной совместимости с помощью прокладок строкового доступа ........................................................... 32.5. Как распознать целочисленность? ............................................. 32.6. Выбор типа возвращаемого значения и перегрузки .................. 32.6.1. Запрет индексов в виде целого со знаком .......................... 32.7. Резюме ......................................................................................
481 481 483 484 485 486 487 487
Глава 33. Порча итератора извне ............................................ 488 33.1. Когерентность элемента и интерфейса ..................................... 33.2. Элементы управления Windows ListBox и ComboBox .................. 33.2.1. Гонка при выборке? ............................................................ 33.2.2. Классы listbox_sequence и combobox_sequence в библиотеке WinSTL ...................................................................... 33.3. Перебор разделов и значений реестра ...................................... 33.3.1. Так в чем проблема? .......................................................... 33.3.2. Библиотека WinSTL Registry ................................................ 33.3.3. Обработка порчи итератора извне ..................................... 33.3.4. Класс winstl::basic_reg_key_sequence ................................. 33.4. Резюме ...................................................................................... 33.5. На компакт)диске ......................................................................
488 491 492 494 497 499 502 503 505 516 516
Часть III. Итераторы ...................................................................... 517 Глава 34.Усовершенствованный класс ostream_iterator ............................................................................... 519 34.1. Введение ................................................................................... 34.2. Класс std::ostream_iterator ......................................................... 34.2.1. Тип разности void ............................................................... 34.3. Класс stlsoft::ostream_iterator .....................................................
519 520 522 522
18
Содержание
34.3.1. Прокладки, что же еще ....................................................... 34.3.2. Безопасная семантика ....................................................... 34.3.3. Совместимость с std::ostream_iterator ................................ 34.3.4. Нарушение принципов проектирования? ........................... 34.4. Определение операторов вставки в поток ................................. 34.5. Резюме ......................................................................................
524 524 526 526 527 528
Глава 35. Интерлюдия: запрет бессмысленного синтаксиса итератора вывода с помощью паттерна Dereference Proxy ........................................................................... 529 35.1. Класс stlsoft::ostream_iterator::deref_proxy ................................. 530
Глава 36. Трансформирующий итератор ............................. 532 36.1. Введение ................................................................................... 36.2. Мотивация ................................................................................. 36.2.1. Использование std::transform() .......................................... 36.2.2. Использование трансформирующего итератора ............... 36.3. Определение адаптеров итераторов ......................................... 36.3.1. Порождающие функции ..................................................... 36.3.2. Тип значения ...................................................................... 36.4. Класс stlsoft::transform_iterator .................................................. 36.4.1. Версия 1 ............................................................................. 36.4.2. Конструирование ............................................................... 36.4.3. Операторы инкремента и декремента и арифметические операции над указателями .............................. 36.4.4. Сравнение и арифметические операции ............................ 36.4.5. А проблема в том, что . . . ................................................... 36.4.6. Версия 2 ............................................................................. 36.4.7. Класс stlsoft::transform_iterator ........................................... 36.5. Составные трансформации ....................................................... 36.6. Нет ли здесь нарушения принципа DRY SPOT? ........................... 36.6.1. Использование typedef и не)временных объектов)функций ......................................................................... 36.6.2. Использование гетерогенных итераторов и алгоритмов .... 36.6.3. Носите, но аккуратно .......................................................... 36.7. Щепотка последовательностей помогает излечить…? .............. 36.8. Резюме ...................................................................................... 36.9. На компакт)диске ......................................................................
532 533 534 535 537 537 538 538 538 540 541 541 542 542 545 547 548 548 550 551 552 552 553
Глава 37. Интерлюдия: береженого бог бережет, или О выборе имен . . . ................................................................. 554 Глава 38. Итератор селекции членов ..................................... 557
Содержание 38.1. Введение ................................................................................... 38.2. Мотивация ................................................................................. 38.2.1. Алгоритм std::accumulate() ................................................. 38.3. Класс stlsoft::member_selector_iterator ....................................... 38.4. Беды порождающей функции .................................................... 38.4.1. Неизменяющий доступ к не)константному массиву .............. 38.4.2. Неизменяющий доступ к константному массиву ................ 38.4.3. Изменяющий доступ к не)константному массиву ............... 38.4.4. Неизменяющий доступ к не)константному набору с итераторами типа класса ............................................................ 38.4.5. Неизменяющий доступ к константному набору с итераторами типа класса ............................................................ 38.4.6. Изменяющий доступ к набору с итераторами типа класса . 38.4.7. Выбор константных членов ................................................. 38.5. Резюме ...................................................................................... 38.6. На компакт)диске ......................................................................
19 557 557 558 560 562 563 563 564 564 565 567 567 568 568
Глава 39. Конкатенация СZстрок .............................................. 569 39.1. Мотивация ................................................................................. 39.2. Негибкая версия ........................................................................ 39.3. Класс stlsoft::cstring_concatenator_iterator ................................. 39.4. Порождающие функции ............................................................. 39.5. Резюме ...................................................................................... 39.6. На компакт)диске ......................................................................
569 570 572 574 575 576
Глава 40. Конкатенация строковых объектов ..................... 577 40.1. Введение ................................................................................... 40.2. Класс stlsoft::string_concatenator_iterator ................................... 40.3. Гетерогенные строковые типы ................................................... 40.4. Однако . . . ................................................................................. 40.4.1. Возможность присваивания ............................................... 40.4.2. Висячие ссылки .................................................................. 40.4.3. Решение ............................................................................. 40.5. Резюме ......................................................................................
577 577 580 580 580 581 581 582
Глава 41. Характеристики адаптированных итераторов ........................................................................................ 583 41.1. Введение ................................................................................... 41.2. Класс stlsoft::adapted_iterator_traits ........................................... 41.2.1. iterator_category ................................................................. 41.2.2. value_type ........................................................................... 41.2.3. difference_type .................................................................... 41.2.4. pointer ................................................................................
583 583 586 586 586 587
20
Содержание
41.2.5. reference ............................................................................. 41.2.6. const_pointer и const_reference ........................................... 41.2.7 effective_reference и effective_const_reference ...................... 41.2.8. effective_pointer и effective_const_pointer ............................ 41.2.9. Использование характеристического класса ..................... 41.3. Резюме ...................................................................................... 41.4. На компакт)диске ......................................................................
587 588 589 589 590 590 591
Глава 42. Фильтрующая итерация .......................................... 592 42.1. Введение ................................................................................... 42.2. Неправильная версия ................................................................ 42.3. Итераторы)члены определяют диапазон ................................... 42.4. Ну и что будем делать. . . ? ......................................................... 42.5. Класс stlsoft::filter_iterator .......................................................... 42.5.1. Семантика однонаправленных итераторов ........................ 42.5.2. Семантика двунаправленных итераторов ........................... 42.5.3. Семантика итераторов с произвольным доступом ............. 42.6. Ограничение категории итераторов .......................................... 42.7. Резюме ...................................................................................... 42.8. На компакт)диске ......................................................................
592 592 593 594 595 595 597 598 599 600 600
Глава 43. Составные адаптеры итераторов ........................ 601 43.1. Введение ................................................................................... 43.2. Трансформация фильтрующего итератора ................................ 43.2.1. Порождающая функция ...................................................... 43.3. Фильтрация трансформирующего итератора ............................ 43.4. И нашим, и вашим ..................................................................... 43.5. Резюме ......................................................................................
601 601 602 603 604 604
Эпилог ................................................................................................. 605 Библиография .......................................................................... 606
Посвящается Дяде Джону, который не шутил, говоря об опасностях второго раза, Бену и Гарри, чьи просьбы «Папа, поиграй со мной» не раз освобождали меня от целого дня тяжелой работой (а заодно дважды не позволили уложиться в сроки), но прежде всего моей красавице жене Саре, без которой я мало чего смог бы добиться, да и добиваться не стоило бы. Ее поддержка в самых разных отношениях превосходит даже самые оптимистические мои ожидания.
Предисловие Мой дядя Джон – «настоящий мачо». Крепкий, с грубыми чертами лицами, рез кий в общении, ковбойского вида, он тем не менее признает право на страх. Поэто му, когда он както упомянул, что сложность второго прыжка с парашютом состо ит в том, чтобы преодолеть страх перед уже известным, я взял это на заметку. Теперь, написав две книги, я полностью подтверждаю его мысль. Решение при няться за вторую, зная, сколько впереди проблем, далось мне нелегко. Так зачем же я взвалил на себя эту ношу? Причина подробно разъясняется в прологе и, если говорить в двух словах, сво дится к попытке разрешить следующее, на первый взгляд, простое противоречие: язык C++ слишком сложен; C++ – единственный язык, достаточно мощный для моих потребностей. Наиболее ярко это противоречие проявляется при использовании и особенно при расширении стандартной библиотеки шаблонов (Standard Template Library – STL). В этой книге (и в ее еще не написанном продолжении) я хотел в концентри рованном виде представить знания и опыт, приобретенные за десять лет работы над этой трудной и в то же время манящей темой.
Цели В этой книге описывается один из способов использования и расширения биб лиотеки STL. Рассматриваются следующие темы: что такое набор и чем он отличается от контейнера; понятие категории ссылки на элемент – почему оно важно, как определяет ся, как распознается и какие с ним связаны компромиссы при проектирова нии наборов и итераторов, расширяющих STL; феномен порчи итератора извне и его влияние на проектирование совмес тимых с STL наборов; механизм выявления возможностей произвольных наборов, которые могут предоставлять или не предоставлять изменяющие операции. Даются ответы на такие вопросы: почему трансформирующий адаптер итератора должен возвращать эле менты по значению; почему фильтрующему итератору всегда нужно передавать пару итера торов; что делать, если набор изменяется в процессе обхода;
Предмет обсуждения
23
почему следует объявить вне закона бессмысленный синтаксис классов, реализующих итераторы вывода, и как воспользоваться для этой цели пат терном «Заместитель разыменования» (Dereference Proxy). Демонстрируется, как решить следующие задачи: адаптировать групповой API к наборам STL; адаптировать поэлементный API к наборам STL; обобществить состояние обхода так, чтобы удовлетворялись требования, предъявляемые итератором ввода; обойти потенциально бесконечный набор; специализировать стандартные алгоритмы для конкретных типов итерато ров с целью повышения производительности; определить безопасное, не зависящее от платформы расширение STL для обхода системного окружения, представленного в виде глобальной пере менной; адаптировать набор, копируемость итераторов которого определяется на этапе выполнения; предоставить неповторяемый доступ к обратимому набору; записывать в буфер символов с помощью итератора. В книге рассматриваются эти и многие другие вопросы. Мы также обсудим, как можно создать универсальную совместимую со стандартом библиотеку, не жертвуя надежностью, гибкостью и особенно производительностью. Будет рас сказано, как безболезненно совместить абстракцию с эффективностью. Эту книгу стоит прочитать тем, кто хочет: изучить принципы и методы расширения библиотеки STL; больше узнать об STL, заглянув внутрь реализации расширений; узнать об общих способах реализации оберток вокруг API операционной системы и библиотек, написанных для одной конкретной технологии; узнать, как пишутся адаптеры итераторов, и разобраться в том, почему на их реализацию и использование налагаются определенные ограничения; познакомиться с техникой оптимизации производительности библиотек общего назначения; научиться применять проверенные временем компоненты для расширения STL.
Предмет обсуждения Я полагаю, что каждый должен писать о том, что знает. Поскольку основная цель этой книги состоит в том, чтобы поделиться знаниями о процедуре расшире ния STL и возникающих при этом сложностях, большая часть материала основана на моей работе над (открытыми) библиотеками STLSoft. Тот факт, что почти все вошедшее в эти библиотеки написано мной с нуля, позволяет мне говорить авто
24
Предисловие
ритетно. Это особенно важно при обсуждении ошибок проектирования; если бы я стал публично описывать чужие ошибки, вряд ли заслужил бы похвалу. Но это не означает, что всякий, кто прочтет эту книгу, обязательно свяжет себя только с STLSoft или что поклонники других библиотек не узнают из нее ничего полезного для себя. Я собирался не воспевать какуюто конкретную биб лиотеку, а рассказать о внутренних механизмах расширения STL, обращая особое внимание на общие принципы и методы, которые не зависят от интимного зна комства с STLSoft или с какойлибо другой библиотекой. Если, прочитав эту кни гу, вы не станете пользоваться библиотекой STLSoft, я не обижусь. Главное, что бы вы унесли с собой знания о том, как реализовывать и применять другие расширения STL. Я вовсе не утверждаю, что описанный мной метод расширения STL – един ственно возможный. C++ – очень мощный язык, который поддерживает самые разные стили и технику, иногда даже во вред самому себе. Например, многие, хотя и не все, наборы лучше реализовывать в виде STLнаборов, но есть и такие, для которых больше подходят автономные итераторы. Здесь существует значитель ное перекрытие и неопределенность. При обсуждении большинства расширений STL читатель (а иногда и автор!) начинает с обертывания исходного API в промежуточные, часто дефектные, клас сы и лишь постепенно приходит к оптимальной или, по крайней мере, близкой к оптимальной реализации. Я не ухожу от сложностей, неважно, касаются они реа лизации или концепции. Сразу хочу сказать, что некоторые приемы преобразова ния внешнего API в форму STL требуют незаурядной технической изобретательно сти. Я не стану ради простоты изложения притворяться, что никаких сложностей не существует, и пропускать их объяснение при описании реализации. Все будет рассмотрено подробно, и тем самым я надеюсь достичь двух целей: (1) показать, что все не так уж сложно, и (2) объяснить, почему сложность всетаки необходима. Один из лучших способов понять STL состоит в том, чтобы разобраться, как реализованы компоненты STL, а для этого лучше всего реализовать их самостоя тельно. Если у вас на это нет времени (или желания), то я рекомендую следующий по эффективности способ – прочитать эту книгу.
Организация книги Эта книга состоит из трех основных частей.
Часть I: Основы Это собрание небольших глав, закладывающих фундамент для частей II и III. Мы начнем с краткого описания основных особенностей библиотеки STL, а затем обсудим идеи и принципы расширения STL, в том числе и новое понятие катего% рии ссылок на элементы. В следующих главах рассматриваются базовые понятия, механизмы, парадигмы и принципы: совместимость, ограничения, контракты, принцип DRY SPOT, идиома RAII и прокладки (shims). И напоследок мы погово рим о технике применения шаблонов, в том числе характеристических классах
Дополнительные материалы
25
(traits) и выводимой адаптации интерфейса, а также о нескольких важных компо нентах, которые найдут применение в реализациях, описываемых в части II и III.
Часть II: Наборы Это основная часть книги. В каждой главе рассматривается один или несколь ко реальных наборов и их приведение к стандарту STL, включая и соответствую щие типы итераторов. Речь пойдет о таких разнородных материях, как обход фай ловой системы, энумераторы COM, контейнеры, не входящие в состав STL, ввод/ вывод с разнесением и сбором, и даже наборы, элементы которых могут изменять ся извне. Мы поговорим о выборе категории итератора и о категориях ссылок на элементы, об обобществлении состояния и о порче итераторов извне.
Часть III: Итераторы Если в части II речь идет об определении типа итератора, ассоциированного с набором, то часть III целиком посвящена автономным итераторам. Здесь рассмат риваются различные вопросы, как то: специализированные типы итераторов, в том числе простое расширение функциональности класса std::ostream_iterator; изощренные адаптеры итераторов, которые могут фильтровать и трансформиро вать типы или значения в тех диапазонах, к которым применяются, и т.д.
Том 2 Второй том еще не закончен и его структура окончательно не определена, но речь пойдет о функциях, алгоритмах, адаптерах, распределителях памяти и по нятиях диапазона и вида, расширяющих инструментарий STL.
Дополнительные материалы CD"ROM На прилагаемом компактдиске находятся различные бесплатные библиотеки (включая все рассматриваемые в тексте), тестовые программы, инструменты и другое полезное программное обеспечение. Включен также полный, хотя и не отредактированный, текст трех глав, не вошедших в окончательный вариант (что бы сэкономить место и избежать чрезмерной зависимости от компилятора), и многочисленные примечания и подразделы из других глав.
Онлайновые ресурсы Дополнительные материалы можно найти также на сайте http://extendedstl.com/.
Благодарности Разумеется, я очень многим обязан своей жене Саре. Во время работы над этой книгой, равно как и над предыдущей, она неизменно поддерживала меня и почти не жаловалась, несмотря на раз за разом переносимые сроки. Она надеется, что это моя последняя книга. Но поскольку брак – это искусство компромисса, то я пообещал ей, что на следующую книгу я потрачу годы (а не месяцы). Пожелайте мне удачи. Еще хочу поблагодарить свою маму и Роберта за решительную поддержку и, в особенности за терпение, с которым они относились к моим вопросам по грамма тике в любое время дня и ночи (они живут в часовом поясе GMT, а я в GMT+10). Работая над книгой, я намотал на велосипеде тысячи километров, частенько с моим (более молодым и крепким) другом Дейвом Трейси. В те дни, когда я не останавливался через каждые несколько километров, чтобы записать очередную порцию вдохновений, я катался с Дейвом, который отвлекал меня от неотвязных мыслей об STL и всячески ободрял. Спасибо, Дейв, но помни – Доктор тебя еще когданибудь обгонит! Гэрт Ланкастер выступал в самых разных ролях: советчика, клиента, друга, сотрапезника, рецензента. Гэрт, спасибо за частые, но всегда полезные, вмеша тельства в мои программы, книги, дела и меню. Встретимся на обеде у Нино! Саймон Смит – один из моих старейших друзей в стране, давшей мне приют, и исключительно толковый парень. Я знаю, что это так, отчасти потому, что его по стоянно стараются залучить все более крупные и известные компании, которые по достоинству ценят его выдающиеся способности технического руководителя, а отчасти потому, что он поручает мне анализировать свои обязанности, чтобы по нять, как еще эффективнее применить свои уникальные таланты (а, может, он просто добр ко мне). Процесс написания книги часто происходил под громкую музыку, поэтому я должен поблагодарить своих любимых фанкмузыкантов: группу 808 State, Барри Уайта, Level 42, MOS, New Order, Стиви Уондера и – разумеется, как же без него, – Fatboy Slim. Вперед, Норман, давай еще одну! И особенно хочется сказать спасибо двум артистам, чья чудесная музыка сопровождала меня на всем пути от востор женного юноши к полному энтузиазма дяде, мужу и отцу. Вопервых, Джорджу Майклу за его фанкбит и за доказательство того, что быстро только кошки родятся (в чем я и пытался убедить своего редактора на протяжении 18 месяцев со дня ис течения первого срока сдачи рукописи). И, хотя я и принадлежу к тем самым маль чикам (сдавшим все экзамены на отлично; правда, в банке Джодрелл никогда не
Благодарности
27
работал), благодарю Падди МакАлуна (Paddy MacAloon)1, который, несомненно, является величайшим лирическим певцом за последние три десятилетия. Раз уж зашла речь о редакторе, то я просто обязан выразить благодарность Питеру Гордону (Peter Gordon), который умело и непреклонно руководил мной на протяжении всего этого марафона и уговаривал «писать поменьше слов». По мощница Питера, Ким Бодигхеймер (Kim Boedigheimer), также заслуживает са мых лестных слов за умение все организовать и за терпение к моим бесконечным просьбам: то мне нужно устройство ввода, то аванс, то книжки задаром. Спасибо также Элизабет Райан (Elizabeth Ryan), Джону Фуллеру (John Fuller), Мари МакКинли (Marie McKinley) и особенно терпеливому и всепрощающему коррек тору с ласкающим вкус и обоняние именем Криста Мэдоубрук (Chrysta Meadow brooke). Теперь дошла очередь и до рецензентов, которым я бесконечно обязан: Ади Шавит (Adi Shavit), Гэрт Ланкастер (Garth Lancaster), Джордж Фразье (George Frazier), Грег Пит (Greg Peet), Nevin :) Liber, Пабло Агилар (Pablo Aguilar), Скотт Мейерс (Scott Meyers), Шон Келли (Sean Kelly), Серж Крынин (Serge Krynine) и Торстен Оттосен (Thorsten Ottosen). Никакими словами нельзя в полной мере выразить мою благодарность, поэтому, как принято, благодарю за исправление моих ошибок и принимаю на себя всю ответственность, если я гдето упрямо оста вался при своем, возможно неверном, мнении. Хочу также поблагодарить ряд людей, повлиявших на эту книгу иными спосо бами: Бьярна Страуструпа, Бьерна Карлсона (Bjorn Karlsson), Гэрта Ланкастера, Грега Комея (Greg Comeau), Кевлина Хэнни (Kevlin Henney) и Скотта Мейерса. Ценным советом или добрым словом они, пусть тонко и неощутимо, но оказали огромное влияние на окончательный вариант этой книги (и тома 2). Большое спа сибо. И наконец спасибо пользователям библиотек Pantheios и STLSoft, многие из которых предлагали помощь, высказывали критические замечания технического характера, вносили предложения и даже требовали предоставить им допечатный вариант рукописи! Надеюсь, что результат вас не разочарует.
Еще о парашютах Кстати, дядя Джон говорит, что прыгать с парашютом в третий раз уже не страшно. Учту при подготовке следующих двух книг, к которым собираюсь при ступить, как только отошлю эту в издательство. До встречи через год!
1
Цитата из песни «Technique» группы Prefab Sprout (it’s for men with horn rimmed glasses, and four distinguished «A Level» passes) (Прим. перев.)
Об авторе Мэтью Уилсон – консультант, работающий по контракту с компанией Synesis Software, разработчик библиотек STLSoft и Pantheios. Автор книги Imperfect C++ (AddisonWesley, 2004), вел колонку в журнале C/C++ Users Journal и пишет ста тьи для нескольких ведущих периодических изданий. В настоящее время прожи вает в Австралии, степень доктора философии получил в Манчестерском государ ственном университете в Великобритании.
Пролог Обречен ли каждый язык давить на барьер сложности до тех пор, пока тот оконча% тельно не рассыплется? – Адам Коннор Если использовать что%то оказывается слиш% ком трудно, я этим просто не используюсь. – Мелани Круг
Дихотомия объекта исследования Когда оставалось уже немного времени до выхода в печать моей первой книги Imperfect C++, я предложил редактору идею этой книги, уверенно заявив, что в ней не будет трудного для усвоения материала, работа над ней займет не больше шести месяцев, а уж такой тонкой окажется, что легко проскользнет между двумя слоями абстракции. Сейчас, когда я пишу эти слова, уже 20 месяцев как минул срок сдачи рукописи, и то, что представлялось мне тоненькой книжицей из 16– 20 глав, разрослось до двух томов, первый из которых содержит 43 главы и ряд интермедий (плюс еще три главы на компактдиске). Мне удалось сдержать лишь одно обещание – весь материал действительно доступен любому читателю, обла дающему некоторым опытом программирования на языке C++. Так почему же я так серьезно ошибся в оценках? Конечно, не только потому, что я программист, а наши оценки, как известно, всегда нужно умножать на π. Полагаю, тут сказались четыре фактора: 1. Библиотека STL интуитивно не очевидна. Чтобы достичь уровня комфорт ной работы с ней, необходимо приложить значительные умственные усилия. 2. Несмотря на техническую изощренность и замечательно продуманную связь отдельных частей, STL не очень хорошо приспособлена для расшире ния и применения к абстракциям, находящимся вне собственной системы понятий, которая иногда оказывается слишком узкой. 3. Язык C++ не совершенен. 4. Язык C++ сложен, но эта сложность окупается эффективностью без при несения в жертву изящества проектного решения. В последние годы C++ принялись «осовременивать», в результате чего он стал еще более мощным, но одновременно, увы, непонятным для непосвященных. Если вы написали нетривиальную библиотеку шаблонов, включающую тот или иной вид метапрограммирования, то, наверное, многому научились и создали для
30
Пролог
себя прекрасный инструмент. Однако весьма вероятно, что разобраться в нем смогут только самые настойчивые и хитроумные любители копаться в исходных текстах. Язык C++ задумывался для использования путем расширения. Если не счи тать немногих приложений, в которых C++ выступает в роли «лучшего C», то в основе применения C++ лежит определение типов: классов, перечислений, струк тур и объединений, которые стремятся сделать похожими на встроенные типы. Именно поэтому в C++ можно перегружать операторы. Так, в классе vector мож но переопределить оператор индексирования operator []() так, что он будет выглядеть (и работать), как во встроенном массиве. Но, поскольку C++ – не со вершенный, мощный и легко расширяемый язык, то он особенно подвержен напасти, которую Джоэл Спольски (Joel Spolsky) назвал «законом дырявых абст ракций». Этот закон гласит: «Все нетривиальные абстракции так или иначе про текают». Иными словами, чтобы успешно пользоваться нетривиальной абстрак цией, надо хотя бы немного знать о том, что за ней скрывается. Именно по этой причине многие программисты на C++ пишут свои собствен ные библиотеки. Дело не просто в синдроме «чужого нам не надо», а в том, что часто оказывается так, что вы понимаете и можете воспользоваться, скажем, 80 процентами написанного кемто компонента, но оставшиеся 20 остаются тай ной, покрытой мраком. Мрак этот может быть результатом излишней сложности, отхода от общепринятых идиом, неэффективности, стремления к выдающейся эффективности, ограниченности области применимости, неэлегантности дизайна или реализации, плохого стиля кодирования и так далее. И на все это могут еще накладываться практические трудности, обусловленные текущим состоянием технологии разработки компиляторов, которые особенно наглядно проявляются в сообщениях об ошибках при инстанцировании нетривиальных шаблонов. Одна из причин, по которой я оказался в состоянии написать эту книгу, зак лючается в том, что я потратил очень много времени на изучение и реализацию библиотек, относящихся к STL, а не просто принял то, что родилось в процессе стандартизации C++ (в 1998 году), или работу, проделанную другими. А решил я ее написать, потому что хотел поделиться тем, чему научился в процессе этой ра боты, не только с теми, кто желает писать расширения STL, но и с теми, кто желал бы лишь использовать уже написанные расширения, но, повинуясь закону дыря вых абстракций, вынужден время от времени заглядывать под капот.
Принципы программирования в системе UNIX В книге The Art of UNIX Programming (AddisonWesley, 2004) Эрик Раймонд (Eric Raymond) формализовал в виде набора правил сложившиеся в сообществе разработчиков для UNIX обычаи, ставшие плодом длительного и разнообразного опыта. Они помогут нам в деле адаптации STL, поэтому приведем их: Принцип ясности: ясность лучше изощренности. Принцип композиции: проектируйте компоненты так, чтобы их можно было связать между собой.
Семь признаков успешных библиотек
31
Принцип разнообразия: не доверяйте никаким претензиям на знание «единственно правильного пути». Принцип экономии: время программиста дорого, пусть лучше работает ма шина. Принцип расширяемости: проектируйте с прицелом на будущее, потому что оно настанет раньше, чем вы ожидаете. Принцип генерации: избегайте кодирования вручную; пишите программы, порождающие другие программы, когда это имеет смысл. Принцип наименьшего удивления: проектируя интерфейс, стремитесь к ин туитивной очевидности. Принцип модульности: пишите простые части, объединяемые с помощью четко сформулированных интерфейсов. Принцип наибольшего удивления: если уж приходится завершать програм му с ошибкой, делайте это как можно более шумно и чем скорее, тем лучше. Принцип оптимизации: пусть сначала заработает, оптимизировать будем потом. Принцип скаредности: пишите большие компоненты только тогда, когда убедительно продемонстрировано, что ничего другого не остается. Принцип надежности: надежность – дитя прозрачности и простоты. Принцип разделения: отделяйте политику от механизма, а интерфейс – от реализации. Принцип простоты: проектируйте так, чтобы было просто пользоваться; раскрывайте сложность внутреннего устройства лишь тогда, когда без это го не обойтись. Принцип прозрачности: проектируйте так, чтобы упростить изучение или отладку кода.
Семь признаков успешных библиотек на C++ Помимо вышеизложенных принципов, мы будем руководствоваться в этой книге (и во втором томе) еще и следующими семью признаками успешной биб лиотеки: эффективность, понятность и прозрачность, выразительные возможнос ти, надежность, гибкость, модульность и переносимость.
Эффективность Когда я закончил университет, потратив четыре года на программирование на C, Modula2, Prolog и SQL, а затем еще три года в докторантуре, программируя симуляторы оптоволоконных сетей на C++, я искренне считал себя перлом творе ния. Хуже того, я полагал, что всякий, кто пользуется какимито языками, кроме C и C++, – невежда и тупица. Ошибочность такого восприятия была не в том, что мне еще предстояло 12 лет работать, пока я наконец начал чтото понимать в C++, а в том, что я не видел достоинств других языков. Теперь я стал немного мудрее и осознаю, что для программирования есть мно жество областей применения с сильно различающимися требованиями. Время
32
Пролог
выполнения – не всегда важный фактор и уж точно не решающий (принцип эконо% мии). Куда разумнее написать административный сценарий на языке Python или (с некоторыми усилиями) на Perl или (предпочтительно) на Ruby, если это займет тридцать минут, чем потратить три дня, реализуя ту же функциональность на C++, чтобы получить 10%ный выигрыш во времени работы. Так обстоит дело во многих случаях, когда небольшое различие в производительности не существен но. Расставшись с мыслью «пиши на C++ или умри», которой был одержим много лет назад, я теперь для многих задач выбираю Ruby; и ничего – со смеху не умер и волосы не выпали. Однако C++ остается моим любимым языком, когда нужно написать надежное высокопроизводительное приложение. В общем: Если у вас не возникает необходимости или желания писать особенно эф фективный код, не программируйте на C++ и не читайте эту книгу. (Но все равно купите ее!) Можно возразить, что для выбора C++ есть и другие причины, особенно кор ректность в отношении const, строгая проверка типов на этапе компиляции, ве ликолепные средства для реализации определенных пользователем типов, обоб щенное программирование и т.д. Согласен, все это важно и во многих других языках отсутствует, но, если взвесить все многообразные аспекты программиро вания в целом, особенно принимая во внимание принципы композиции, экономии, наименьшего удивления, прозрачности и оптимизации, то становится ясно (по крайней мере, мне), что именно эффективность – первостепенный фактор, дикту ющий выбор C++. Некоторые твердокаменные противники этого языка все еще утверждают, что C++ не эффективен. Этим заблудшим душам я отвечу, что если C++ для вас слишком медленный, значит, вы просто неправильно его используе те. Есть более многочисленная группа оппонентов, утверждающая, что и на дру гих языках можно писать очень эффективные программы. Это действительно так в применении к некоторым областям, но идея о том, что какойто другой язык в настоящее время способен составить C++ конкуренцию по эффективности и широте применения, –чистой воды фантазия. Почему так надо стремиться к эффективности? Говорят, что если ученые не изобретут какието новые материалы, мы скоро исчерпаем потенциал электрон ных подложек в плане повышения производительности. Поскольку я никогда не посещал Школу Смиренных Предзнаменований, то воздержусь от поддержки этой мысли в отсутствие неопровержимых доказательств. Однако, даже если нам удастся продвигаться вперед, оставаясь в рамках неквантовых подложек, все рав но операционные системы будут становиться все более громоздкими и медленны ми, а программное обеспечение будет и дальше усложняться (по различным при чинам нетехнического характера) и использоваться во все более разнообразных физических устройствах. Эффективность важна, и от этого никуда не деться. Можно спросить, не противоречит ли такая забота об эффективности принци% пу оптимизации. Если совать ее везде и всюду, то да, противоречит. Но у библио тек обычно довольно широкий круг пользователей и долгая жизнь, поэтому все
Семь признаков успешных библиотек
33
гда найдутся приложения, для которых производительность стоит на первом мес те. Следовательно, если автор хочет, чтобы его библиотека добилась успеха, то, помимо корректности, должен уделить внимание и эффективности. Библиотека STL проектировалась как очень эффективная и при правильном применении таковой и является, что будет доказано на многочисленных приме рах ниже. Сделано это отчасти и потому, что она открыта для расширения. Но ее также очень просто использовать (или расширить) неэффективно. Следователь но, одной из основных тем этой книги будет пропаганда эффективных способов разработки библиотек на C++. И приносить за это извинения я не намерен.
Понятность и прозрачность Хотя эффективность – сильный аргумент в пользу выбора C++, но этому языку недостает двух необходимых характеристик, которые мы сочли обязательными для любой библиотеки: понятности и прозрачности. Пространное определение этих двух аспектов в общем случае приведено в книге Раймонда Art of UNIX Program% ming. А я дам собственные определения, специфичные для библиотек C++ (и C): Определение. Понятность определяется тем, насколько просто разобраться в компо) ненте до такой степени, чтобы можно было им воспользоваться.
Определение. Прозрачность определяется тем, насколько просто разобраться в компо) ненте до такой степени, чтобы можно было его модифицировать.
Понятность – это, прежде всего, мера интуитивной очевидности интерфейса компонента – его формы, непротиворечивости, ортогональности, соглашений об именовании, отношений между параметрами, имен методов, идиоматичности и т.д. Но сюда же относится документация, наличие учебных руководств, приме ров и всего, что помогает потенциальному пользователю прийти к пониманию. Понятный интерфейс легко использовать правильно, и трудно – неправильно. Прозрачность в большей степени относится к организации кода: размещению файлов, расстановке скобок, именам локальных переменных, полезным коммен тариям и т.д., хотя в этом же ряду стоит документация, описывающая реализацию, если таковая существует. Обе характеристики взаимосвязаны – если компонент непонятен, то код, в котором он используется, будет непрозрачен. Позвольте поделиться личным наблюдением. В своей профессиональной дея тельности мне пришлось разрабатывать коммерческие приложения, в которых я пользовался очень немногими открытыми библиотеками (и даже они требовали значительного усовершенствования для повышения гибкости). Во всех осталь ных случаях, где мне нужен был C++, я писал собственные библиотеки. На пер вый взгляд, это обличает меня как больного серьезной формой синдрома «чужого нам не надо»: натуральный эмпиризм, вышедший изпод контроля. Но давайте сравним это с моим отношением к библиотекам на других языках. Я пользовался десятками библиотек, написанных на C (или имеющих интерфейс для вызова из
34
Пролог
C), и был вполне доволен. Работая на других языках – D, .NET, Java, Perl, Python, Ruby, я пользуюсь чужими библиотеками без малейших колебаний. Почему так? В какойто мере это объясняется эффективностью, но чаще речь идет о по нятности и прозрачности. (И я даже не касаюсь того факта, что обещанная объек тная ориентированность на базе полиморфизма времени выполнения не достиг нута. Обуждение этого аспекта выходит за рамки данной книги, я в этом вообще не специалист и не очень интересуюсь.) Хороший программист инстинктивно следует принципу «выживает сильней ший», у него сильно развито чувство оценки плюсов и минусов при выборе компо нентов для потенциального использования. Если компонент много обещает в пла не производительности или функциональности, но совершенно непонятен, то у него возникают «дурные предчувствия» по поводу того, стоит ли брать такой компонент на вооружение. Тутто и возникает желание написать свою собствен ную библиотеку. Но если с понятностью все нормально (и даже очень хорошо), компонент все равно может быть отвергнут изза проблем с прозрачностью. Прозрачность важна по нескольким причинам. Вопервых, простой и тщательно документированный интерфейс – это, конечно, прекрасно, но как вы сможете исправить ошибки или внести изменения хоть с какойто долей уверенности в результате, если код напо минает творение безумной вязальщицы (или сумасшедшего вязальщика, если угодно. Я бы не хотел, чтобы ктото усмотрел в моих метафорах какието гендер ные предпочтения.) Вовторых, если достоинства компонента таковы, что вы го товы использовать его, несмотря на непонятность, то для того чтобы разобраться в его применении, придется заглядывать в код. Втретьих, у вас может возникнуть желание научиться самому реализовывать похожие библиотеки – вполне есте ственное в эпоху, когда исходные тексты открыты. Наконец, здравый смысл под сказывает, что если реализация выглядит плохо, то, наверное, она и работать бу дет плохо, так что возникает естественный скептицизм по отношению к другим характеристикам этого программного обеспечения и его авторов. Лично для меня понятность и прозрачность – очень важные характеристики библиотеки на C++, поэтому я так много внимания уделяю им в этой книге и в своем коде. Если вы заглянете в мои библиотеки, то обнаружите четкую (ктото скажет – педантичную) структуру, общую для всего кода, и понятную организа цию файлов. Это еще не означает, что сам код прозрачен, но я стараюсь, честное слово, стараюсь.
Выразительные возможности Язык C++ используют еще и потому, что он обладает колоссальной вырази тельностью (мощью). Определение. Под выразительностью понимают меру, характеризуемую числом пред) ложений языка, необходимых для решения конкретной задачи.
Семь признаков успешных библиотек
35
У выразительного кода три основных достоинства. Вопервых, повышается производительность труда, так как приходится писать меньше кода и на более высоком уровне абстракции. Вовторых, это способствует повторному использо ванию, что, в свою очередь, повышает надежность, так как повторно используе мые компоненты тестировались в различных контекстах. Втретьих, уменьшается число ошибок. Частота ошибок сильно зависит от числа строк кода и снижается отчасти изза меньшего числа ветвлений и отсутствия явного управления ресур сами при работе на более высоком уровне абстракции. Рассмотрим следующий фрагмент кода на C, задача которого удалить все файлы в текущем каталоге. DIR* dir = opendir("."); if(NULL != dir) { struct dirent* de; for(; NULL != (de = readdir(dir)); ) { struct stat st; if( 0 == stat(de->d_name, &st) && S_IFREG == (st.st_mode & S_IFMT)) { remove(de->d_name); } } closedir(dir); }
Полагаю, что любой компетентный программист на C, взглянув на этот код, сразу скажет, что он делает. Даже если раньше вы работали в ОС VMS или Windows и это первый в вашей практике пример кода, написанного для UNIX, вы поймете все, кроме разве что смысла констант S_IFREG и S_IFMT. Этот код совершенно прозрачен и предполагается, что API opendir/readdir понятен, с чем вряд ли кто будет спорить. Однако он не слишком выразителен. Код многословен и содержит ряд предложений управления потоком выполнения. Поэтому, если вы захотите повторить его в другом месте, внеся небольшие изменения, то каждый раз придет ся прибегать к копированию и вставке. А теперь взгляните, как это можно записать на C++/STL с помощью класса unixstl::readdir_sequence (глава 19). readdir_sequence entries(".", readdir_sequence::files); std::for_each(entries.begin(), entries.end(), ::remove);
В противоположность предыдущему примеру, здесь практически весь код непос редственно относится к решаемой задаче. Всякий, кто знаком с идиомой пары итера торов и с алгоритмами STL, согласится, что этот код понятен. Вторая строка читается так: «for_each элемента в диапазоне [entries.begin(), entries.end()) выпол нить remove()». (Здесь используется нотация для обозначения полуоткрытого интервала – открывающая квадратная скобка и закрывающая круглая. Так мы
36
Пролог
описываем все элементы, начиная с entries.begin() и кончая entries.end(), не включая последнего.) Даже если читатель ничего не знает о классе readdir_sequence и не имеет документации, этот код покажется ему прозрач ным (а, значит, интерфейс readdir_sequence понятен), если только он знает или может догадаться о смысле слова «readdir». Впрочем, у высокого уровня выразительности есть и недостатки. Вопервых, излишняя абстрактность – враг прозрачности (и потенциально понятности). По этому уровень абстрагирования должен быть относительно низок; сделаете чуть повыше и пострадает прозрачность системы в целом, даже если для конкректного компонента все нормально. Вовторых, тот факт, что такое небольшое число пред ложений выполняет потенциально много работы, может отрицательно сказаться на производительности; важно, чтобы и скрытый за ними код был эффективен. Втретьих, пользователи компонента не видят, какие возможности предоставляет абстракция. В данном случае последовательность позволяет задавать флаги для фильтрации файлов или каталогов, но нет возможности фильтровать по атрибу там или размерам файлов. Если уровень абстракции слишком высок, то кажущее ся произвольным решение предоставить одну функциональность в ущерб другой может вызывать раздражение. (И ведь неизменно все хотят разного!) Для компонентов STL есть и еще две проблемы. Вопервых, многие библиоте ки STL, включая и несколько реализаций стандартной библиотеки, написаны так, что их трудно читать, а это неблагоприятно сказывается на прозрачности. Выло вить ошибки времени выполнения иногда очень трудно. А ситуация с ошибками компиляции не лучше, а то и еще хуже. Состояние дел с диагностикой ошибок при инстанцировании шаблонов даже в самых последних компиляторах C++ таково, что при возникновении ошибок страдают и понятность, и прозрачность. Даже в относительно простых случаях сообщение об ошибке может оказаться совер шенно непостижимым. Поэтому при написании расширений STL так важно пред видеть подобные ситуации и добавлять неисполняемый код, который помог бы пользователю разобраться, в чем дело. Мы встретимся с несколькими примерами подобного рода при реализации компонентов. Кроме того, столь очевидное повышение выразительности, как в примере выше, достижимо не всегда. Часто подходящей функции не существует, а, значит, приходится писать собственный класс, что снижает выразительность (так как по являются классы, находящиеся вне области видимости), или применять адаптеры функций, а это плохо с точки зрения понятности и прозрачности. Во втором томе мы рассмотрим более сложные приемы программирования для таких случаев, но ни один из них не дает безупречного сочетания эффективности, понятности, про зрачности, гибкости и переносимости.
Надежность Если чтото не работает, успеха не будет. Язык C++ часто обвиняют в том, что его слишком легко применить во вред. Естественно, я с этим не согласен и, напро тив, утверждаю, что, если придерживаться определенной дисциплины, то про
Семь признаков успешных библиотек
37
граммы на C++ могут быть очень надежными. (Моя любимая оплачиваемая рабо та – писать сетевые серверы и наблюдать, как они годами работают без ошибок. Правда, изза этого у меня нет жирных контрактов на сопровождение. Написанное мной приложение уже передало через весь континент транзакций на миллиарды долларов без единого сбоя. И мне так и не представилась возможность получить наличными за исправление ошибок. Впрочем, так и должно быть, не правда ли?) Обеспечение надежности затрудняется при использовании шаблонов, по скольку компилятор завершает проверку только в момент инстанцирования шаб лона. Поэтому ошибки в библиотеках шаблонов могут долго оставаться незаме ченными компилятором, проявляясь лишь при некоторых специализациях. Для улучшения ситуации все библиотеки на C++ и в особенности библиотеки шабло нов должны всемерно применять технику программирования по контракту (глава 7), чтобы обнаруживать некорректные состояния и нарушение ограничений (гла ва 8) и тем самым предотвращать попытки инстанцирования с запрещенными специализациями. К надежности применимы принципы ясности, композиции, модульности, раз% деления и простоты. Ниже мы часто будет обращаться к этой теме.
Гибкость Принципы наименьшего удивления и композиции подразумевают, что компо ненты должны быть написаны для совместной работы в соответствии с ожидани ями пользователя. Шаблоны многое обещают в этом отношении, поскольку мы можем определить функциональность, применимую к инстанцированиям произ вольными типами. Классический пример – шаблон функции std::max(). template
T max(T const& t1, T const& t2);
Легко понять, какую степень обобщенности обеспечивает этот шаблон. Но со всем несложно написать код, идущий вразрез с предназначением шаблона. int i1 = 1; long l1 = 11; max(i1, l1); // Îøèáêà êîìïèëÿöèè!
Бывают и не такие явные проявления негибкости. Предположим, что вы хоти те загрузить динамическую библиотеку, используя некий класс (в данном случае гипотетический класс dynamic_library). Путь к файлу хранится в Сстроке. char const* dynamic_library
pathName = . . . dl(pathName);
Если впоследствии вы захотите создать экземпляр dynamic_library, задав путь в виде переменной типа std::string, то придется изменить обе строки, хотя логически действие осталось тем же самым. std::string const& dynamic_library
pathName = . . . dl(pathName.c_str());
38
Пролог
Это нарушение принципа композиции. Класс dynamic_library должны ра ботать с объектами string так же, как с Cстроками. Иначе пользователям будет неудобно, а получившийся код окажется уязвим к небольшим изменениям.
Модульность Если не позаботиться о модульности, то результатом станет разбухший и хрупкий монолитный каркас, недовольные пользователи, плохая производитель ность, ненадежность, ограниченность возможностей и недостаточная гибкость. (А уж о времени компиляции и говорить нечего!) Поскольку в C++ типы проверя ются статически, а модель объявления и включения унаследована от C, то ненаро ком организовать ненужную связанность довольно просто. И в C, и в C++ поддер живается опережающее объявление типов, но работает это только в том случае, когда объекты этих типов используются по указателю или по ссылке, а не по зна чению. Так как C++ поощряет применение семантики значений, возникает проти воречивая ситуация. Модульность – это та область, в которой мощь шаблонов может проявиться в полной мере, если только пользоваться ими правильно. Поскольку компилято ры применяют механизм структурного соответствия (раздел 10.1), когда пыта ются определить, допустимо ли данное инстанцирование, можно написать код, который будет работать с типами, удовлетворяющими определенным условиям, не требуя включения этих типов в область видимости. В библиотеке STL такой подход был реализован впервые, а другие последовали примеру.
Переносимость Если нет твердой уверенности в том, что текущий контекст – архитектура, операционная система, компилятор и его параметры, стандартные и дополнитель ные библиотеки и т.д. – на протяжении обозримого будущего не изменится, то автор библиотеки, претендующей на успех, должен озаботиться переносимостью. Исключения крайне редки, а, значит, на практике почти все авторы должны при ложить к этому усилия, если не хотят, чтобы их творение кануло в Лету. Достаточно всего лишь заглянуть в системные заголовки своей любимой опе рационной системы, чтобы понять, к чему ведет пренебрежение переносимостью. Но обеспечить ее не такто просто, иначе у многих толковых программистов хло пот было бы куда меньше. При написании переносимого кода нужно все время помнить о предположениях, в которых вы работаете. Диапазон весьма широк: от очевидных, например аппаратной архитектуры и операционной системы, до куда более тонких – вплоть до версий библиотек, имеющихся в них ошибок и способов их обхода. Еще один аспект переносимости – это используемый диалект C++. У боль шинства компиляторов есть параметры для избирательного включения или от ключения тех или иных особенностей языка, иными словами выбора некоторого рабочего подмножества, или диалекта, языка. Например, мелкие компоненты обычно собираются без поддержки исключений. Если иметь это в виду (и прило
Поиск компромиссов
39
жить усилия!), то понятие переносимости можно распространить и на такие слу чаи. В примерах ниже мы увидим, как это делается. Расширения STL по самой своей природе должны быть переносимыми – на чиная от работы в разных операционных системах и кончая учетом ошибок ком пилятора и диалектов языка. Поэтому на протяжении всей книги мы будем уде лять этой теме особое внимание.
Поиск компромиссов: довольство тем, что имеешь, диалектизм и идиомы Вряд ли стоит удивляться тому, что очень немногие библиотеки отмечены всеми семью признаками успешности. Наверное, это можно сказать лишь о со всем маленьких библиотеках с очень ограниченной функциональностью. Для всех прочих приходится искать подходящий компромисс. В этом разделе я опишу собственную стратегию достижения баланса. Как и в большинстве случаев, когда прагматизм отодвигает догматизм на вто рой план, единственно верного решения не существует. Никто не отрицает мощь библиотеки STL, но вместе с тем нельзя не заметить ряд недостатков: запутан ность, а иногда и полную непостижимость содержащегося в ней кода, сложность сопровождения изза необдуманных решений, неэффективность, непереноси мость. Всему этому есть несколько объяснений. Когда вы пишете библиотеку шаблонов на C++, очень легко впасть в грех изобретения собственного стиля и техники, создания только вам понятных идиом. Такой диалектизм затрудняет об мен информацией с другими программистами. Пользователь библиотеки может в конечном итоге разобраться с тем, что по началу казалось непонятным изза непривычного диалекта или неудачного ди зайна, и даже почувствовать себя относительно комфортно. Но далее он окажется в одной из потенциального бесконечного числа точек локального максимума эф фективности. Вполне возможно, что вы выбрали лучшую библиотеку для реше ния конкретной задачи и используете ее самым эффективным способом, но, быть может, куда эффективнее было бы взять какуюто другую библиотеку или приме нять выбранную иным способом. Для описания этой ситуации в английском язы ке применяется слово satisfice – «быть довольным (satisfied) тем, чего (на данный момент) достаточно (suffices)». Мне нравится приятное на слух слово satisfiction. Довольство тем, что имеешь, может вести к диалектизму, игнорированию хоро ших идиом или тому и другому вместе. Но программисты не располагают беско нечным временем для поиска оптимальных способов решения задачи, а инстру менты, которыми мы пользуемся, так громоздки и сложны, что избежать этого синдрома практически невозможно. Каждый из нас вытаптывает себе небольшую полянку в дебрях сложности, внутри которой чувствует себя вполне комфортно. А раз нам комфортно, то работа уже не кажется обескураживающе трудной. Другие люди могут принять ее, очаро ванные мощью, эффективностью и гибкостью, или оттолкнуть с отвращением, со чтя непонятной, непереносимой и сильно связанной. Распространение нашей рабо
40
Пролог
ты далее будет зависеть как от ее технических достоинств, так и от маркетинга. Если она распространится достаточно широко, то все будут воспринимать ее как вполне нормальную и простую, хотя изначально она такой не была. Быть может, лучшим примером в подтверждение этой мысли служит сама библиотека STL. Выступаете ли вы в роли автора библиотеки, пользователя или того и другого одновременно, вам необходимы механизмы, позволяющие както сосуществовать с диалектизмом и довольством малым. К числу таких механизмов относятся иди омы, антиидиомы и вскрытие черного ящика. Опытные специалисты часто забы вают, что идиома – не понятная от рождения конструкция. Правописание многих слов в английском языке интуитивно далеко не очевидно. Применение мыши для нажатия кнопок, выбора пунктов меню и прокрутки содержимого окна интуитив но не очевидно. И уж вовсе не является интуитивно очевидной никакая часть STL, да и мало какие конструкции в C++. Возьмем пример. В C++ объект любого класса по умолчанию допускает копи рование (CopyConstructible и Assignable). По моему мнению, которое разделяют многие, это ошибка. И тот факт, что программист должен принять специальные меры, чтобы сделать класс некопируемым, никак не назовешь очевидным. class NonCopyable { public: // Òèïû-÷ëåíû typedef NonCopyable class_type; . . . private: // Ðåàëèçîâûâàòü íå íàäî NonCopyable(class_type const&); class_type& operator =(class_type const&); };
Этот прием настолько широко распространен, что стал «традиционным знани ем» в C++. Это идиома. А вся библиотека STL – одна гигантская идиома. Освоив ее, вы применяете базовые конструкции STL, не задумываясь. Если некоторое расши рение STL вводит новые понятия и приемы, то понадобятся и новые идиомы. Помимо хороших идиом – старых или новых, могут существовать и антииди омы. Программист, применяющий STL, должен всеми силами избегать их. На пример, трактовка итератора как указателя – это антиидиома, которая способна только сбить с толку. Обнаружение и всемерное использование общепринятых идиом, описание новых полезных идиом, предостережение по поводу ложных антиидиом и загля дывание внутрь черного ящика – все эти тактические меры будут встречаться в этой книге (и в томе 2). С их помощью мы постараемся найти оптимальное соче тание семи признаков успешной библиотеки.
Примеры библиотек Будучи человеком практического склада ума, я предпочитаю читать книги, основанные на реальном опыте. (Подругому можно было бы сказать: «Мои мозги начинают плавиться, когда мне предлагают поселиться на абстрактной плоско
Примеры библиотек
41
сти».) Поэтому большинство примеров, приводимых в этой книге, взято из моих собственных работ, в частности из немногих проектов с открытыми исходными текстами. Циники усмехнутся при мысли, что это просто жалкая уловка для попу ляризации своих библиотек. Отчасти, может, и так, но есть и более серьезные при чины. Вопервых, я уверен, что знаю то, о чем говорю, а это для автора книги необ ходимое условие. А, вовторых, изложение на основе своей работы позволяет мне обсуждать ошибки во всей их отталкивающей неприглядности, никого не оскорб ляя и не опасаясь судебного преследования. И ничто не помешает тебе, любезный читатель, заглянуть в тексты библиотек и убедиться, что я был с тобой честен и откровенен.
STLSoft Библиотека STLSoft – это мое любимое дитя, хнычущее и брыкающееся, ко торое на протяжении последних лет пяти я вскармливал в хладных просторах C++. Она бесплатна, претендует на переносимость (между различными компиля торами и там, где возможно, между разными операционными системами), проста в использовании и, что самое главное, эффективна. Как и все мои открытые биб лиотеки, она распространяется на условиях модифицированной лицензии BSD. Простота использования и особенно расширения STLSoft обеспечивается двумя факторами. Вопервых, она состоит только из заголовочных файлов. Вам нужно лишь включить подходящий файл и поместить путь к каталогу include в список путей, которые просматривает компилятор. Вовторых, я намеренно выбрал относительно низкий уровень абстракции (принцип скаредности) и ста рался не включать в основной текст технологии и возможности, предоставляемые конкретными операционными системами (принцип простоты). Вместо этого биб лиотека разбита на ряд подпроектов, каждый из которых относится к той или иной технологии. Хотя в библиотеке есть много полезных средств для программирования как в рамках STL, так и вне их, основная цель STLSoft – предоставить универсальные компоненты и механизмы на умеренно низком уровне абстракции, которые мож но было бы использовать в коммерческих и открытых проектах. При том, что биб лиотека эффективна, гибка и переносима, сами компоненты слабо связаны между собой. Если приходилось идти на компромисс, то выразительность и богатство абстракций приносились в жертву этим характеристикам.
Подпроекты STLSoft Главный подпроект называется STLSoft, пусть это вас не смущает. В нем на ходится большая часть кода, не зависящего от платформы и технологии. Здесь вы найдете распределители памяти и адаптеры распределителей (см. том 2), алгорит мы (см. том 2), итераторы и их адаптеры (рассматриваются в части III), утилиты для работы с памятью, функции и классы для работы со строками (глава 27), клас сы (эффективные по быстродействию и использованию памяти) для определения
42
Пролог
свойств на языке C++ (см. главу 35 книги Imperfect C++), компоненты для мета программирования (главы 12, 13 и 14), прокладки (глава 9), средства для иденти фикации (и подмены) особенностей компилятора и стандартной библиотеки и многое другое. Компоненты подпроекта STLSoft находятся в пространстве имен stlsoft. Три самых крупных подпроекта – это COMSTL, UNIXSTL и WinSTL. Их компоненты находятся в пространствах имен comstl, unixstl и winstl соответ ственно. COMSTL предоставляет набор вспомогательных компонентов для рабо ты с моделью компонентных объектов (Component Object Model – COM), а также STLсовместимые адаптеры последовательностей, надстроенные над энумерато% рами и наборами COM; они описаны соответственно в главах 28 и 30. COMSTL поддерживает также одну из моих новых библиотек VOLE (она также состоит ис ключительно из заголовков и записана на компактдиске), которая предлагает на дежный, лаконичный и не зависящий от компилятора способ управления сервера ми COMавтоматизации из C++. UNIXSTL и WinSTL содержат зависящие от операционной системы и техноло гии компоненты для ОС UNIX и Windows. О некоторых из них будет рассказано в частях I и II. Они пользуются рядом структурно совместимых компонентов, напри мер: environment_variable, file_path_buffer (раздел 16.4), filesystem_traits (раздел 16.3), memory_mapped_file, module, path, performance_counter, process_mutex и thread_mutex. Все эти, а также некоторые недавно включенные компоненты, к примеру environment_map (глава 25), помещены в пространство имен platformstl и составляют подпроект PlatformSTL. Отметим, что этот под ход разительно отличается от абстрагирования различий операционных систем: в подпроект PlatformSTL включены только такие компоненты, которые струк турно совместимы настолько, чтобы можно было писать платформеннонезависи мый код, не прибегая к препроцессору. Есть и другие подпроекты, относящиеся к дополнительным технологиям. ACESTL применяет идеи STL к некоторым компонентам из популярной библио теки Adaptive Communications Environment (ACE) (глава 31). MFCSTL – это по пытка придать почтенной библиотеке Microsoft Foundation Classes (MFC) об лик, более напоминающий STL. Так, в главе 24 мы познакомимся с написанными в духе std::vector адаптерами для класса CArray. RangeLib – реализация идеи диапазона в STLSoft; диапазоны рассматриваются в томе 2. ATLSTL, InetSTL и WTLSTL – небольшие проекты, наделяющие библиотеки ATL, WinInet (сетевое программирование) и WTL чертами STL. Хотя у каждого подпроекта STLSoft (за исключением главного проекта, кото рый находится в пространстве имен stlsoft) есть отдельное пространстве имен верхнего уровня, на самом деле это псевдонимы пространств имен, вложенных в stlsoft. Например, пространство имен comstl определено в заголовочном файле следующим образом: // comstl/comstl.h namespace stlsoft {
Примеры библиотек
43
namespace comstl_project { . . . // êîìïîíåíòû COMSTL } // ïðîñòðàíñòâî èìåí comstl_project } // ïðîñòðàíñòâî èìåí stlsoft namespace comstl = ::stlsoft::comstl_project;
Все остальные компоненты COMSTL, определенные в других заголовочных файлах (каждый из которых включает файл ), помещают свои компоненты в пространство имен stlsoft::comstl_project, но клиентско му коду они представляются находящимися в пространстве имен comstl. В ре зультате все компоненты в пространстве имен stlsoft автоматически видимы компонентам, находящимся в фиктивном пространстве имен comstl, что избав ляет нас от необходимости печатать полностью квалифицированные имена. Та кая же техника применяется и во всех остальных подпроектах. Совет. Применяйте псевдонимы пространств имен для организации иерархий про) странств имен с минимальными синтаксическими помехами клиентскому коду.
Boost Boost – это организация, ставящая себе целью разработку открытых библио тек, которые интегрируются со стандартной библиотекой и впоследствии могут быть предложены для включения в будущий стандарт. В создании библиотек уча ствуют многие разработчики, в том числе и несколько членов комитета по стан дартизации C++. Я не отношусь ни к пользователям, ни к авторам Boost, поэтому в первом томе компоненты Boost детально не рассматриваются. Мы обсудим только компонент boost::tokenizer (раздел 27.5.4). Если вы хотите узнать, как пользоваться Boost, обратите внимание на книгу Beyond the C++ Standard Library: An Introduc% tion to Boost (AddisonWesley, 2005), написанную моим другом Бьерном Карлсо ном (Bjorn Karlsson).
Open"RJ OpenRJ – это библиотека для чтения структурированного файла в формате RecordJAR. Она содержит привязки к нескольким языкам и технологиям, в том числе COM, D, .NET, Python и Ruby. В главе 32 я опишу общий механизм эмуля ции на C++ гибкой семантики оператора индексирования в языках Python и Ruby с помощью класса Record из библиотеки OpenRJ/C++.
Pantheios Pantheios – это библиотека протоколирования для C++, безопасная относи тельно типов, обобщенная, безопасная относительно потоков, атомарная и исклю% чительно эффективная. Вы платите только за то, чем пользуетесь, причем всего
44
Предисловие
один раз. В архитектуре Pantheios можно выделить четыре части: ядро, клиентс кую часть, серверную часть и прикладной уровень. На прикладном уровне приме няются прокладки строкового доступа из STLSoft (раздел 9.3.1), которые обеспе чивают безграничную обобщенность и расширяемость. Ядро агрегирует все составные части записи в протокол в единую строку и отправляет ее серверному компоненту. В качестве последнего может выступать один из готовых компонен тов или написанный вами самостоятельно. Клиентская часть анализирует серьез ность сообщения и на его основе определяет, какие сообщения следует обрабо тать, а какие – отбросить. Допускается подключение собственной клиентской части. Реализация ядра Pantheios обсуждается в главах 38 и 39, где показано, как можно воспользоваться адаптерами итераторов для применения алгоритмов к пользовательским типам.
recls recls (recursive ls) – это многоплатформенная библиотека для рекурсивного поиска в файловой системе. Она написана на C++, но предлагает также API для языка C. Подобно OpenRJ, recls содержит привязки к нескольким языкам и тех нологиям, в том числе COM, D, Java, .NET, Python, Ruby и STL. Для поиска с по мощью recls следует задать начальный каталог, образец и флаги, управляющие выполнением поиска. Это подробно описывается в разделах 20.10, 28.2, 30.2, 34.1 и 36.5. Как и Pantheios, эта библиотека пользуется различными компонентами из библиотеки STLSoft, в частности file_path_buffer, glob_sequence (глава 17) и findfile_sequence (глава 20).
Типографские соглашения Мне нет дела до того, что обо мне думают. – Питер Брок Моя английский не знать? Это возможно не быть. – Ральф Виггам, Симпсоны По большей части типографские соглашения понятны без слов, поэтому останов люсь лишь на тех, которые требуют некоторых пояснений.
Шрифты С помощью шрифтов и заглавных букв в основном тексте выделяются следу ющие сущности: API (например, API glob), êîä, понятие (например, понятие про% кладки), , библиотека (например, библиотека ACE), ëèòåðàë или ïóòü (например., "ìîÿ ñòðîêà", NULL, 123, /usr/include), Паттерн (напри мер, паттерн Facade ), принцип (например, принцип наименьшего удивления), про кладка (например, прокладка get_ptr). В листингах применяются следующие со глашения: //  ïðîñòðàíñòâå èìåí namespace_name class class_name { . . . // íå÷òî äàííîå èëè óæå âñòðå÷àâøååñÿ âûøå public: // Êëàññ Ðàçäåë Èìÿ, íàïðèìåð "Êîíñòðóèðîâàíèå" class_name() { this->something_emphasized();// ÷òî-òî òðåáóþùåå âûäåëåíèÿ something_new_or_changed_from_previous_listing(); // ÷òî-òî îòëè÷àþùååñÿ îò ïðåäûäóùåãî } . . .
. . . сравни . . . Здесь многоточием обозначен показанный ранее код, трафаретный (boiler plate) код или уже встречавшийся выше или еще подлежащий заданию список параметров шаблона. Не следует путать с лексемой … , которая используется в сиг натурах функций для обозначения переменного числа аргументов и в предложе нии catch для перехвата всех исключений.
46
Типографские соглашения
Предварительное вычисление концевого итератора Чтобы не затемнять фрагменты кода, я записывал циклы с итераторами, не вычисляя концевой итератор заранее. Другими словами, текст, который в книге выглядит так: typedef unixstl::readdir_sequence rds_t; rds_t files(".", rds_t::files); for(rds_t::const_iterator b = files.begin(); b != files.end(); ++b) { std::cout << *b << std::endl; }
я на самом деле пишу так: . . . for(rds_t::const_iterator b = files.begin(), e = files.end(); b != e; ++b) { std::cout << *b << std::endl; }
Во многих случаях это более эффективно (и никогда не снижает эффективно сти). То же самое относится к однократному вычислению размера набора с по мощью функции size(). Поэтому вопреки тому, что вы встретите далее в книге, я рекомендую следующее: Совет. Старайтесь заранее вычислять концевой итератор при обходе диапазона по ите) ратору. Старайтесь заранее вычислять размера набора при обходе по индексу.
Разумеется, лучший способ предварительного вычисления итератора – это применение соответствующего алгоритма там, где есть такая возможность: std::copy(files.begin(), files.end(), std::ostream_iterator(std::cout, "\n"));
Квалификация типа вложенного класса Я также позволил себе сократить квалификацию типов там, где это не вызыва ет неоднозначности. Например, вместо того чтобы полностью указать тип Fibonacci_sequence::const_ iterator::class_type& значения, возвращае мого методом Fibonacci_sequence::const_iterator:: operator ++(), я со кращаю запись до: class_type& Fibonacci_sequence::const_iterator::operator ++() { . . .
Имена параметров шаблона
47
Если вы пожелаете откомпилировать код, приведенный прямо в тексте книги, не забывайте о таких вольностях. Впрочем, все тестовые файлы имеются на ком пактдиске, так что заниматься такими исправлениями нет нужды.
NULL Я применяю константу NULL для указателей по двум причинам. Вопервых, что бы вам ни говорили, в C++ имеется строго типизированный NULL (см. раз дел 15.1 книги Imperfect C++). Вовторых, я полагаю, что все разговоры о том, что надо быть современным и писать 0, так как для компилятора NULL не имеет смыс ла, – полная чушь. Код пишется в первую очередь для людей и только во вторую – для компилятора.
Имена параметров шаблона Параметры шаблона обозначаются одной или двумя заглавными буквами, на пример: T (тип или тип значения), S (последовательность (sequence) или строка (string)), C (контейнер (container) или символ (character)), VP (тип политики зна чения (value policy type)) и так далее. Имена некоторых параметров приводятся полностью, например CHOOSE_FIRST_TYPE, но в коде все равно будет одна или две буквы. Тому есть две причины. Вопервых, многие слова, записываемые только заглавными буквами, определены с помощью директивы #define в заголовочных файлах других библиотек или прикладных программ. Гарантирую, что раз на ткнувшись на такое, вы приложите все усилия, чтобы больше не наступить на те же грабли! Напротив, можно предположить, что слова, содержащие всего одну или две буквы не определяются с помощью #define. Если вы столкнетесь с биб лиотекой, где определены одно или двухбуквенные символы, можете с чистой совестью ее выбрасывать. Вторая причина заключается в том, что C++ запрещает создавать типчлен с таким же именем, как у параметра шаблона. Иначе говоря, следующий код не корректен: template struct thing { typedef iterator iterator; // Îøèáêà êîìïèëÿöèè };
Мне кажется, что запись: template struct thing { typedef I };
iterator;
выглядит гораздо понятнее, чем:
48
Типографские соглашения
template struct thing { typedef Iterator iterator; };
Почти всегда я сразу же использую короткие имена параметров шаблона для определения типовчленов, так что в определении шаблонного класса они уже не встречаются. Это особенно важно, потому что подчеркивает отсутствие ти повчленов, которые компилятор может не видеть очень долго (а когда увидит, это может стать причиной досадной ошибки). Если же параметр шаблона обо значен одной буквой, то в определении шаблонного класса придется первым де лом определить все эти типычлены, и это хорошо. Если вы с этим не согласны, что ж, ваше право. Но уверяю, что это самая надежная схема, которую я смог придумать.
Имена типов"членов и типов в области видимости пространства имен Типычлены называются xyz_type за исключением таких стандартных и об щепринятых имен, как iterator, pointer и т.д.; локальные типы и типы в облас ти видимости пространства имен называются xyz_t.
Соглашения о вызове В соответствующих контекстах три принятых в Windows соглашения о вызо ве обозначаются cdecl (эквивалент соглашения о вызове, принятого в UNIX), fastcall и stdcall. Если явно не оговорено противное, все COMметоды и все функ ции, определенные в Windows API, вызываются по соглашению stdcall, а все ос тальные функции – по соглашению cdecl, хотя я всегда оговариваю, какое согла шение о вызове применяется, если это имеет отношение к обсуждаемой теме. Для обозначения соглашений о вызове всегда применяются ключевые слова, зарезер вированные компилятором: _cdecl, _fastcall и _stdcall; в других компилято рах для тех же целей могут использоваться другие ключевые слова.
Концевые итераторы В стандарте C++ значения, возвращаемые итераторами end(), rend() и экви валентными им, называются «значениями за концом». Для простоты я буду пользоваться термином концевые итераторы. Ведь не называются же эти функ ции past_the_end() и rpast_the_end(). Когда речь идет о завершении обхода по итератору, я буду говорить, что итератор достиг точки end(), то есть такого состояния, в котором сравнение на равенство с итератором, возвращаемым мето дом end(), дает true.
Имена заголовочных файлов
49
Пространство имен для имен из стандартной библиотеки C Я не помещаю типы, имена функций и другие зарезервированные для стан дартной библиотеки C имена в пространство имен std. Это экономит место в при мерах кода, но и в реальной практике я поступаю точно так же. Можно с уверенно стью сказать, что имена size_t или strlen никогда не будут исключены из глобального пространства имен, поэтому полная запись в виде std::size_t и std::strlen неуместна и только отвлекает внимание. (По крайней мере, для меня это так, у других авторов может быть иная точка зрения.)
Адаптеры классов и адаптеры экземпляров В некоторых публикациях по паттернам проектирования употребляются тер мины Adaptor и Object Adaptor, когда говорят соответственно об адаптации типа на этапе компиляции и экземпляра типа на этапе выполнения. Ни один из этих терминов мне не нравится. Первый слишком общий, а во втором встречается сло во object, которое и так слишком перегружено в литературе по разработке про граммного обеспечения. Я предпочитаю термины Class Adaptor (адаптер класса) и Instance Adaptor (адаптер экземпляра), ими и буду пользоваться в тексте книги.
Имена заголовочных файлов Имена заголовочных файлов всегда заключаются в угловые скобки. Так дела ется для того, чтобы избежать неоднозначности при упоминании (неудачно на званных) стандартных заголовков. Другими словами, для ссылки на заголовоч ный файл я пишу , а не algorithm.
Часть I. Основы Шестнадцать коротеньких глав в этой части содержат базовый материал, необхо димый для чтения частей II и III (и тома 2), а именно: понятия, встречающиеся в библиотеке STL (и ее расширениях); концепции, правила, приемы и принципы, полезные при расширении STL; специальные средства и приемы работы с шаблонами, положенные в осно ву этой книги; компоненты общего назначения, используемые при реализации компонен тов, описанных в частях II и III. В главе 1 дается краткий обзор стандартной библиотеки шаблонов и, в частно сти, вводятся понятия контейнера (раздел 1.2) и итератора (раздел 1.3). Как бу дет показано далее, этих понятий недостаточно для охвата всего многообразия расширений STL. В главе 1 описываются дополнительные понятия, в частности, STL%набор (раздел 2.2). В главе 3 формулируется важное новое понятие катего% рии ссылок на элементы, необходимое для адекватного описания и надежного ма нипулирования наборами, которые не владеют содержащимися в них элемента ми. В главе 4 описывается малоизвестная особенность C++, которая оказывает как положительное, так и отрицательное влияние на те наборы STL, для которых ссылки на элементы относятся к категории временных по значению (раздел 3.3.5). Следующие семь глав посвящены вопросам программирования на C++ вооб ще и расширению STL в частности. Здесь рассматриваются принцип DRY SPOT (глава 5), закон дырявых абстракций (глава 6), программирование по контракту (глава 7), ограничения (глава 8), понятие прокладки (глава 9), правило утки, прави ло гуся и соответствие (глава 10), а также главный принцип управления ресурсами в C++ – захват ресурса есть инициализация (Resource Acquisition Is Initialization – RAII) (глава 11). Забегая чуть вперед, отметим, что в главе 14 обсуждается сниже ние понятности как плата за (предположительное) увеличение мощи посредством добавления аргументов шаблона. Эта проблема будет проявляться в нескольких расширениях, обсуждаемых в частях II и III. Глава 12 посвящена классамхарактеристикам, минихарактеристикам и про чим способам работы с шаблонами, применяемым в разных частях книги. В гла ве 13 обсуждается существенно более значимый универсальный механизм вывода типов и управления ими, который применяется для «исправления» неполных или функционально ограниченных типов, а также для определения должным образом
ограниченной функциональности в адаптированных наборах и типах итераторов. В главе 15 описывается простая техника, позволяющая избежать зависимости от одного из аспектов поведения компилятора – ошибок компиляции – путем реали зации операторов, не являющихся членами, в терминах открытых функцийчле нов, выполняющих сравнение. Завершает первую часть глава 16, в которой описаны некоторые компоненты (из библиотек STLSoft), используемые при реализации наборов и адаптеров ите раторов в частях II и III.
Глава 1. Стандартная библиотека шаблонов В настоящее время C++ – самый лучший из известных мне языков, на котором я могу выразить то, что хочу. – Александр Степанов О каждой революции можно сказать, что сначала она казалась немыслимой, а потом неизбежной. – Кэвин Билер Введение в стандартную библиотеку шаблонов (STL) могло бы стать отдельной книгой, но не той, которую я пишу. На эту тему есть несколько книг (некоторые из них упомянуты в библиографии), и я рекомендую познакомиться хотя бы с одной из них, а уж потом приступать к чтению этой.
1.1. Основные понятия В основе библиотеки STL лежат шесть понятий: контейнеры, итераторы, алго ритмы, объектыфункции, адаптеры и распределители (памяти). В контейнерах хранятся объекты. Итератор – это не зависящая от структуры данных абстрак ция, позволяющая получать доступ к элементам контейнера и обходить диапазо ны элементов. В алгоритмах итераторы применяются для обобщенного манипу лирования диапазонами элементов, ничего не зная о типах самих элементов и структурах данных, в которых они размещаются. С помощью объектов%функций определяются обобщенные операции, применяемые к элементам, которыми ма нипулируют алгоритмы. Адаптеры позволяют согласовать между собой несопос тавимые типы путем изменения их интерфейсов. Различают адаптеры класса (они адаптируют сами типы) и адаптеры экземпляра (они адаптируют экземп ляры типов). Распределители абстрагируют операции распределения памяти и конструирования объектов, выполняемые контейнерами. С использованием термина STL в мире C++ царит некая путаница. Строго го воря, он относится к обобщенной библиотеке шаблонов, которая выстроена на базе шести основных понятий и была разработана Степановым и его сотрудниками между 1980 и 1990 г.г. Но сейчас мы употребляем термин STL просто для обозначе ния того подмножества стандартной библиотеки C++, в котором используются эти шесть понятий. У этого смещения акцентов есть два следствия. Вопервых, суще
Стандартная библиотека шаблонов
53
ствует немало ревнителей чистоты C++, которые ухватятся за любую возмож ность поставить вас на место за неправильное употребление слова STL, указав при этом, что вы, вероятно, имели в виду стандартную библиотеку. Вовторых, в стан дартной библиотеке есть совместимые с STL вещи, которых в исходном варианте STL не было. Прежде всего, речь идет о потоках ввода/вывода IOStreams. В педа гогических целях я буду отмечать STLные особенности IOStreams в разных мес тах книги, а в главе 34 рассмотрю одно расширение STL, которое целиком отно сится к IOStreams. Так или иначе, я всюду буду говорить просто STL, поскольку так короче, а заниматься теми компонентами стандартной библиотеки, которые не пересекаются с оригинальной версией STL, мы не собираемся. Кроме того, сло во стандартный я хочу зарезервировать для обозначения компонентов, которые включены в стандартную библиотеку или согласуются с требованиями, сформу лированными в стандарте C++ (редакция C++03).
1.2. Контейнеры В стандарте определено четыре последовательных и четыре ассоциативных контейнера, а также три адаптера последовательных контейнеров.
1.2.1. Последовательные контейнеры Последовательный контейнер (C++03: 23.2) поддерживает строгую линей ную упорядоченность своих элементов. Три наиболее употребительных шаблон ных класса, соответствующих последовательным контейнерам, – deque, list и vector. Стандарт (C++03: 23.1.1;2) рекомендует использовать list, если выпол няются частые вставки и удаления в середине последовательности, deque – если вставки и удаления выполняются преимущественно в начале или в конце после довательности, и vector – во всех остальных случаях. Четвертый контейнерный класс basic_string служит главным образом для представления строк симво лов, но тем не менее это полноценный последовательный контейнер, так что, если вы обладаете извращенным складом ума, то можете хранить в нем не только объекты типа char или wchar_t. Определены следующие три шаблонных класса адаптеров последовательных контейнеров: queue, priority_queue и stack. Все они являются адаптерами класса, то есть могут применяться для адаптации любого типа (последовательно го контейнера), удовлетворяющего требованию CopyConstructible (C++03: 20.1.3; конструктор копирования порождает копию, эквивалентную оригиналу) и предо ставляющего небольшой набор обязательных операций. (Экземпляры типа T, удовлетворяющего требованию CopyConstructible, можно конструировать копи рованием из константных и неконстантных экземпляров T.) Например, адаптер стек stack требует, чтобы адаптируемый тип предоставлял методы empty(), size(), back(), push_back() и pop_back(), на основе которых он может реализовать собственные методы empty(), size(), top(), push() и pop(). В стандарте не определено ни одного адаптера экземпляра для контейнеров; мы столкнемся с таким в главе 24.
54
Основы
1.2.2. Ассоциативные контейнеры Ассоциативный контейнер (C++03: 23.3) позволяет искать элементы по ключу. Определены следующие ассоциативные контейнеры: map, multimap, set и multiset. Контейнеры map и multimap описывают ассоциацию между ключом и отображаемым типом. Например, специализация std::map<std::string, int> определяет отобра жение между типами std::string и int, а специализация std::set<std::wstring> описывает набор уникальных значений типа std::wstring.
1.2.3. Непрерывность памяти Среди всех контейнеров, определенных в стандартной библиотеке, только vector гарантирует непрерывность памяти, поэтому он совместим с C API. Ины
ми словами, для любого непустого экземпляра такой код корректен: extern "C" void sort_ints(int* p, size_t n); std::vector v = . . . assert(!v.empty()); sort_ints(&v[0], v.size());
тогда как следующий некорректен: std::deque d = . . . assert(!d.empty()); sort_ints(&d[0], d.size()); // Äî êðàõà îñòàëîñü íåäîëãî
И уж заодно отметим, что нельзя писать и так: std::string s1("abc"); char s2[4]; assert(!s1.empty()); ::strcpy(&s2[0], &s1[0], s1.size()); // Ðàíî èëè ïîçäíî êîí÷èòñÿ ïå÷àëüíî
Для многих пользователей класса std::string (или std::wstring) это на верняка сюрприз, особенно если учесть, что во всех известных на сегодняшний момент реализациях стандартной библиотеки для хранения строк выделяется не прерывная память. Тем не менее, стандарт этого не гарантирует, и забывать об этом не следует.
1.2.4. swap Все стандартные контейнеры предоставляют метод swap() с постоянным вре менем выполнения, который позволяет обменять внутреннее состояние экземп ляров одного и того же типа путем обмена значений переменныхчленов. std::vector vec1(100); std::vector vec2; vec1.swap(vec2); assert(0 == vec1.size()); assert(100 == vec2.size());
Стандартная библиотека шаблонов
55
Гарантируется, что метод swap() не возбуждает исключений, что облегчает написание безопасных относительно исключений компонентов в терминах стан дартных контейнеров. Многие другие классы (и не только стандартные) предос тавляют метод swap() по той же причине.
1.3. Итераторы Модель итератора построена по образцу указателей в C/C++. В общем случае к итератору можно применять операции инкремента, разыменования и сравне ния. К некоторым итераторам применимы также операции декремента и указа тельная арифметика. Категория итератора определяется набором осмысленных операций, которые можно к нему применить. В настоящее время в стандартной библиотеке определено пять категорий итераторов: итератор ввода, итератор вы вода, однонаправленный итератор, двунаправленный итератор и итератор с про извольным доступом. Часто на них ссылаются как на отдельные понятия, напри мер, говорят о понятии двунаправленного итератора.
1.3.1. Итераторы ввода Простейшая категория неизменяющих итераторов – это итераторы ввода. Такой тип поддерживает ограниченный набор операций (C++03: 24.1.1). Рас смотрим экземпляр ii типа II, моделирующего категорию итераторов ввода. У него есть конструктор по умолчанию: II ii;
Есть конструктор копирования: II ii2(ii);
Определена операция копирующего присваивания: II ii3; ii3 = ii2;
К нему можно применить операцию инкремента: ++ii2; ii3++;
// ïðåäèíêðåìåíò èëè // ïîñòèíêðåìåíò
Можно сравнить его с другим экземпляром такого же типа (взятым из того же контейнера): if(ii == ii2) {} if(ii3 != ii) {}
Если итератор не ссылается на конец контейнера, можно разыменовать его и по лучить экземпляр типа значения итератора V: II end = . . . if(end != ii)
// Ïîëó÷àåì çíà÷åíèå êîíöåâîãî èòåðàòîðà
Основы
56 { V v1 = *ii;
// Ðàçûìåíîâàíèå ii äàåò òèï, êîòîðûé ìîæíî ïðåîáðàçîâàòü â V
}
Эквивалентные экземпляры должны после разыменования давать идентичные значения: II ii5 = ii; if(end != ii) { assert(*ii5 == *ii); }
Важнейшая особенность итераторов ввода формулируется так: «никакой ал горитм не должен пытаться пройти через один и тот же итератор дважды; все ра ботающие с ними алгоритмы должны быть однопроходными» (C++03: 24.1.1;3). Это означает, что второе из следующих двух утверждений неверно: II ii6 = ii; assert(ii == ii6); ++ii; ++ii6; assert(ii == ii6);
// Âåðíî
// Íåâåðíî!
Конкретные типы поддерживают это требование разными способами. Иногда значения извлекаются из набора независимо, а иногда состояние обобществляет ся, как мы увидим в части II. Важно понимать, что однопроходность относится ко всем копиям исходного итератора. В следующем фрагменте, где II – тип итератора ввода, величины n1 и n2 или n1 и n3 никогда не будут равны (за исключением случая, когда n1 равно 0), поскольку n2 и n3 всегда равны 0. Объясняется это тем, что все три экземпляра итератора ii1, ii7 и ii8 разделяют одно и то же общее состояние. II ii7 II ii8 int n1 int n2 int n3
= = = = =
ii1; ii1; std::distance(ii7, end); std::distance(ii8, end); std::distance(ii1, end);
// Îáõîä äèàïàçîíà [ii7, end) // ii8 óæå == end // ii1 óæå == end
assert(0 == n2); assert(0 == n3);
1.3.2. Итераторы вывода Простейшая категория изменяющих итераторов – это итераторы вывода. Та кой тип поддерживает ограниченный набор операций (C++03: 24.1.2). Рассмот рим экземпляр oi типа OI, моделирующего итератор вывода. Как и в случае ите раторов ввода, мы можем конструировать его по умолчанию или копированием, выполнять операции копирующего присваивания, пред и постинкремента и сравнения. Но в отличие от итераторов ввода, разыменование не дает значения;
Стандартная библиотека шаблонов
57
результатом этой операции является l%значение, то есть нечто, что может нахо диться в левой части оператора присваивания. Следовательно, разыменование итератора вывода – это механизм вывода значения: OI oi = . . . // Ïîëó÷èòü íà÷àëî äèàïàçîíà, äîïóñêàþùåãî çàïèñü OI end = . . . // Ïîëó÷èòü çíà÷åíèå êîíöåâîãî èòåðàòîðà for(; end != oi; ++oi) { *oi = V(); // Çàïèñàòü ðåçóëüòàò âû÷èñëåíèÿ V() â *oi }
1.3.3. Однонаправленные итераторы Категория однонаправленных итераторов объединяет свойства итераторов ввода и вывода. При этом добавляется важное требование о том, что типы, моде лирующие эту категорию, могут принимать участие в многопроходных алгорит мах. Это означает, что копия итератора должна представлять тот же диапазон, что и оригинал. В следующем фрагменте, где FI – тип однонаправленного итератора, величины n1, n2 и n3 будут равны. Объясняется это тем, что у экземпляров итера торов fi1, fi2 и fi3 независимое состояние. FI fi1 = . . . FI fi2 = fi1; FI fi3 = fi1; FI end = . . . int n1 = std::distance(fi1, end); // Îáõîä äèàïàçîíà [fi1, end) int n2 = std::distance(fi2, end); // Îáõîä äèàïàçîíà [fi2, end) int n3 = std::distance(fi3, end); // Îáõîä äèàïàçîíà [fi3, end) assert(n1 == n2); assert(n1 == n3);
У этого положения есть важные для реализации расширений STL послед ствия. Чтобы поддержать семантику однонаправленного итератора, тип должен уметь получать доступ к одним и тем же элементам диапазона с помощью разных (но взаимосвязанных) экземпляров. Позже мы увидим, что при адаптации неко торых API к STL для обеспечения семантики однонаправленного итератора при ходится прилагать значительные усилия, иногда даже копировать заметный объем информации о состоянии из оригинала в копию (глава 28). Мы встретимся также со случаями, когда копии итераторов, хотя и обладают независимым состо янием, не могут гарантировать обход идентичного диапазона значений (глава 26).
1.3.4. Двунаправленные итераторы Категория двунаправленных итераторов включает все требования к однонап равленным итераторам и еще добавляет операцию декремента, позволяющую вы полнять обход диапазона в обратном порядке. BI bi = . . . BI end = . . .
58
Основы
BI begin = . . . for(bi = begin; bi != end; ++bi) // Âïåðåä {} //  ýòîé òî÷êå bi == end. Òåïåðü îáîéäåì òîò æå äèàïàçîí â îáðàòíîì ïîðÿäêå. for(; ; —bi) { if(bi == begin) { break; } }
Чтобы контейнер был обратимым, его итераторы должны поддерживать по крайней мере требования, предъявляемые к двунаправленным итераторам. Важ но отметить, что, как показано в примере выше, к концевому двунаправленному итератору также применима операция декремента, то есть он должен хранить ин формацию о том, как «вернуться назад к диапазону». Следовательно, в отличие от итератора ввода и однонаправленного итератора, экземпляр двунаправленного итератора, созданный конструктором по умолчанию, не может быть равен экземп ляру, представляющему концевой итератор. К чему это приводит, мы увидим в главах 23, 26 и 36.
1.3.5. Итераторы с произвольным доступом Категория итераторов с произвольным доступом включает все требования к двунаправленным итераторам и еще поддерживает арифметические операции над указателями. Это означает, что к экземпляру итератора можно применять операцию индексирования, а также прибавлять или вычитать смещение. RI ri = . . . RI ri2 = ri + 1; RI ri3 = ri - -1; assert(ri2 == ri3); V v1 = ri[1]; V v2 = *ri2; assert(v1 == v2);
Важно понимать, что хотя указатели удовлетворяют всем требованиям, предъявляемым к итераторам с произвольным доступом, обратное неверно: ите раторы с произвольным доступом не являются указателями, хотя к ним примени мы многие синтаксические конструкции, характерные для последних. Необходи мо помнить, что следующее важнейшее утверждение верно для указателей, но может быть ложным для итераторов с произвольным доступом: &*(p + 1) == &*p + 1;
1.3.6. Оператор выбора члена Стандарт требует, чтобы все типы итераторов, для которых типом значения является агрегат, поддерживали оператор выбора члена (стрелка): struct X {
Стандартная библиотека шаблонов
59
int x; }; some_iterator<X> si = . . . some_iterator<X> si2 = . . . some_iterator<X> end = . . . if( end != si && end != si2) { si->x = si2->x; }
Стандарт (C++03: 24.1.1;1) требует, чтобы выражение, в котором оператор «стрелка» применяется к итератору, было семантически эквивалентно выражению, в котором оператор «точка» применяется к результату применения оператора разы менования к итератору. Другими словами, it->m – то же самое, что (*it).m. К сожалению, при расширении STL встречаются ситуации (см. раздел 3.5 и главу 36), когда этот оператор поддержать невозможно, так как экземпляр ите ратора не имеет доступа к экземпляру типа значения, адрес которого он мог бы вернуть с помощью operator ->(). Кроме того, возможны неприятности при при менении этого оператора к итераторам над диапазонами так называемых интел лектуальных указателей. Пусть имеется контейнерный тип (C), в экземплярах которого хранятся эк земпляры типа интеллектуального указателя (P), который управляет временем жизни объектов, и пусть этот интеллектуальный указатель может освобождать управляемый им объект при вызове метода release(). Предположим далее, что в управляемом типе (T) также имеется метод release(), и мы хотим вызвать его для экземпляра типа итератора данного контейнера (I). Можно было бы написать такой код: C cont = . . . I it = cont.begin(); it->release();
Увы, при этом будет вызван метод P::release(), а не T::release(), поэтому (скорее всего) экземпляр T будет уничтожен. Когда мы снова попытаемся вос пользоваться контейнером cont, нас будет поджидать неприятный сюрприз. На самом деле нам требуется вот что: C cont = . . . I it = cont.begin(); it->->release();
Но в языке C++ такая конструкция не поддерживается и не без причины. (Уж конкурсы по написанию самого непонятного кода на C++ точно были бы завале ны работами участников, состязающихся в том, кто больше напишет сцепленных операторов «стрелка» в одном выражении.) Чтобы заставить C++ сделать то, что нам нужно, придется отказаться от опе ратора выбора члена и воспользоваться вместо него оператором разыменования (заключив его в скобки):
60
Основы
C cont = . . . I it = cont.begin(); (*it)->release();
Этот недостаток в синтаксисе итераторов вызывает раздражение. Когда мы будем рассматривать итераторы, для которых категория ссылок на элементы – вре% менная по значению (см. раздел 3.3.5), применение оператора «стрелка» будет источ ником горьких разочарований. Поддержка его в итераторах – не более чем синтакси ческая глазурь без какоголибо внутреннего смысла, и я полагаю, что предписание стандарта в данном случае – серьезная ошибка. В своей работе я вообще избегаю пользоваться оператором «стрелка», и за исключением пары случаев, в этой книге и в моем коде вы будете видеть только разыменование итератора с последующим вы зовом оператора «точка». Рекомендую и вам поступать таким же образом. Совет. Предпочитайте разыменование итератора и оператор «точка» ((*it).m) операто) ру «стрелка» (it->m).
1.3.7. Предопределенные адаптеры итераторов Помимо пяти категорий итераторов, в стандарте определяется также несколь ко адаптеров итераторов. std::reverse_iterator – это адаптер класса, применяемый к типу двунап равленного итератора или итератора с произвольным доступом для определения соответствующего типа обратного итератора. В листинге 1.1 показаны определе ния типов обратных итераторов для контейнерного класса std::vector. Листинг 1.1. ТипыZчлены класса std::vector, определяющие обратные итераторы //  ïðîñòðàíñòâå èìåí std template< typename T // Òèï çíà÷åíèé â êîíòåéíåðå , typename A = std::allocator > class vector { public: // Òèïû-÷ëåíû typedef ???? iterator; typedef ???? const_iterator; std::reverse_iterator reverse_iterator; std::reverse_iterator const_reverse_iterator; . . .
Почти во всех случаях, когда нужно определить тип обратного итератора для контейнеров и наборов, класс std::reverse_iterator вполне подходит. В гла ве 26 мы покажем редкое исключение из этого правила. В стандарте определены также несколько адаптеров экземпляра для ите раторов. Типы std::back_insert_iterator, std::front_insert_iterator и
Стандартная библиотека шаблонов
61
std::insert_iterator адаптируют экземпляры контейнера, предоставляя ите раторы вывода, с помощью которых в контейнер вставляются значения путем вы зова методов push_back(), push_front() и insert() соответственно. Экземпля ры итераторов возвращают порождающие функции std::back_inserter(), std::front_inserter() и std::inserter() соответственно. Порождающие функции существенно упрощают работу с шаблонами. В частности, механизм ав томатического вывода типов, необходимый для разрешения перегрузки функций, позволяет пользователю не задавать параметры шаблона явно. В части III мы уви дим, как это позволяет обойти сложные проблемы. Например, следующий код копирует элементы из вектора в список в обратном порядке: std::vector vec; . . . // Êîä, âñòàâëÿþùèé ýëåìåíòû â vec std::list lst; std::copy(vec.begin(), vec.end(), std::front_inserter(lst));
Еще один вид стандартных адаптеров экземпляра – это потоковые итераторы, которые адаптируют потоки ввода и вывода и буферы потоков, представляя их в виде итераторов ввода и вывода: std::istream_iterator, std::ostream_iterator, std::istreambuf_iterator и std::ostreambuf_iterator. В следующем примере показано, как тип std::istream_iterator можно ис пользовать для чтения набора значений из стандартного потока ввода std::cin, а тип std::ostream_iterator – для записи этого набора после сортировки в строковый поток. std::vector std::stringstream
values; sstm;
std::copy(std::istream_iterator(std::cin) , std::istream_iterator() , std::back_inserter(values)); std::sort(values.begin(), values.end()); std::copy(values.begin(), values.end() , std::ostream_iterator(sstm, " "));
// Ïðî÷èòàòü çíà÷åíèÿ
// Îòñîðòèðîâàòü çíà÷åíèÿ // Âûâåñòè
Вся часть III посвящена адаптерам итераторов.
1.4. Алгоритмы В STL алгоритмы – это обобщенные шаблоны функций, которые применяют ся к диапазонам, определяемым итераторами, и выполняют некоторые операции над самими диапазонами или над хранящимися в них элементами. Например, ал горитм std::distance() не разыменовывает переданные ему итераторы, а про сто вычисляет число элементов в представленном с их помощью диапазоне. На против, алгоритм std::transform() присваивает элементам из одного диапазона преобразованные значения элементов из другого диапазона.
Основы
62 std::vector v1 = . . . ; std::vector v2(v1.size()); std::transform(v1.begin(), v1.end(), v2.begin(), ::abs);
Хотя в этом томе приводятся многочисленные примеры применения алгорит мов, сама концепция детально обсуждается только в томе 2, где будет также рас сказано об их настройке и расширении.
1.5. Объекты"функции Объектомфункцией, или функтором называется объект, который можно вызы вать. Это может быть настоящая функция или экземпляр класса, в котором опреде лен оператор вызова. Объектыфункции используются прежде всего в сочетании с алгоритмами, где встречаются в виде предикатов или функций. Предикат прини мает один или несколько аргументов и возвращает булевское значение, позволяя тем самым управлять выбором элементом из диапазона. Функция принимает нуль или более аргументов и возвращает значение, которое можно использовать для трансформации или присваивания элементам. Объектыфункции будут рассмат риваться в томе 2; там же речь пойдет о том, как их настраивать и расширять.
1.6. Распределители Концепция распределителя (C++03: 20.1.5) описывает требования к типам, предоставляющим сервисы по управлению памятью. При этом подразумевается, что в типе определены некоторые члены, а также методы для выделения и освобож дения неформатированной памяти и конструирования (по месту) и уничтожения объектов того типа, который задан в параметре распределителя. По существу рас пределитель абстрагирует детали работы с памятью, вынося их из контейнера (и других компонентов, манипулирующих памятью) в отдельный четко опреде ленный интерфейс, за счет чего компоненты можно специализировать такими сервисами управления памятью, которые необходимы для решения конкретной задачи. Например, в следующем фрагменте показаны две специализации шаблон ного класса std::vector: неявный выбор стандартной схемы распределения па мяти и явный выбор написанного пользователем распределителя (в данном слу чае класса processheap_allocator из библиотеки WinSTL, который реализует паттерн Facade вокруг Windows Heap API). std::vector vec1; std::vector >
vec2;
Здесь память для vec1 распределяется с помощью std::allocator из свобод ной памяти C++, а для vec2 – с помощью winstl::processheap_allocator из кучи процесса Windows. Подробнее концепция распределителя будет рассмотрена в томе 2, где приве дены также примеры и приемы определения фасадов и адаптеров распределите лей, а также обсуждается интересный вопрос о распределителях, обладающих со стоянием.
Глава 2. Концепции расширения STL, или Как STL ведет себя при встрече с реальным миром Хороший закон ясен и прост и предоставля% ет исполнителям широкие полномочия при рассмотрении конкретных дел. – Профессор Рон МакКаллум Узнай, что тебе надлежит делать, и делай это. – Билли Коннолли В предыдущей главе мы рассмотрели основы STL, в том числе фундаментальные понятия контейнера, итератора, алгоритма, объектафункции, распределителя и адаптера. К сожалению, при попытке расширить STL выясняется, что некоторые из них слишком ограничительны или недостаточно детальны. В этой главе мы об судим эти понятия применительно к наборам и адаптерам итераторов, являю щимся темой данного тома.
2.1. Терминология В стандартную библиотеку вошло многое из оригинальной библиотеки STL, но (пока) не все. Например, в стандарте C++03 определены лишь ассоциатив ные контейнеры на базе деревьев, но нет контейнеров на базе хэштаблиц, имею щихся в оригинальном варианте. Впрочем, по поводу таких контейнеров уже внесены предложения, и в следующую версию стандарта они будут добавлены. Тем не менее, факт остается фактом – стандартная библиотека не является над множеством STL. С другой стороны, в стандартной библиотеке есть STLсовместимые компо ненты, которых не было в STL, а точнее IOStreams. Что бы вы ни думали о досто инствах IOStreams как серьезной библиотеки ввода/вывода, добавление к ней STLсовместимых интерфейсов – безусловное благо для C++. STL и стандартная библиотека во многом пересекаются, но все же различны. В этом томе я не стану рассматривать те компоненты STL, которые еще не вошли в стандартную библиотеку, и наоборот. Таким образом, говоря о стандартных компонентах, я имею в виду только те, которые входят и в STL, и в стандартную
64
Основы
библиотеку, и при этом подразумеваю определение, действующее для стандарт ной библиотеки. Но поскольку эта книга посвящена прежде всего расширению STL, нам понадо бится определить термины, в которых мы будем обсуждать эту тему. Под расшире нием понимается не только добавление новых контейнеров. На самом деле, боль шинство расширений, предоставляющих доступ к диапазонам элементов, вообще нельзя назвать контейнерами. Я буду называть их наборами (collection). В следую щем разделе мы обсудим понятие набора и его связь с понятием контейнера.
2.2. Наборы В библиотеке STL рассматриваются только контейнеры. Стандарт определя ет контейнер как «объект, который содержит другие объекты и управляет распре делением и освобождением памяти для них с помощью конструкторов, деструкто ров, операций вставки и удаления» (C++03: 23.1;1). Однако существует гораздо больше типов, так или иначе связанных с наборами объектов, чем подразумевает ся данным определением (см. рис. 2.1). Описанная в данной главе классификация будет использоваться далее в этой книге и в особенности в части II. Прежде всего, определим понятие набора. Определение. Набором называется множество из нуля или более элементов в совокуп) ности с интерфейсом, позволяющим получать доступ к его элементам с возможностью или без возможности изменения.
Можно выделить несколько видов наборов. Вычисляемые наборы, например математические ряды, не имеют физического воплощения. К таковым можно от нести последовательность чисел Фибоначчи (глава 23). API доступа к элементам – это способ получить доступ к элементам набора с помощью программного интерфейса, который не обязательно возвращает те же типы, что хранятся в обслуживаемом им диапазоне. Более того, способ возврата элементов вызывающей программе может не иметь ничего общего со способом их внутреннего хранения. Например, API glob в UNIX возвращает выделенный функ цией блок памяти, в котором хранятся строки, содержащие пути ко всем элемен там, соответствующим заданной маске. Это групповой API (elements%en%bloc API). Напротив, API opendir/readdir возвращает информацию о каждом элементе фай ловой системы в указанном каталоге в виде завершающейся нулем строки, по од ному за обращение. Это поэлементный API (element%at%a%time API). (Структура и функции этих API и их адаптация к наборам STL рассматриваются в главах 17 и 19 соответственно). Взяв за образец стандарт C++, дадим следующее определение контейнера. Определение. Контейнером называется набор, который владеет своими объектами и предоставляет операции для получения доступа к этим объектам, модификации их и, воз) можно, добавления, удаления и переупорядочения.
Концепции расширения STL
65
Рис. 2.1. Классификация наборов (показано пересечение с STL)
Проще говоря, вы можете считать, что контейнер – это именно то, что вы при выкли так называть, а набор – это контейнер плюс все остальное. Поскольку в этой книге нас интересует STL, то я определю еще два понятия: STLнабор и STLконтейнер. Определение. STL набором называется набор, который предоставляет изменяющие и/или неизменяющие диапазоны итераторов с помощью методов begin() и end().
Определение. STL контейнером называется контейнер, который предоставляет изме) няющие и/или неизменяющие диапазоны итераторов с помощью методов begin() и end().
Отметим, что STLконтейнер необязательно удовлетворяет всем требовани ям, предъявляемым к стандартному контейнеру; те же, которые им удовлетворя ют, можно назвать контейнерами, совместимыми со стандартом.
66
Основы
Определение. Совместимым со стандартом контейнером называется STL)контейнер, который удовлетворяет требованиям, предъявляемым к контейнеру из стандартной биб) лиотеки.
Определение. Стандартным контейнером называется совместимый со стандартом кон) тейнер, который определен в текущей версии стандартной библиотеки.
Обычно удовлетворить требованиям, предъявляемым к контейнеру, можно, добавив методы size() (и max_size()), empty(), swap(), а также несколько псевдонимов типов typedef. Однако так бывает не всегда. Наконец, определим еще два термина: один для контейнеров, которые, подоб но std::vector, хранят элементы в непрерывном блоке памяти, а другой – для наборов, которые предоставляют доступ к хранимым таким образом элементам и обладают непрерывными итераторами (раздел 2.3.6). Определение. Непрерывным контейнером называется STL)контейнер, который разме) щает элементы в непрерывной области памяти и предоставляет непрерывные итераторы.
Определение. Непрерывным набором называется STL)набор, элементы которого разме) щены в непрерывной области памяти и который предоставляет непрерывные итераторы.
Внутри обведенной пунктирной линией области на рис 2.1 мы можем приме нять понятия STL и обеспечить совместимость с STL. По определению, стандарт ные контейнеры являются в то же время STLконтейнерами. Перекрытие с други ми контейнерами, вычисляемыми наборами и API доступа к элементам (обоих типов) – это как раз то, чем будем заниматься в части II, оборачивая все виды на боров в STLсовместимые типы. Отметим, что API перебора с обратным вызовом несовместим с STL и не может быть сделан таковым. Причины будут рассмотрены в томе 2, когда мы будем гово рить о понятии диапазона. Диапазоны связаны с манипулированием целыми группами элементов и потому способны инкапсулировать все типы наборов. Тем самым это понятие является дополнительным к понятию итератора, который дает возможность манипулировать отдельными элементами.
2.2.1. Изменчивость Большинство контейнеров допускают изменение и переупорядочение содер жащихся в них элементов. Так, std::list позволяет модифицировать элементы с помощью ссылок, которые возвращают методы front() и back(), и разыменовы вать изменяющий (неconst) итератор, а также менять порядок элементов с по мощью таких операций, как insert(), erase() и sort(). Подобные контейнеры, очевидно, изменчивы. Не очевидно, однако, то, что они поддерживают две формы изменчивости, а это критически важно для понимания и классификации наборов.
Концепции расширения STL
67
Определение. Набор с изменяемыми элементами допускает модификацию своих эле) ментов. Набор с неизменяемыми элементами этого не допускает.
Определение. Набор с изменяемым порядком допускает модификацию порядка распо) ложения своих элементов. Набор с неизменяемым порядком этого не допускает.
Все совместимые со стандартом контейнеры являются наборами с изменяе мыми элементами и с изменяемым порядком. Некоторые контейнеры, например принадлежащие семейству многомерных массивов из библиотеки STLSoft (описаны в главе 33 книги Imperfect C++), допус кают модификацию элементов, но обладают фиксированной структурой, то есть все объекты создаются в конструкторе контейнера и существуют до момента унич тожения в деструкторе. Это контейнеры с неизменяемыми элементами. Теоре тически возможны контейнеры с изменяемыми элементами, но с неизменяемым порядком, хотя на практике мне с такими сталкиваться не приходилось. Однако стандартная библиотека не потворствует такой возможности. (Подобные контей неры есть в других языках, для которых характерно наличие неизменяемых типов (обычно строк); в качестве примеров можно назвать Python, Java и .NET.) Если набор не допускает ни изменения элементов, ни изменения порядка, он называется неизменяемым. Определение. Неизменяемый набор не допускает ни изменения своих элементов, ни изменения их порядка.
Термин неизменяемый контейнер внутренне противоречив, так как основная цель контейнера – возможность группировать сущности, состав которых не изве стен до момента выполнения. Однако что касается наборов, то многие, пожалуй, даже большинство, допускают только изменение элементов или только измене ние порядка. А некоторые вообще неизменяемы. Иногда это связано с тем, что элементы, к которым они предоставляют доступ, представляют собой группу, возвращаемую соответствующим API, а иногда с тем, что не существует никаких средств для модификации элементов. В частях II и III и в томе 2 мы встретимся с примерами наборов, обладающих разными формами изменчивости.
2.3. Итераторы Есть и другие аспекты, в которых расширения не согласуются с устоявшими ся концепциями STL. В предыдущей главе мы обсуждали характеристики различ ных категорий итераторов. Неприятность заключается в том, что в пяти категори ях, определенных в стандарте, многие характеристики слиты. При определении и использовании итераторов применительно к разным кон тейнерным типам эти проблемы проявляются наглядно и очевидно. Некоторые из них представляются скорее теоретическими. Но когда мы расширяем STL и вы
68
Основы
нуждены определять типы итераторов для STLнаборов, все оказывается отнюдь не так ясно, и мы с этим еще неоднократно столкнемся.
2.3.1. Изменчивость В том мире, который описывается стандартом, итераторы ввода поддержива ют только неизменяющий доступ к элементам, итераторы вывода – только изме няющий доступ, а все остальные итераторы – оба вида доступа. Но в реальном мире хотелось бы уметь определять STLнаборы, предоставляющие различные сочетания этих характеристик, например, только неизменяющие операции при поддержке двустороннего обхода.
2.3.2. Обход Экземпляр однонаправленного итератора можно копировать, и при этом по лучается экземпляр, который будет перебирать идентичный диапазон элементов. Двунаправленный итератор – это частный случай однонаправленного итератора, в котором определены еще операции для обхода диапазона в обратном порядке. Для обратимого итератора, который не может гарантировать идентичность диа пазонов в копиях, стандарт не определяет подходящей категории. Такой пример мы увидим в главе 26.
2.3.3. Определение характеристик на этапе компиляции Для всех случаев, покрываемых STL, категория итератора известна на этапе компиляции. Это справедливо и для большинства расширений STL. Но есть не сколько случаев, когда требуемое поведение набора может изменяться во время выполнения. В STL нет механизма для представления такой ситуации. В главе 28 мы рассмотрим соответствующий пример и покажем, как поступать в этом случае.
2.3.4. Категория ссылок на элементы Варианты ссылок на элементы, возвращаемые итераторами, – я называю их категориями ссылок на элементы (глава 3) – в стандарте не рассматриваются вов се. Предполагается, что итератор в любой момент может вернуть настоящую ссылку в смысле C++ на текущий элемент. Как мы увидим, бывают случаи, когда это нежелательно из соображений логики и/или эффективности. Редко, но встре чаются также ситуации, когда это вообще невозможно (в трансформирующих адаптерах итераторов). Детально эта проблема, которая затрагивает большинство наборов (часть II) и адаптеров итераторов (часть III), обсуждается в главе 3.
2.3.5. Общее и независимое состояние Тип, моделирующий итератор ввода (раздел 1.3.1), делает так только потому, что не может удовлетворить требованиям, предъявляемым к однонаправленному
Концепции расширения STL
69
итератору (раздел 1.3.3). Обычно это связано с тем, что соответствующий диапа зон можно обойти только один раз. В стандартной библиотеке есть всего два при мера таких итераторов: istream_iterator и istreambuf_iterator, и оба они однопроходные, потому что получают следующий элемент путем обращения к операции выборки того потока или буфера, над которым надстроены. Этот по ток или буфер разделяется всеми копиями данного экземпляра, что и определяет общее состояние и однопроходную природу. Однако реализация расширений STL над поэлементным API не всегда бывает такой простой. Возможно наличие состояния, связанного с выборкой, и это состоя ние должно быть общим для всех копий данного экземпляра, только при этом усло вии удастся безопасно и в полном объеме реализовать семантику итератора ввода. В главах 19 и 20 и в разделе 31.4 мы увидим, что, как это ни удивительно, но больше всего сложностей возникает с реализацией итераторов самых простых категорий.
2.3.6. Не пересмотреть ли классификацию итераторов? В идеальном мире нам хотелось бы иметь гораздо более широкое определение категорий итераторов. И в следующей главе я буду говорить о совершенно другой классификации – по категории ссылок на элементы. Варьируя основную тему, вокруг которой вращаются расширения STL, можно было бы предложить три и даже больше ортогональных характеристик. И привести соответствующие аргу менты. Даже если у такого разграничения нет особых технических достоинств – а ниже мы увидим, что категорию ссылок на элементы можно выразить в терми нах уже имеющихся характеристик (глава 41), – такой подход позволяет лучше понять проблему. Например, вместо константного однонаправленного итератора можно было бы говорить о неизменяющем клонируемом однонаправленном итера% торе. Однако, экспоненциальное увеличение числа таких характеристик, скорее всего, было бы не продуктивным, даже если бы удалось достичь согласия между практикующими программистами. Но это все академические споры. Существует стандарт, и вряд ли можно рассчитывать на радикальное изменение такой фунда ментальной концепции, как итератор. Тем не менее, для ясности я все же введу два новых термина. В применении к итераторам ввода я буду говорить о неизменяющей однопроходной семантике, дабы подчеркнуть, что для данного диапазона в каждый момент времени может существовать лишь один итератор такого типа. Другой термин – непрерывный итератор – уточняет идею итератора с произвольным доступом. Такие итерато ры обладают всеми характеристиками итератора с произвольным доступом и до полнительно удовлетворяют следующему условию: &*(it + 1) == &*it + 1;
Я уже упоминал это соотношение в разделе 1.3.5, когда говорил об отличии итератора с произвольным доступом от указателя. Подчеркну еще раз, что не все непрерывные итераторы являются указателями, так как класс, в котором перегру жен оператор взятия адреса (operator &()), тоже может поддерживать это усло вие. (В главе 26 книги Imperfect C++ я настоятельно предостерегал против пере
Основы
70
грузки этого оператора. Но несмотря на убедительные возражения, иногда это все же делают. В дополнительной главе «Остерегайтесь непрерывных итераторов, не являющихся указателями», которая имеется на компактдиске, вы увидите при мер использования этого оператора в реализации стандартной библиотеки для одного компилятора, проблемы, которые при этом возникают при попытке рас ширения STL, и пути их решения.) Наверное, вас интересует, почему так важна эта дополнительная категория. Единственный контейнер в стандартной библиотеке, о котором можно сказать, что он предоставляет непрерывные итераторы, – это std::vector. (Заметим, что мы не включаем контейнер std::valarray, о котором один рецензент сказал, что это «уб людочное отродье контейнеров из стандартной библиотеки». Я не занимаюсь чис ленными методами, поэтому, наверное, упускаю какието тонкие нюансы, но, на мой взгляд, хуже valarray может быть только std::vector, и написан он был лишь потому, что в тот момент эта мысль комуто показалась интересной. Если вы не в курсе, скажу что operator ==() для valarray не является неизменяющим оператором сравнения, возвращающим булевское значение, которое говорит, рав ны ли две сравниваемых величины. На самом деле это функция, возвращающая объект контейнера valarray, элементы которого равны true или false в зави симости от того, равны или нет соответственные элементы сравниваемых контейне ров! Хорошо это или плохо, но в этой книге обсуждать valarray мы не будем.) За пределами стандартной библиотеки можно встретить много других непре рывных итераторов, относящихся к различным наборам, расширяющим STL. На личие введенного выше термина позволит нам уточнить эту характеристику набо ра и подчеркнуть различия между итераторами с произвольным доступом и указателями (и эквивалентыми им типами класса, в которых переопределен опе ратор operator &()). Кроме того, диапазон, представленный непрерывными ите раторами, будет совместим с функцией, принимающей указатели в качестве па раметров. Рассмотрим следующие два диапазона, представленные итераторами с произвольным доступом (RI) и непрерывными итераторами (CI), для каждого из которых тип значения равен V: RI RI CI CI
r_begin r_end = c_begin c_end =
= . = .
. . . .
. . . . . .
Если дана функция use_V(): void use_V(V* pv, size_t n);
то в предположении, что диапазоны [r_begin, r_end) и [c_begin, c_end) не пусты, следующее выражение определено корректно: use_V(&*c_begin, c_end – c_begin);
а следующее – нет: use_V(&*r_begin, r_end – r_begin);
В частях II и III мы увидим несколько примеров, демонстрирующих важность различия между двумя этими категориями.
Глава 3. Категории ссылок на элементы В любой новой области знания старый спо% соб мышления оказывается как правило не% верным. – Давид Судзуки Если бы у меня была ветчина, то я мог бы приготовить яичницу с ветчиной, будь у ме% ня еще и яйца. – Л. Хантер Ловин
3.1. Введение В: Когда ссылка на элемент не является ссылкой? О: Когда элемент получен из набора, не владеющего собственными элементами. На первый взгляд, это утверждение представляется бесспорным. Но мы уви дим, что оно имеет важные и далеко идущие последствия для расширения STL. Я введу понятие категории ссылок на элементы по аналогии с уже знакомой нам концепцией категорий итераторов, которая окажется для расширения STL не менее значимой. Изучая STL, вы до сих пор не сталкивались с классификацией ссылок на эле менты по одной простой причине: среди стандартных компонентнов STL почти нет таких, для которых были бы характерны достаточно экзотические и ограничи тельные категории таких ссылок. Поэтому давайте отправимся в необходимое, хотя временами и трудное, путешествие в мир ссылок на элементы.
3.2. Ссылки в C++ В языке C++ ссылка – это альтернативное имя существующего объекта. С точки зрения расширений STL, нас будут интересовать два важных аспекта ссылок. Вопервых, ссылка – это просто имя некоего объекта, который так и назы вается: объект ссылки. Иными словами, ссылка – синоним для объекта ссылки. Все, что вы делаете со ссылкой, на самом деле выполняется над объектом ссылки, как показано в примере ниже: int referent = 0; int& reference = referent; ++reference; assert(1 == referent);
72
Основы
Вовторых, ссылка – это имя существующего объекта. Если в ходе работы про граммы объект ссылки становится недействительным, то результаты последую щего использования ссылки на него не определены. Одна из классических оши бок, допускаемых программистами на C++, – возврат ссылки на локальную переменную: std::string const& fn(char const* s) { std::string str(s); return str; } std::string const& rstr = fn("Íàðûâàåìñÿ íà íåïðèÿòíîñòè"); std::cout << rstr << std::endl; // Áóì!
Согласно правилам областей действия в C++ экземпляр str уничтожается еще до возврата из функции fn(), поэтому использование его в предложении вы вода – пример неопределенного поведения (скорее всего, это приведет к аварий ному завершению программы).
3.2.1. Ссылки на элементы STL"контейнеров Стандарт требует, чтобы в контейнере хранились сами значения элементов. Но для удобства пользования и ради производительности контейнеры часто дают доступ к своим элементам по ссылке. Например, чтобы изменить первый элемент контейнера std::vector<std::string>, нам нужно получить ссылку на него. std::vector<std::string> cont = . . . // Ïðåäïîëàãàåòñÿ, ÷òî âåêòîð íå ïóñò std::string& str = cont.front(); str.append(" è åùå ÷óòü-÷óòü"); // Èçìåíÿåì ýëåìåíò, õðàíÿùèéñÿ â cont
Если бы STLконтейнеры не умели предоставлять изменяемые ссылки на свои элементы, то их содержимое пришлось бы заменять полностью, что в лучшем случае неэффективно, а иногда семантически неприемлемо. Например, чтобы до бавить элемент в нулевую позицию, мы должны были бы добавить его в копию, а затем подменить оригинал этой копией. cont.replace(0, cont[0] + " è åùå ÷óòü-÷óòü");
Аналогично, если бы мы могли получать содержащиеся в контейнере элемен ты только по значению, а не по неизменяемой (const) ссылке, то для многих ти пов это привело бы к резкому снижению производительности. std::string str = cont[0]; // ýêçåìïëÿð êîïèðóåòñÿ, ðàñòî÷èòåëüíî ...
Получить ссылку намного лучше. Кстати, неизменяемая (const) ссылка на элемент STLконтейнера остается действительной, пока к самому контейнеру не будет применена изменяющая операция. (А некоторые изменяющие операции для некоторых контейнеров, например, push_back() для std::list оставляют действительными полученные ранее ссылки.) Это позволяет делать довольно сильные предположения, например:
Категории ссылок на элементы
73
void fn(std::vector const& cont) { if(!cont.empty()) { int const& ri = *cont.begin(); // ri äåéñòâèòåëüíà, íà÷èíàÿ ñ ýòîãî ìåñòà ... . . . } // ... è äî ýòîãî âíå çàâèñèìîñòè îò òîãî, ÷òî ïðîèñõîäèëî // â ïðîìåæóòêå }
Тем не менее, такое беззаботное смешение ссылок с семантикой значений в контейнере не означает, что вопрос о действительности объекта ссылки в STL не стоит. И уж тем более это не так для расширений STL. А теперь я хотел бы позна комить вас со своей классификацией.
3.3. Классификация ссылок на элементы Существует пять категорий (и один вариант) ссылок на элементы. На рис. 3.1 они приведены в порядке убывания значимости. В последующих разделах мы рас смотрим каждую категорию.
3.3.1. Перманентные Определение. Перманентная ссылка ссылается на элемент нелокального набора с неиз) меняемым порядком (раздел 2.2).
Рассмотрим следующую перманентную ссылку на элемент: namespace something // Òàêæå ïðèãîäíà äëÿ ãëîáàëüíîãî ïðîñòðàíñòâà èìåí { int ai[10]; int const& ri = ai[4]; } // ïðîñòðàíñòâî èìåí something
Если оставить в стороне патологические зависимости от порядка глобальных объектов (этот вопрос рассматривается в главе 11 книги Imperfect C++), то гаран
Рис. 3.1. Классификация ссылок на элементы
74
Основы
тируется, что такие ссылки действительны на протяжении всего времени своей жизни. Перманентные ссылки – это частный случай описываемых ниже фиксиро ванных ссылок, и на практике они встречаются крайне редко. Больше в этой книге они не появятся.
3.3.2. Фиксированные Определение. Фиксированная ссылка ссылается на элемент набора с неизменяемым порядком.
Рассмотрим пример: stlsoft::fixed_array_2d ar2d(10, 20); int& i_2_3 = ar2d[2][3];
В библиотеке STLSoft имеется набор шаблонных классов под общим названи ем «классы фиксированных массивов». Они не допускают изменения порядка, поскольку размеры массива задаются в момент конструирования, и тогда же создаются все элементы. Не существует операций, приводящих к уничтожению элементов. Ссылка на элемент с индексами [2][3] остается действительной на протяжении всего времени жизни массива ar2 и уничтожается вместе со всеми остальными элементами в деструкторе класса. В отличие от перманентных, получить недействительную фиксированную ссылку не так трудно, хотя для этого нужно предпринять обдуманное действие (или программировать уж очень небрежно). int& bad_wolf() { stlsoft::fixed_array_2d ar2d(10, 20); return ar2d[2][3]; } int& i_2_3 = bad_wolf(); // Ðåçóëüòàò èñïîëüçîâàíèÿ i_2_3 íå îïðåäåëåí. Áóì!
Хотя на практике такое встречается редко, наборы с изменяемым порядком (раздел 2.2) могут поддерживать категорию фиксированных ссылок, если исполь зуются таким образом, что изменяющие операции никогда не делают существую щие ссылки недействительными. Примером может служить класс, обертываю щий std::list<> и предоставляющий только операции добавления, он показан в разделе 25.6.1.
3.3.3. Чувствительные Определение. Чувствительная (invalidatable) ссылка ссылается на элемент набора с из) меняемым порядком.
Чувствительная ссылка действительна только до тех пор, пока к набору не применена изменяющая операция, которая разрушает объект ссылки. Другими
Категории ссылок на элементы
75
словами, после выполнения операции insert() или erase() (а также родствен ных им операций push_front(), push_back(), clear() и прочих) объект ссылки становится недействительным. Именно с чувствительными ссылками вы, скорее всего, встречались при работе со стандартными контейнерами. Какие конкретно изменяющие методы делают ссылки недействительными, зависит от набора и по существу является фундаментальным структурным свой ством типа набора. Например, добавление элемента в контейнер std::vector, в котором нет свободного места (т.е. capacity() == size()), приводит к перерас пределению памяти, в которой хранятся элементы, а, значит, все итераторы и ссылки на элементы, существовавшие перед вставкой, становятся недействитель ными. std::vector vi(10); int& ri = vi[0]; vi.resize(vi.capacity() + 1); int i = ri; // Íåîïðåäåëåííîå ïîâåäåíèå!
Напротив, добавление элемента в контейнер std::list никогда не приводит к недействительности существующих итераторов и ссылок. А удаление одного или нескольких элементов из std::list делает недействительными только ите раторы и ссылки на элементы из удаленного диапазона. Рассмотрим следующий фрагмент: std::list vi; vi.push_back(10); vi.push_back(20); std::list::iterator b0 = vi.begin(); std::list::iterator b1 = b0; int& ri0 = *b0; int& ri1 = *++b1; vi.erase(b0); std::cout << "ri0=" << ri0; // Çäåñü îáèòàþò äðàêîíû! std::cout << "; ri1=" << ri1 << std::endl;
Использование ссылки ri1 в последнем предложении вполне допустимо. Не действительной будет только ссылка ri0. На моей тестовой машине это предло жение напечатало "ri0=-17891602; ri1=20". Напротив, удаление элемента из std::vector делает недействительными все элементы, начиная от точки удаления и до конца последовательности. Однако эта ошибка не так очевидна, как в случае std::list, и может проявляться не сразу. Рассмотрим такой код: std::vector vi; vi.push_back(10);
Основы
76 vi.push_back(20); std::vector::iterator std::vector::iterator int& int&
b0 = vi.begin(); b1 = b0; ri0 = *b0; ri1 = *++b1;
vi.erase(b0); std::cout << "ri0=" << ri0; // Çäåñü îáèòàþò åùå áîëåå êîâàðíûå äðàêîíû! std::cout << "; ri1=" << ri1 << std::endl;
В данном случае на моей машине было напечатано "ri0=20; ri1=20". Немно го удивляет тот факт, что объект ссылки ri0 в данном случае не стал недействи тельным, поскольку имеется более одного элемента. Все элементы, начиная с точ ки удаления, логически сдвигаются в конец контейнера, но реально это делается путем присваивания копированием; это одна из причин, по которой требуется, чтобы элементы std::vector удовлетворяли ограничению Assignable (C++03: 23.1). Копия, получающаяся в результате копирующего присваивания, эквива лентна оригиналу. Следовательно, объект ссылки ri0, который ранее содержал значение 10, теперь стал равен 20. Естественно, тот факт, что память, в которой ранее находился второй элемент, теперь содержит значение 20, целиком и полностью зависит от неопределенных свойств данной реализации; теоретически там вполне могло бы оказаться произ вольное значение. Ниже мы увидим, что адаптация к STL некоторых контейнеров с непрерыв ной областью памяти (например, адаптеры класса CArray, рассматриваемые в гла ве 24) обладает иной семантикой сдвига, и тогда ссылка ri0 таки становится не действительной (даже если, по странному стечению обстоятельств продолжает работать). Вне зависимости от того, как реализовано изменение набора, чувстви тельные ссылки на элементы становятся недействительными только в результате операций, изменяющих порядок.
3.3.4. Недолговечные Определение. Недолговечная (transient) ссылка может стать недействительной в ре) зультате неизменяющей операции над объектом ссылки.
Недолговечные ссылки на элементы обычно встречаются в итераторах, класс которых содержит элемент того же типа, что и значения в наборе, в том случае, когда набор не хранит свои элементы. Примером может служить класс std::istream_iterator. После конструирования и при каждом вызове операто ра инкремента последние прочитанные из потока байты, составляющие тип значе ния, сохраняются во внутренней переменной в предвидении доступа с помощью оператора operator *(). Стандарт не требует, чтобы для хранения каждого значения, прочитанного из потока, использовался один и тот же экземпляр; он лишь говорит, что оператор
Категории ссылок на элементы
77
operator *() возвращает значение типа T const&. Это и приводит как к логиче ской, так и к физической недолговечности таких ссылок. Логически любой инкре мент (или декремент) итератора делает недействительными ранее полученные от него ссылки, причем это не зависит от состояния элементов набора, к которому применяется итератор. Кроме того, вы обязаны предполагать, что такие ссылки и физически недолго вечны. Иными словами, память, в которой хранится объект ссылки, уничтожается при инкременте итератора. В реальности, во всех реализациях стандартной биб лиотеки, для которых я тестировал следующий код, экземпляр итератора пользу ется одной и той же внутренней переменнойчленом. std::istream_iterator b(std::cin); for(; b != std::istream_iterator(); ++b) { ::printf("*(%p) == %c\n", &*b, *b); }
В случае компилятора GCC 3.4 и входной строки "abcd" печатается следую щий результат: *(0022FED4) *(0022FED4) *(0022FED4) *(0022FED4)
== == == ==
a b c d
Таким образом, ссылки на элементы, полученные от итератора, в разных точ ках выполнения программы, ссылаются на одну и ту же физическую переменную. При чтении двух и более символов остается верным следующее утверждение: std::istream_iterator b(std::cin); char const& r0 = *b++; char const& r1 = *b++; assert(&r0 == &r1); // Âåðíî äëÿ ðàñïðîñòðàíåííûõ ðåàëèçàöèé, // íî íå ãàðàíòèðóåòñÿ ñòàíäàðòîì
Довольно просто реализовать совместимый со стандартом итератор, который не будет использовать одну и ту же переменнуючлен. Естественно, потехи ради я реализовал такой класс: transient_istream_iterator (код имеется на компакт диске). При каждом чтении он выделяет новую память для значения из кучи. Если выполнить для него тот же код, что и выше, то получим: *(003F4F80) *(003F4EA0) *(003F4F80) *(003F4EA0)
== == == ==
a b c d
Для тех, кто желает возразить против сочетания несочетаемого или глумле ния над правилами STL при написании расширений, стоит отметить, что итератор std::istream_iterator делает то же самое. Стандарт говорит, что «результат
78
Основы
применения operator -> в конце потока не определен» (C++03: 24.5.1;1). Мы еще встретимся с такими исключениями из правил STL в дальнейшем.
3.3.5. Временные по значению Определение. Временная по значению (by)value temporary) ссылка на элемент – это во) обще не ссылка, а временный объект, возвращаемый вызванной функцией.
Обычно вызванной функцией является итератор – либо экземпляр адаптера итератора (см. часть III), либо итератор над расширенным STLнабором (см. часть II). Например, адаптер transform_iterator (глава 36) применяет унарную функцию к элементам адаптируемого типа с целью трансформации значения или типа. Поэтому он не может вернуть ссылку на чтолибо, а, стало быть, принадле жит к категории временных по значению ссылок на элементы. Значение, которое возвращает оператор разыменования, – это экземпляр трансформированного типа, а не ссылка на него. Аналогично итераторный класс string_tokeniser (раздел 27.6) не хранит копию элемента в текущей точке перебора, а по запро су создает объект, который представляет фрагмент строки, соответствующий этой точке. В итераторах, демонстрирующих поведение, характерное для временных по значению ссылок, типчлен pointer должен быть определен как void, а типчлен reference должен совпадать с типом значения. Первое требование позволяет обнаружить временные по значению ссылки на этапе компиляции (см главу 41), а второе нужно для того, чтобы адаптации, в особенности std::reverse_iterator, располагали отличным от void типом, с помощью которого можно реализовать оператор разыменования. Например, в листинге 3.1 показана специализация по рождающего шаблона std::iterator (раздел 12.2) для класса string_tokeniser ::const_iterator (раздел 27.6.1). Листинг 3.1. Объявление итератора из категории временных по значению template <. . .> class string_tokeniser { . . . typedef ???? value_type; . . . class const_iterator : public std::iterator< std::forward_iterator_tag , value_type, ptrdiff_t , void, value_type // âðåìåííûé ïî çíà÷åíèþ > { . . .
Бывает, что сам STLнабор принадлежит к категории временных по значению ссылок на элементы (в листингах я буду обозначать это сокращенно BVT). Напри мер, набор environment_map (глава 25) не может хранить внутренние ссылки на
Категории ссылок на элементы
79
свои элементы. Его оператор индексирования должен возвращать экземпляры строкового типа. Хотя поначалу категория временных по значению ссылок может показаться экзотикой, на самом деле при расширении STL она встречается очень часто, быть может, даже чаще всех остальных (по крайней мере, в моих программах). На раз личных примерах из тома 1 мы неоднократно убедимся, что понимание смысла этой категории и умение правильно ей пользоваться, жизненно важно для реали зации расширений STL.
3.3.6. Отсутствующие Определение. Категория ссылок отсутствующие означает, что от экземпляра итератора нельзя получить никакого значения.
«Отсутствующие» – самая ограничительная и мало на что пригодная катего рия ссылок на элементы. Итераторы и контейнеры, принадлежащие этой катего рии, не могут создавать значений. В стандартной библиотеке есть пример такого итератора: ostream_iterator – результат специализации порождающего шабло на std::iterator типом void для всех типовчленов, кроме категории итератора. template <. . .> class ostream_iterator : public iterator { . . .
Все итераторы вывода (см. главы 34, 39 и 40) должны делать одно и то же.
3.4. Использование категорий ссылок на итераторы Вы можете сказать: «Все это замечательно, но какое отношение имеет к моей реализации и использованию STLнаборов?» А вот какое. Если мы можем выя вить существенные с точки зрения категорий ссылок различия между типами, которые хочется использовать обобщенно, то сможем подстроить свой код к воз можностям этих типов, дабы повысить эффективность и избежать неопределен ного поведения.
3.4.1. Определение категории на этапе компиляции Хотя я осознал это не сразу, в существующем каркасе STL имеется возмож ность выявить наиболее существенные характеристики категорий ссылок на эле менты для итераторов, а это важнейшая часть техники адаптации итераторов, описываемой в части III. Определив, какие из типовчленов pointer, reference и value_type опреде лены как void, мы можем отделить категории временных по значению и отсутству
Основы
80
ющих ссылок, как показано в таблице 3.1. (Определение типа difference_type как void для итераторов категории «отсутствующие» несущественно для идентифи кации категории ссылок, но очень полезно, когда нужно запретить арифметиче ские операции над указателями для итераторов вывода.) Таблица 3.1. Идентификация категории ссылок на элементы по характеристикам типовчленов итераторов Категория ссылок
Определяющая характеристика
Перманентные, фиксированные, Член)тип pointer отличен от void. чувствительные и недолговечные Член)тип reference отличен от void. Член)тип value_type отличен от void. Член)тип difference_type отличен от void. Временные по значению
Член)тип pointer равен void. Член)тип reference отличен void, но не является ссылкой в смысле C++. Член)тип value_type отличен от void. Член)тип difference_type отличен от void.
Отсутствующие
Член)тип pointer равен void. Член)тип reference равен void. Член)тип value_type равен void. Член)тип difference_type равен void.
Идентифицировать другие категории ссылок на элементы для итераторов, ос новываясь только на типахчленах, невозможно, но это и не имеет практического значения, просто потому, что любой опытный пользователь STL знает, что не нужно сохранять ссылки, полученные от итератора, который в дальнейшем будет подвергнут инкременту или декременту, без тщательной проверки исходных до пущений.
3.4.2. Как компилятор может помочь избежать неопределенного поведения итератора Важная особенность временных ссылок состоит в том, что они, скажем так, поистине временные. Рассмотрим следующий шаблон функции, который, на пер вый взгляд, не вызывает никаких опасений: template typename std::iterator_traits::value_type& get_iter_val(I it) { return *it; }
Для всех стандартных и многих нестандартных итераторов такая функция будет работать без ошибок (если только итератор можно разыменовать, то есть он не является концевым, итератором вывода или voidитератором). Функция полу
Категории ссылок на элементы
81
чает из параметра it ссылку на ассоциированный с ним элемент и возвращает эту ссылку вызывающей программе. Не делается никаких копий элемента, не выделя ется память и по всей вероятности на уровне машинного кода вообще ничего су щественного не происходит. Однако, если итератор принадлежит к категории временных по значению ссы лок, то у этой функции возникнут проблемы, так как разыменование такого ите ратора дает не ссылку, а сам экземпляр элемента. В C++ запрещено присваивать временный объект неконстантной ссылке (C++03: 8.5.3). Но даже если бы это было разрешено, полученная клиентом ссылка вела бы на экземпляр, который уничтожен еще до возврата из get_iter_val(), как следует из правил областей действия в C++ (раздел 3.2). Чтобы правильно определить такую функцию, следует воспользоваться ти помчленом std::iterator_traits:: reference: template typename std::iterator_traits::reference get_iter_val(I it) { return *it; }
В таком виде она будет работать для всех разыменовываемых итераторов. В случае временных по значению итераторов это так, потому что член итератора reference совпадает с value_type, следовательно, возвращает значение. Для всех остальных типов итераторов это определение совпадает с приведенным выше.
3.5. Определение оператора operator ">() Следствием определения категорий временных по значению и отсутствую щих ссылок является тот факт, что в итераторах, возвращающие такие ссылки, не может быть реализован оператор «стрелка». Связано это с тем, что нет ничего та кого, что могло бы послужить в качестве возвращаемого значения, к которому можно было бы применить указатель или ссылку. А спецификация языка требует выполнения этого условия для любой перегрузки оператора ->(). Правило. Итераторы, относящиеся к категории временных по значению и отсутствующих ссылок, не могут определять оператор «стрелка».
Это правило полезно соотнести с проблемами, описанными в разделе 1.3.6. По правде говоря, в большинстве случаев итератор, поддерживающий семантику временной по значению ссылки, можно преобразовать так, чтобы он поддерживал недолговечные ссылки – достаточно было бы сохранить экземпляр текущего зна чения, но при этом может пострадать производительность. К счастью, как мы уви дим в главе 41, существуют способы распознать наличие или отсутствие операто ра «стрелка».
82
Основы
3.6. Еще о категориях ссылок на элементы Если сейчас все это кажется вам малопонятным или бесцельным, ничего страшного. Важность этого материала станет ясной по мере чтения частей II и III, где вы увидите, как различные характеристики влияют на проектирование и реа лизацию наборов, их итераторов и адаптеров итераторов.
Глава 4. Забавная безвременная ссылка Будь собой, прочие роли уже заняты. – Оскар Уайльд В C++ есть одна малоизвестная особенность, имеющая отношение к семантике кода, в котором используются временные по значению ссылки на элементы (раз дел 3.3). Рассмотрим следующий фрагмент: std::string const& rs = std::string("Áåçîïàñíî? Ñîìíèòåëüíî"); std::cout << rs << std::endl;
Если вашей первой реакцией было назвать этот код некорректным, а исполь зование ссылки rs в предложении вывода чреватым крахом процесса, то можете считать себя человеком разумным, хотя вы и ошибаетесь. Это не так. C++ требует, чтобы ссылки на экземпляры типа класса оставались действительными до конца области видимости, в которой они объявлены (C++03: 8.5.3;5, 12.2;3). А достичь этого можно путем создания скрытого временного объекта ссылки (раздел 3.2). Я называю это забавными безвременными ссылками. Таким образом, при некоторых обстоятельствах временные по значению ссылки на элементы (раздел 3.3.5) могут вести так, будто принадлежат более дол говечной категории. Например, следующий код, в котором используется компо нент platformstl::environment_map (глава 25), корректен: environment_map::value_type const& v = environment_map()["PATH"]; std::cout << v << std::endl;
Но есть две причины, изза которых чересчур спокойно чувствовать себя в этой ситуации не стоит. Вопервых, временный объект обязан существовать лишь до тех пор, пока не закрыта его область видимости. Если создать на него другую ссылку, скажем, попытавшись вернуть ее из функции (мы видели пример в разделе 3.2), то все гарантии аннулируются. Вовторых, эта техника не годится для наборов, тип значения которых не явля ется классом и не имеет семантики значения. Точно такая же конструкция, что и выше, будет вести себя непредсказуемо как для набора unixstl::glob_sequence (раздел 17.3): char const*& v = *glob_sequence(".", "*stl*.h*").begin(); std::cout << v << std::endl; // Íè÷åãî õîðîøåãî èç ýòîãî íå ïîëó÷èòñÿ!
84
Основы
так и для набора comstl::enumerator_sequence (раздел 28.5): typedef enumerator_sequence enseq_t; IEnumVARIANT* p = . . . VARIANT const*& v = *enseq_t(p, false).begin(); ::VariantCopy(. . . , v); // Êðàõ ãàðàíòèðîâàí!
Ниже мы увидим, что даже при безопасном применении забавные безвремен ные ссылки могут быть использованы как на пользу (раздел 9.2.2), так и во вред (раздел 25.9.5). Но в любом случае знать о них полезно.
Глава 5. Принцип DRY SPOT Мне нравится быть писателем. Только вот писать на бумаге я не люблю. – Питер де Вриз Принцип DRY SPOT рекомендует избегать многократных определений одного и того же. Аббревиатура DRY означает Don’t Repeat Yourself (не повторяйся) и по шла от мастеров своего дела – Прагматичных Программистов, которые впервые ввели ее в книге с одноименным названием. SPOT расшифровывается как Single Point of Truth (единый источник истины). Под этим названием эквивалентный принцип широко известен в мире UNIX. Уважая оба непогрешимых источника, я решил назвать этот принцип DRY SPOT (буквально «сухое пятно») в немалой степени потому, что, будучи англичанином, не могу противиться дурной игре слов, как собака не может не обнюхать другого представителя своего вида. Принцип DRY SPOT, как бы его ни называть, – это основа основ для любого хорошего программиста. Он требует, чтобы любая информация находилась толь ко в одном месте. Например, вместо того чтобы для обозначения количества раз рядов в типе int писать в разных местах программы 32, вы должны определить константу, например, так: const BITS_IN_INT = 8 * sizeof(int);
5.1. Принцип DRY SPOT в C++ В языке C++ мы немного отступаем от этого принципа, допуская в качестве исключения 0, отчасти потому что его можно преобразовать в любой числовой или указательный тип. (Тем не менее, я всегда для обозначения нулевого указате ля использую литерал NULL, а не 0).
5.1.1. Константы Программисты на C++ иногда придают специальный смысл и другим кон стантам; самые очевидные примеры – это 1 и -1. Например, бывает интересно знать, когда счетчик ссылок равен 1 (раздел 25.9). Заводить для этого отдельную константу, например, REF_COUNT_1 было бы глупо. По той же причине не имеет смысла определять константу -1 для обозначения недопустимого дескриптора файла, который возвращают в случае ошибки системные вызовы open() и socket() в UNIX. Это только сделало бы программу менее понятной. Не стоит
86
Основы
проявлять педантизм и в операциях умножения. Если вы пишете код для удвое ния размера изображения, то обозначать множитель константой NUMBER_2 просто нелепо. Но за исключением таких проявлений здравого смысла использование так на зываемых «магических чисел» – это зло, которого следует всеми силами избегать. Напишите сценарий, который поищет в вашем коде все числа, кроме 1, 0 и -1. Воз можно, вас ждет неприятный сюрприз.
5.1.2. Оператор dimensionof() В главе 14 книги Imperfect C++ я сетовал на отсутствие оператора dimensionof(), который вычислял бы (самую внешнюю) размерность массива на
этапе компиляции, и рассуждал по поводу возможной реализации. Я писал, что хорошо известная в языке C конструкция sizeof(x) / sizeof(x[0]) не будет работать, если x – указатель или экземпляр пользовательского типа, в котором определен оператор индексирования. В обоих случаях результат деления, скорее всего, даст не то, что нам нужно. Приводимое ниже решение используется в библиотеках STLSoft и основано на применении шаблонов (см. листинг 5.1). Компилятор транслирует макрос STLSOFT_NUM_ELEMENTS(), принимающий имя массива, в операцию sizeof() над шаблонной функцией, которая возвращает экземпляр шаблонной струк туры, размер которой равен размеру массива. (Для старых компиляторов, не поддерживающих шаблонов функций, возвращается просто (sizeof(ar) / sizeof(0[ar])).) Листинг 5.1. Определение макроса STLSOFT_NUM_ELEMENTS() в STLSoft // Â ïðîñòðàíñòâå èìåí stlsoft template struct ss_array_size_struct { unsigned char c[N]; }; template ss_array_size_struct const& ss_static_array_size(T (&)[N]); #define STLSOFT_NUM_ELEMENTS(ar) \ sizeof(stlsoft::ss_static_array_size(ar).c)
Не буду вдаваться в подробные пояснения, редактор пришел в ужас от объема получившегося творения и без цитат из ранее опубликованной работы! Если вы захотите приобрести экземпляр Imperfect C++ и прочитать об этом решении, буду только рад. А можете поверить мне на слово, что всякий раз, встречая в этой книге вызов STLSOFT_NUM_ELEMENTS(ar), вы имеете результат вычисления компилято ром внешней размерности массива ar, причем компилятор отвергнет любую по пытку применить эту конструкцию к указателям или экземплярам пользователь ских типов, для которых определен оператор индексирования.
Принцип DRY SPOT
87
Правило. Всегда используйте технику, аналогичную примененной в dimensionof(), ко) торая позволяет опознать и отвергнуть (на этапе компиляции) указатели и пользователь) ские типы при определении размерности массивов.
5.1.3. Порождающие функции Некоторые шаблоны настолько сложны, что запомнить, как их нужно специа лизировать, проблематично. На наше счастье, в C++ поддерживается неявное ин станцирование шаблонов функций, так что необязательно точно оговаривать все типы, участвующие в выражении. Это легло в основу идеи порождающих функ ций, с которыми вы будете часто встречаться в главах части III, посвященных ите раторам. Например, вместо такого кода (взят из главы 36): avg_mean(stlsoft::transform_iterator >(from, std::ptr_fun(::abs)) , stlsoft::transform_iterator >(to, std::ptr_fun(::abs)));
можно написать просто: avg_mean( stlsoft::transformer(from, std::ptr_fun(::abs)) , stlsoft::transformer(to, std::ptr_fun(::abs)));
и это без преувеличения можно назвать большим облегчением. Указание одного и того трансформирующего объектафункции в обоих обращениях к transformer() кажется нарушением принципа DRY SPOT, но на самом деле это не так (кроме ред ких случаев). Мы еще вернемся к этой теме в главе 36.
5.2. Когда в C++ приходится нарушать принцип DRY SPOT Увы, иногда в общемто достойная поддержка принципа DRY SPOT в C++ дает сбои.
5.2.1. Родительские классы При определении иерархии классов имеет смысл – и я это всегда делаю – оп ределять типчлен parent_class_type, с помощью которого можно сослаться на родительский класс. Особенно это полезно, когда иерархия подвержена измене ниям, в результате которых классы переименовываются или переставляются мес тами. Если в дочернем классе все выражено в терминах типа parent_class_type, а не самого имени (текущего) родительского класса, то изменения пройдут куда безболезненнее с точки зрения как простоты внесения, так и различий, которые показывает система управления версиями. Рассмотрим класс исключения missing_entry_point_exception из библио теки UNIXSTL (листинг 5.2), наследующий классу unix_exception. Он исполь зуется в функции dl_call() (раздел 16.6).
88
Основы
Листинг 5.2. Класс исключения, в котором определен типZчлен parent_class_type //  ïðîñòðàíñòâå èìåí rangelib class missing_entry_point_exception : public unix_exception { public: // Òèïû-÷ëåíû typedef unix_exception parent_class_type; typedef missing_entry_point_exception class_type; public: // Ìåòîäû äîñòóïà explicit char const* what() const throw() { if(m_name.empty()) { return parent_class_type::what(); } else . . .
Если нужно, чтобы missing_entry_point_exception унаследовал какому то другому классу, то придется лишь изменить имя родительского класса и опре деление parent_class_type. Поскольку процедура состоит из двух шагов, она не вполне следует принципу DRY SPOT. Но настолько близка к нему, насколько это возможно. Совет: Всегда определяйте член parent_class_type во всех производных классах, вхо) дящих в иерархию (одиночного) наследования.
Этот прием особенно важен, когда в середину иерархии могут вставляться но вые классы. Пусть, например, нам захотелось вставить в иерархию исключений класс dl_exception между missing_entry_point_exception и unix_exception (который станет теперь дедом последнего). Если бы в классе missing_entry_ point_exception не использовался тип parent_class_type, а были бы просто упоминания имени unix_exception, то легко можно было бы забыть о внесении изменений в метод what(), что привело бы к ошибкам при перехвате и опросе типа исключения.
5.2.2. Типы значений, возвращаемых функциями Еще одно отступление от идеала наблюдается для значений, возвращаемых шаблонными функциями. Рассмотрим следующий фрагмент кода одного из пере груженных вариантов прокладки строкового доступа c_str_ptr (раздел 9.3.1): Листинг 5.3. Определение перегруженного варианта функции c_str_ptr для параметра типа SYSTEMTIME, встречающегося в Windows basic_shim_string c_str_ptr(SYSTEMTIME const& t) {
Принцип DRY SPOT
89
typedef basic_shim_string string_t; . . . // Ðàçëè÷íûå âûçîâû äëÿ âû÷èñëåíèÿ ðàçìåðà è çàïîìèíàíèÿ // â ïåðåìåííîé numChars string_t s(numChars); . . . // Ðàçëè÷íûå âûçîâû äëÿ ïîìåùåíèÿ â áóôåð ñòðîêîâîãî ïðåäñòàâëåíèÿ t return s; }
В данном случае специализацию basic_shim_string пришлось выполнять дважды: при описании типа возвращаемого значения и внутри тела функции. (Ясно, что внутри функции следует создать typedef, как и показано в коде. В про тивном случае эта специализация встречалась бы еще чаще.) Если вы думаете, что никакой опасности тут нет, так как компилятор предупредит о несоответствии, то вынужден вас разочаровать. Обратите внимание на определенный вовне символь ный тип TCHAR, вместо которого может быть подставлено как char, так и wchar_t (в зависимости от наличия или отсутствия символа препроцессора UNICODE). Воз можно расхождение между TCHAR и char или между TCHAR и wchar_t, поэтому в одном случае компиляция пройдет успешно, а в другом нет. Такие вещи могут происходить, когда вы уже давно перестали работать над кодом, и повергать в ужас. Один из рецензентов книги высказал свое разочарование тем, что у этой про блемы нет окончательного решения. К сожалению, это так – что%то всегда прихо дится задавать дважды. Однако, если это что%то достаточно сложное, то можно в значительной степени заменить копирование/вставку порождающим шаблоном (раздел 1.2.2), как показано в определении функций, порождающих итератор member_selector() в разделе 38.4.5. Совет. Применяйте порождающие шаблоны, чтобы уменьшить риск, сопряженный с ко) пированием и вставкой, когда определяете функцию, для которой нетривиальный тип возвращаемого значения должен быть явно задан внутри тела.
5.3. Замкнутые пространства имен Есть еще один аспект принципа DRY SPOT, который легко не заметить среди многочисленных нюансов C++. Надеюсь, вам известно, что пространство имен, определяемое ключевым словом namespace, открыто. Это означает, что его мож но пополнить в любой момент, открыв заново в другом объявлении пространства имен с тем же именем. Это дает возможность использовать код так, как его автор и не предполагал. Например, вы могли бы написать следующий код, ожидая, что ваше пространство имен останется священным и неприкосновенным. // OstensiblyDefinitiveVersion.hpp namespace covenant { int func(std::string const& s); // Êîíêðåòíàÿ âåðñèÿ äëÿ òèïà std::string template int func(S const& s); // Îáùàÿ âåðñèÿ äëÿ äðóãèõ òèïîâ } // ïðîñòðàíñòâî èìåí covenant
90
Основы
Но ничто не мешает любому пользователю включить в то же пространство имен, код, который потенциально мог бы нарушить работу вашего. // CavalierManipulations.hpp namespace covenant { int func(std::exception const& x); } // ïðîñòðàíñòâî èìåí covenant
Теперь область действия оригинального шаблона функции сужена, а поведе ние кода, написанного в предположении о неизменности пространства имен, мо жет измениться. В общем случае, это плохо, поэтому стандарт запрещает любые добавления в пространство имен std, за исключением полных специализаций су ществующих шаблонов. (Интересное замечание на эту тему см. в разделе 31.5.3.) Разумеется, плохо это не всегда, мощная и бесконечно расширяемая концепция прокладки (глава 9) всецело зависит от этой возможности. Чтобы получить понастоящему замкнутое пространство имен, придется вос пользоваться конструкциями class/union/struct: // EnforceablyDefinitiveVersion.hpp struct covenant { private: covenant(); // Ïðåäîòâðàòèòü êîíñòðóèðîâàíèå, âåäü ýòî æå "ïðîñòðàíñòâî èìåí" public: static int func(std::string const& s); template static int func(S const& s); }; // "ïðîñòðàíñòâî èìåí" covenant
На такое «пространство имен» налагаются ограничения, которым настоящие пространства имен не подвержены. В частности, в нем не может быть констант членов не интегральных типов. Нельзя также объявлять пространства именчле ны, хотя их можно имитировать с помощью вложенных class/struct/union. Помимо замкнутости, у таких конструкций есть и другие преимущества над про странствами имен. Например, можно объявлять закрытые члены и их друзей и тем самым ограничивать возможности членов заведомо известными контекстами. Совет. Имитируйте замкнутые пространства имен с помощью структур (с закрытыми кон) структорами).
Глава 6. Закон дырявых абстракций Чем сильнее вера, тем ближе дьявол. – Автор неизвестен В ноябре 2002 года Джоэл Спольски (Joel Spolsky) – автор популярного блога «Joel on Software» – сформулировал новый закон в области разработки программ ного обеспечения, который назвал Законом дырявых абстракций. Закон. Все нетривиальные абстракции так или иначе протекают.
По существу, тезис Джоэла означает, что все (нетривиальные) абстракции не совершенны, то есть всегда найдется место, в котором пользователю абстракции понадобится знать, что за ней скрывается. STL – замечательная абстракция. Я абсолютно серьезно, без иронии. Пораз мыслите минутку над тем, насколько она замечательна, и, если в данный момент вы с этим не согласны, то вернитесь к этой мысли еще раз, когда прочитаете книгу. Уверен, что тогда вы согласитесь. Но – и от этого никуда не уйти – абстракция STL на удивление дырявая. Чес тно говоря, большинство абстракций в C++ протекают по необходимости, отчасти изза той свободы, которая предоставлена программистам на этом языке (предпо лагается, что все они сведущи и сознательны), а отчасти изза необходимости со хранять совместимость с добрым старым C. Но STL в этом отношении выделяется даже на фоне C++. Взять, к примеру, итераторы. В C++ в основу итератора поло жена идея указателя. И вот тут начинается… Указатель p – это адрес области памяти. Память непрерывна. К элементу e, на кото рый ведет указатель, можно получить доступ – разыменовать – с помощью примене ния к указателю оператора разыменования operator *() – *p. Если типом элемента является класс – struct, class или union, то к входящему в состав этого класса элементу m можно получить доступ, применив к указателю оператор выбора члена operator ->() – p->m. Если указатель указывает на массив элементов, то к нему применимы арифметические операции. Выражения ++p и p++ инкрементируют ука затель, так чтобы он указывал на элемент, следующий непосредственно за e; при этом значением первого из них является значение указателя до инкремента, а второго – значение после инкремента. Выражения – p и p – аналогичны, но сдвигают указатель на элемент, непосредственно предшествующий e в памяти. Каждое из выражений ++p, p + 1, &p[1], &(*(p + 1)), &(*(1 + p)), &1[p] дает один и тот же результат. Указатели можно сравнивать на равенство и неравенство со специальным значением 0
92
Основы
(иногда его кодируют в виде макроса NULL). Два указателя одного и того же типа мож но сравнивать арифметически (с помощью операторов ==, !=, <, <=, > и >=), если они указывают на элементы одного массива и тип *p отличен от void.
Ни одна из пяти категорий итераторов, определенных в стандарте, не поддер живает описанную семантику указателей в полной мере. Даже введенная мной дополнительная категория непрерывных итераторов (раздел 2.3.6), и та не обла дает абсолютной семантической эквивалентностью с указателями (хотя близка к этому). Ни один итератор в STL не поддерживает семантику той абстракции, которую моделирует. И мы еще удивляемся, почему люди приходят в замеша тельство! В этой книге (и в томе 2) вам встретится много абстракций, и все они протека ют. Это грань жизни, грань практики программирования, грань C++ и, в особен ности, грань STL. От абстракций никуда не деться, но мы можем подойти к ним, вооружившись знаниями, опытом, идиомами, рекомендованной практикой и кон кретными технологиями. Эта книга даст вам все, кроме опыта, а уж его вам при дется набираться самостоятельно.
Глава 7. Программирование по контракту С понятием ответственности неразрывно связаны превентивные меры. – Профессор Мартин Селигман Авария! Авария! У нас авария! – Холли, Красный Карлик Программирование по контракту – это способ выражения ожидаемого и допу стимого поведения программных компонентов в виде контрактов и включение в код конструкций, верифицирующих выполнение этих контрактов. В настоящей главе мы вкратце опишем те аспекты программирования по контракту, которые имеют отношение к содержимому этой книги и ее второго тома. На компактдиск я поместил ряд своих статей на эту тему, в которых четко описаны мое видение и обоснование программирования по контракту в C++, – на случай, если вы захоти те углубиться в эту проблематику. Важно понимать, что программный контракт не обязан ни контролироваться, ни даже явно выражаться в коде. Иногда это даже попросту невозможно. А иногда автор может не видеть в этом смысла. Мой подход состоит в том, чтобы обращать пристальное внимание на выражение и принудительный контроль выполнения контракта в программе, но это не означает, будто я настаиваю на том, что любое условие контракта можно выразить в коде или что отсутствие контроля делает допустимым любое не запрещенное явно поведение.
7.1. Виды контроля Контролем называются действия, предпринимаемые программой для того, чтобы обнаружить и както обработать нарушения контракта. Широко распрост ранены три вида контроля: предусловия, постусловия и инварианты класса. В предусловиях формулируются условия, которые должны выполняться для того, чтобы функция или метод могли работать в соответствии с задуманным. Обеспечение выполнения предусловий – забота вызывающей программы. В по% стусловиях говорится, какие условия должны быть выполнены после того, как функция или метод завершили работу. Гарантировать выполнение постусловий должна вызываемая функция. Инварианты класса – это набор условий, которым должен удовлетворять класс, чтобы оказаться в состоянии работать в соответ ствии с задуманным. Инвариант должен удовлетворяться в любой момент при
94
Содержание
наблюдении за классом извне. Инварианты класса следует проверять после кон струирования, перед уничтожением, а также до и после вызова извне любой от крытой функциичлена. Важно, что инвариант класса не обязан удовлетворяться на протяжении выполнения метода. (Иначе как мы могли бы вообще изменять состояние класса, имеющего более одной переменнойчлена?) Контроль предусловий в C++ обычно принимает форму проверки аргументов метода или состояния объекта, чей метод вызывается. В следующих примерах по казано, как просто его можно реализовать. В листинге 7.1 демонстрируется про верка в одном из конструкторов stlsoft::scoped_handle (раздел 16.5) преду словия, заключающегося в том, что переданный указатель на функцию очистки не равен нулю. Листинг 7.1. Пример проверки предусловия в конструкторе scoped_handleConstructor //  ïðîñòðàíñòâå èìåí stlsoft template class scoped_handle { . . . public: // Êîíñòðóèðîâàíèå scoped_handle(H h, void (STLSOFT_CDECL* fn)(H), H hNull = 0) : . . . // Èíèöèàëèçèðîâàòü ÷ëåíû { STLSOFT_MESSAGE_ASSERT("Íàðóøåíî ïðåäóñëîâèå: óêàçàòåëü fn ðàâåí NULL" , NULL != fn); } . . .
Гораздо сложнее обеспечить осмысленную реализацию постусловий в C++, и в этой книге (да и во втором томе, наверное) вы их не найдете. Лично я для реализации инвариантов определяю в каждом классе неоткры тый, невиртуальный метод is_valid() без аргументов, который возвращает булевское значение. Он вызывается в начале (после проверки предусловий) и в конце каждого открытого метода. Если удобно, метод проверки инварианта может внутри себя содержать утверждения, не откладывая дело до возврата в вы зывающую программу. В листинге 6.2 показано, как в шаблонном классе enumerator_sequence (раздел 28.6) инвариант итератора проверяется до и после реализации оператора инкремента (оформленной в виде закрытого метода increment_()). Листинг 7.2. Оператор прединкремента с проверкой инварианта template <. . .> class_type& enumerator_sequence::iterator::operator ++() { COMSTL_ASSERT(is_valid()); increment_();
Содержание
95
COMSTL_ASSERT(is_valid()); return *this; }
7.2. Механизмы контроля У механизма контроля есть три аспекта: обнаружение, извещение и реакция. На этапе обнаружения проверяется, не нарушают ли условия контракта передан ные аргументы и/или текущее состояние. Извещение – это информирование кого то или чегото о нарушении. Под реакцией понимаются действия по завершению процесса. В C/C++ самым распространенным средством контроля является макрос assert(), сочетающий в себе все три аспекта: предложение if (или тернарный оператор), вызов print() и вызов exit(). В библиотеках STLSoft (и других моих библиотеках, упоминаемых в этой книге) применяются собственные макросы типа assert, которые пользователь может переопределить, если хочет реализо вать обнаружение, извещение и реакцию удобным для себя способом.
Глава 8. Ограничения Я вижу и не понимаю. Я слышу и забываю. Я делаю и вспоминаю. – Поговорка Средний человек не хочет быть свободным. Он хочет чувствовать себя в безопасности. – Генри Луис Менкен Ограничением называется условие, соблюдения которого один элемент про граммы требует от другого на этапе компиляции, так как только в этом случае они смогут работать совместно. Можно считать, что ограничения – это вид контроля выполнения контракта на этапе компиляции. В разделе 1.2 книги Imperfect C++ я много говорил об ограничениях. Редактор был бы очень недоволен, если бы я взял ся повторять весь этот материал еще и здесь, поэтому я опишу лишь два основных типа поддерживаемых языком ограничений и приведу несколько примеров.
8.1. Поддержка со стороны системы типов Классический пример ограничения, приведенный Бьярном Страуструпом (в сообщении в конференции comp.lang.c++.moderated), – это требование, чтобы тип D был производным от типа B. В листинге 8.1 показана моя версия этого ограниче ния, взятая из библиотеки STLSoft. Листинг 8.1. Ограничение must_have_base на допустимый тип класса template< typename D // Ïðîèçâîäíûé êëàññ , typename B // Áàçîâûé êëàññ > struct must_have_base { public: ~must_have_base() { void (*p)(D*, B*) = constraints; } private: static void constraints(D* pd, B* pb) { pb = pd; } };
Ограничения
97
В этом ограничении требуется, чтобы указатель на параметр шаблона D можно было присвоить указателю на параметр шаблона B. Это возможно только, если тип D совпадает с типом B или (открыто) наследует ему. Такое ограничение могло бы пригодиться при написании шаблона функции, для которого требуется, чтобы тип занимал определенное место в иерархии насле дования, как в случае класса c_str_ptr_null_CWnd_proxy, приведенного в лис тинге 8.2. (Это внутренний служебный класс. Пусть его имя вас не пугает, в кли ентском коде он не встречается.) Листинг 8.2. Ограничение на тип параметра шаблонного конструктора // Â ïðîñòðàíñòâå èìåí mfcstl class c_str_ptr_null_CWnd_proxy { . . . public: // Êîíñòðóèðîâàíèå template c_str_ptr_null_CWnd_proxy(W const& w) { must_have_base<W, CWnd>() // Ãàðàíòèðóåò, ÷òî W ïðèíàäëåæèò êëàññó CWnd èëè ïðîèçâîäíîìó îò íåãî . . .
Ограничения такого вида основаны на гарантиях, которые дает система типов в C++.
8.2. Статические утверждения Другой механизм накладывания ограничений – это статические утвержде% ния. Так называется макрос, результатом расширения которого является код, ко торый будет успешно компилироваться только, если утверждение выполняется. Существует несколько вариантов: предложения switch, битовые поля и объявле ния массивов. Я воспользуюсь последним. Рассмотрим статическое утверждение (листинг 8.3) из библиотеки dl_call() (раздел 16.6), призванное гарантировать, что все три аргумента a0, a1 и a2, пе редаваемые динамически загружаемой функции, допустимы. Аргумент считает ся допустимым, если он имеет один из фундаментальных типов, тип указателя или тип указателя на функцию, а также если пользователь явно обозначил допу стимость аргумента, определив специализацию характеристического класса is_valid_dl_call_arg. (Для первых трех случаев в библиотеке STLSoft есть го товые характеристические классы, а последний – часть библиотеки dl_call().) Листинг 8.1. Наложение ограничений на типы параметров обобщенного шаблона функции // Â ïðîñòðàíñòâå èìåí unixstl / winstl template< typename R, typename L, typename FD template, typename A0, typename A1, typename A2 template>
98
Содержание
R dl_call(L const& library, FD const& fd, A0 a0, A1 a1, A2 a2) { STLSOFT_STATIC_ASSERT (is_fundamental_type::value || is_pointer_type::value || is_function_pointer_type::value || is_valid_dl_call_arg::value); . . . // Òî æå ñàìîå äëÿ A1 è A2 return dl_call_MOD(. . . , a0, a1, a2); }
Если функции передать параметры, типы которых не удовлетворяют этим ограничениям, то в результате расширения макроса STLSOFT_STATIC_ASSERT() будет предпринята попытка объявить массив длины 0 (или 1, в зависимости от компилятора). При этом произойдет ошибка компиляции, не дающая обойти ограничения на использование функции dl_call(), наложенные ее автором. То есть мной. А нечего!
Глава 9. Прокладки Доминирующим фактором в современном обществе является изменение, непрерывное и неизбежное. Ни одно разумное решение уже нельзя принимать с учетом только си% юминутного состояния мира. Нужно учи% тывать и то, каким мир станет. – Айзек Азимов Ничто не вызывает у меня большего восхи% щения, чем стойкость, с которой миллионе% ры переносят тяготы своего богатства. – Ниро Вульф
9.1. Введение Прокладки (shims) – это простой, но чрезвычайно мощный инструмент обоб щенного программирования, который позволяет добиться умеренного или даже высокого сцепления (cohesion) при минимальной, а часто вовсе отсутствующей, связанности (coupling). Прокладка описывает неограниченный набор перегру женных вариантов функции, которые обобщают некоторые аспекты концептуаль но взаимосвязанных, но физически несоотносимых типов. У прокладки имеются следующие характеристики: Имя. Имя прокладки составлено из имени функции и пространства имен, в котором она находится. Например, полное имя прокладки c_str_ptr – stlsoft::c_str_ptr. Назначение. Для чего предназначены все функции, входящие в состав про кладки. Например, назначение прокладки c_str_ptr состоит в том, чтобы преобразовать любой поддерживаемый тип в ненулевой указатель на стро ку символов, завершающуюся нулем, то есть представить экземпляр типа в виде строки. Категория. Описывает соглашение об именовании, поведение и ограниче ния на использование прокладки. Видимый тип возвращаемого значения. Все функции, входящие в состав прокладки, должны возвращать значение либо этого типа, либо такого, ко торый может быть в него неявно преобразован.
100
Основы
Существует четыре основных категории прокладок: атрибутные (раздел 9.2.1), управляющие, конвертирующие (раздел 9.2.2) и логические. Комбинируя их, мож но получать составные категории, важнейшей из которых является категория прокладок доступа (раздел 9.2.3). В этой главе мы рассмотрим только те катего рии, которые будут использоваться в частях II и III для повышения гибкости рас ширений STL: атрибутные, конвертирующие и прокладки строкового доступа (раздел 9.3.1). Полное определение всех идентифицированных на данный момент категорий прокладок и всех общеизвестных конкретных прокладок имеется в онлайновой документации по STLSoft. А в моей следующей книге Breaking Up the Monolith (Разрушение монолита) будет полно информации о прокладках и различных спо собах их применения. Если вам интересно, как далеко можно продвинуться по пути обобщенного программирования, сведя к минимуму связанность и наклад ные расходы, то призываю вас приобрести экземплярчик. (Конечно, я не самый беспристрастный советчик.)
9.2. Основные прокладки 9.2.1. Атрибутные прокладки Атрибутная прокладка извлекает атрибуты или состояния экземпляров тех типов, для которых определена. Примером может служить прокладка get_ptr. В листинге 9.1 приведены некоторые из входящих в нее функций. (Отметим, что в действительности они находятся в другом заголовочном файле.) Листинг 9.1. Определения перегруженных вариантов get_ptr // Â ïðîñòðàíñòâå èìåí stlsoft template T* get_ptr(T* p) { return p; } template T* get_ptr(std::auto_ptr const& p) { return p.get(); } inline void const* get_ptr(unixstl::memory_mapped_file const& mmf) { return mmf.memory(); }
Если нужно написать код, который будет одинаково работать с настоящими и ин теллектуальными указателями, то программируйте в терминах stlsoft::get_ptr(), не думая о том, вызывается ли для доступа к реальному указателю метод get(), memory() или еще какойто.
Прокладки
101
9.2.2. Конвертирующие прокладки Конвертирующая прокладка преобразует экземпляры тех типов, для кото рых определена, в какойто конкретный тип. Например, можно определить про кладку to_string, для которой видимый тип возвращаемого значения – const std::string. Вот некоторые из возможных перегруженных вариантов: // Â ïðîñòðàíñòâå èìåí my_shims inline std::string to_string(char const *s) { return std::string(s); } inline std::string const& to_string(std::string const &s) { return s; } inline std::string to_string(int i) { char buf[21]; return std::string(&buf[0], size_t(::sprintf(&buf[0], "%d", i))); }
Отметим, что во втором случае возвращается константный вариант передан ной в качестве аргумент ссылки, а не экземпляр типа std::string. Это нор мально, поскольку в любом коде, где предполагается видимый тип const std::string, можно с тем же успехом использовать и тип const std::string&, как показано ниже: int i = 101; char const* cs = "abc"; std::string str("def"); ::puts(my_shims::to_string(i).c_str()); ::puts(my_shims::to_string(cs).c_str()); ::puts(my_shims::to_string(str).c_str());
// Ïðàâèëüíî // Ïðàâèëüíî // Ïðàâèëüíî
Благодаря феномену забавной безвременной ссылки (глава 4) эта прокладка работает даже в следующем случае: std::string const& str1 = my_shims::to_string(i); std::string const& str2 = my_shims::to_string(cs); std::string const& str3 = my_shims::to_string(str); ::puts(str1.c_str()); ::puts(str2.c_str()); ::puts(str3.c_str());
Здесь str1 и str2 – забавные безвременные ссылки; а str3 – обычная ссылка. В тех случаях, когда видимый возвращаемый тип не обладает семантикой зна чения, конвертирующие прокладки применяются не так прямолинейно. Напри мер, мы могли бы определить конвертирующую прокладку to_file_handle, кото рая возвращает значение видимого типа int. (Я говорю могли бы, потому что такая прокладка оказалась бы настолько высокоуровневой и мощной, что иметь ее
102
Основы
в своем распоряжении было бы просто опасно. Этот пример приведен в чисто пе дагогических целях, не вздумайте вставлять его в свой код!) Преобразование де скриптора файла в дескриптор файла очевидно: // Â ïðîñòðàíñòâå èìåí my_shims inline int to_file_handle(int h) { return h; }
Пока все хорошо. Но предположим, что нужно преобразовать имя файла (char const*). Такая перегрузка прокладки должна была бы вернуть экземпляр временного класса, который имеет неявный конвертор в тип int и управляет де скриптором файла с заданным именем (листинг 9.2). Листинг 9.2. Пример конвертирующей прокладки to_file_handle и поддерживающего ее класса //  ïðîñòðàíñòâå èìåí my_shims class shim_file_handle { public: // Êîíñòðóèðîâàíèå shim_file_handle(char const* fileName); // Âûçûâàåòñÿ ::open() ~shim_file_handle() throw(); // Âûçûâàåòñÿ ::close() public: // Ïðåîáðàçîâàíèå operator int () const; // Íåÿâíîå ïðåîáðàçîâàíèå â âèäèìûé òèï private: // Ïåðåìåííûå-÷ëåíû int m_handle; }; shim_file_handle to_file_handle(char const* fileName) { return shim_file_handle(fileName); }
Предположим, что имеется функция, которая принимает дескриптор файл и чтото с ним делает, например, преобразует содержимое в список строк: std::list<std::string> readlines(int hFile);
С помощью прокладки to_file_handle можно написать обобщенный шаблон функции readlines(): template std::list<std::string> readlines(F const& fileDescriptor) { return readlines(my_shims::to_file_handle(fileDescriptor)); }
Теперь мы можем прочитать строки из любого файла, неважно, задан он деск риптором (int): int fh = ::open("myfile.txt", . . . ); std::list<std::string> lines1 = readlines(fh);
или именем (char const*): std::list<std::string> lines2 = readlines("myfile.txt");
Прокладки
103
Важно понимать, что теперь функция readlines() стала обобщенной и пол ной. Чтобы расширить диапазон типов, с которыми она может работать, нужно лишь определить дополнительные перегрузки прокладки to_file_handle() (в пространстве имен my_shims). Будь у нас сторонняя библиотека, в которой определен класс File, предоставляющий дескриптор с помощью метода raw_handle(), мы могли бы написать такой совместимый с readlines() класс: // Â ïðîñòðàíñòâå èìåí ThirdPartyPeople class File; // Â ïðîñòðàíñòâå èìåí my_shims inline int to_file_handle(ThirdPartyPeople::File& file) { return file.raw_handle(); } // Êëèåíòñêèé êîä ThirdPartyPeople::File file("myfile.ext"); std::list<std::string> lines1 = readlines(file);
Теперь тип File совместим с любыми функциями и классами, которые мани пулируют дескрипторами файлов через прокладку to_file_handle. Это хорошая демонстрация мощи прокладок. Однако ее легко можно свести на нет, если забыть о том, что возвращаемый прокладкой to_file_handle видимый тип не обладает семантикой значения. Если бы мы написали перегруженный шаб лон функции readlines(), как показано ниже, то получили бы неопределенное поведение при работе с дескриптором файла типа char const* (или любого дру гого типа, прокладка которого возвращает объект не типа int, а типа, преобразуе мого в int): template std::list<std::string> readlines(F const& fileDescriptor) { int hFile = to_file_handle(fileDescriptor); return readlines(hFile); // Äåñêðèïòîð óæå çàêðûò! }
К тому моменту, как программа дойдет до вызова readlines(int), дескрип тор файла, полученный от прокладки to_file_handle(char const*), будет не действителен, так как экземпляр возвращенного ей класса shim_file_handle, уже уничтожен, а вместе с ним закрыт и файл, открытый в момент конструиро вания. Эта проблема возникает только тогда, когда возвращенное значение сохраня ется для последующего использования. Следовательно, при работе с конверти рующими прокладками нужно строго придерживаться следующего правила. Правило. Значение, возвращенное конвертирующей прокладкой, никогда не должно ис) пользоваться вне выражения, в котором прокладка была вызвана, за исключением тех случаев, когда видимый возвращаемый тип обладает семантикой значения.
104
Основы
9.3. Составные прокладки Составной называется прокладка, полученная комбинированием прокладок, принадлежащих другим категориям. На составные прокладки налагаются все ограничения, присущие составляющим их категориям. В этой книге нас будут ин тересовать только составные прокладки, полученные композицией атрибутной и конвертирующей прокладок. Мы будем называть их прокладками доступа.
9.3.1. Прокладки строкового доступа В библиотеке STLSoft и многих связанных с ней (FastFormat, OpenRJ, Pantheios и recls), а также в своих коммерческих программах, разработанных на протяжении последних пяти лет, я применял главным образом прокладки строко% вого доступа. Мы еще не раз увидим, как с их помощью можно заметно повысить гибкость используемых компонентов. Опишем три основных разновидности та ких прокладок. 1. Прокладка c_str_ptr и ее варианты для различных кодировок символов c_str_ptr_a и c_str_ptr_w. Перегруженные функции, входящие в состав этих прокладок, возвращают ненулевой указатель на завершающуюся ну лем строку в стиле C, дающую строковое представление соответствующего типа. Видимый тип значения, возвращаемого прокладкой c_str_ptr_a – char const*; прокладкой c_str_ptr_w – wchar_t const*; а прокладка c_str_ptr возвращает тип, определяемый типом параметра, то есть c_str_ptr (std::string const*) возвращает char const*, а c_str_ptr(std::wstring const*) – wchar_t const*. 2. Прокладка c_str_data и ее варианты для различных кодировок символов c_str_data_a и c_str_data_w. Перегруженные функции, входящие в состав этих прокладок, возвращают указатель на строку в стиле C (не обязательно завершающуюся нулем), дающую строковое представление соответствую щего типа. Строка не обязана завершаться нулем, так как c_str_data всегда используется в сочетании с c_str_len. Видимые возвращаемые типы такие же, как для c_str_ptr (и ее вариантов). 3. Прокладка c_str_len и ее варианты для различных кодировок символов c_str_len_a и c_str_len_w. Перегруженные функции, входящие в состав этих прокладок, возвращают длину строки, полученной от соответствен ной перегруженной функции из прокладки c_str_ptr (или ее варианта) либо c_str_data (или ее варианта). Видимый возвращаемый тип – size_t (или эквивалентный ему). (Прокладка c_str_len и ее варианты – это в чис том виде атрибутные прокладки, но, поскольку они используются только в сочетании с c_str_data или c_str_ptr, я счел уместным назвать их про кладками строкового доступа.) Мощь прокладок строкового доступа трудно переоценить. Рассмотрим биб лиотеку протоколирования Pantheios, в которой прокладки c_str_data_a и c_str_len_a применяются в шаблонах функций прикладного уровня (они автома
Прокладки
105
тически генерируются сценарием). В листинге 9.3 приведено определение пере груженного варианта шаблона log_ALERT() с тремя параметрами. Листинг 9.3. Применение прокладок строкового доступа в функциях прикладного уровня библиотеки Pantheios template int log_ALERT(T0 const& v0, T1 const& v1, T2 const& v2) { if(!isSeverityLogged(PANTHEIOS_SEV_ALERT)) { return 0; } else { return log_dispatch_3(PANTHEIOS_SEV_ALERT , stlsoft::c_str_len_a(v0), stlsoft::c_str_data_a(v0) , stlsoft::c_str_len_a(v1), stlsoft::c_str_data_a(v1) , stlsoft::c_str_len_a(v2), stlsoft::c_str_data_a(v2)); } }
Указание независимых типов параметров шаблона (T0, T1 и T2) в сочетании с прокладками строкового доступа обеспечивает практически безграничную рас ширяемость Pantheios со стопроцентной безопасностью относительно типов. Отметим также, что извлечение значений параметров, все преобразования, выде ление памяти, копирование и конкатенация в результирующую строку произво дятся лишь после того, как выяснилось, что сообщения данного уровня серьезности (PANTHEIOS_SEV_ALERT) действительно надо протоколировать. Следовательно, если некий уровень протоколирования отключен, то практически никаких на кладных расходов и не возникает. За счет использования прокладок c_str_data_a и c_str_len_a прикладной слой Pantheios совместим с любым типом, для которого определена соответст вующая прокладка, что позволяет писать код просто и естественно: void func(std::string const& s1, char const* s2, struct tm const* t) { pantheios::log_DEBUG("func(", s1, ", ", s2, ", ", t, ")"); . . .
или: catch(std::exception& x) { pantheios::log_CRITICAL("Âñå ïîøëî íàïåðåêîñÿê: ", x); }
или: VARIANT CWindow& HWND
var1 = . . . wnd1 = . . . hwnd2 = . . .
pantheios::log_ERROR("var=", var1, "; wnd=", wnd1, "; hwnd=", hwnd2);
106
Основы
Если передать тип, для которого не определен перегруженный вариант про кладки (или он не виден, поскольку вы забыли директиву #include), то вы полу чите вполне вразумительное (по понятиям библиотеки шаблонов) сообщение об ошибке, в котором говорится, что ни один из N известных библиотеке перегружен ных вариантов не совместим с этим типом. На момент написания этой книги в библиотеке STLSoft имеются прокладки строкового доступа для следующих стандартных или сторонних типов: char const*/char*, wchar_t const*/wchar_t*, std::string, std::wstring, std::exception, struct tm, struct in_addr, ACE_CString, ACE_WString, ACE_INET_Addr и ACE_Time_Value из библиотеки ACE; CComBSTR, CComVARIANT и CWindow из библиотеки ATL; BSTR, GUID и VARIANT для COM; CString и CWnd (и прочих оконных классов) из библиотеки MFC; struct dirent для UNIX; FILETIME, HWND, LSA_UNICODE_STRING и SYSTEMTIME для Windows. Кроме того, прокладки строкового доступа определены для всех типов из биб лиотеки STLSoft, которые являются строками или могут быть осмысленно пред ставлены в виде строки. Некоторые определения перегруженных вариантов про кладки c_str_ptr показаны в листинге 9.4. Обратите внимание на варианты для FILETIME, SYSTEMTIME и CWnd: это конвертирующие прокладки, они возвращают экземпляры классов, которые могут быть неявно преобразованы в тип char const*. Листинг 9.4. Объявления различных готовых прокладок строкового доступа // Âñå íàõîäÿòñÿ â ïðîñòðàíñòâå èìåí stlsoft // Èç ôàéëà stlsoft/shims/access/string.hpp char const* c_str_ptr(char const*); char const* c_str_ptr(std::string const&); char const* c_str_ptr(stlsoft::basic_simple_string const&); char const* c_str_ptr(stlsoft::basic_static_string const&); // Èç ôàéëà winstl/shims/access/string/time.hpp stlsoft::basic_shim_string c_str_ptr(FILETIME const& t); stlsoft::basic_shim_string c_str_ptr(SYSTEMTIME const& t); // Èç ôàéëà unixstl/shims/access/string/dirent.hpp char const* c_str_ptr(struct dirent const* d); char const* c_str_ptr(struct dirent const& d); // Èç ôàéëà mfcstl/shims/access/string/cwnd.hpp c_str_ptr_CWnd_proxy c_str_ptr(CWnd const& w);
Если не забывать о том, что прокладки строкового доступа подчиняются пра вилу, действующему для конвертирующих прокладок, и что видимый возвращае мый тип (char const* или wchar_t const*) не обладает семантикой значения, то вы сможете безо всяких проблем увеличивать гибкость и производительность своих компонентов.
Прокладки
107
Правило. Значение, возвращаемое прокладкой доступа, никогда не должно использо) ваться вне выражения, в котором эта прокладка вызывается.
В других библиотеках и коммерческих программах, которые я написал за по следние несколько лет, также определены перегруженные варианты прокладок строкового доступа, которые помещены в пространство имен stlsoft и потому автоматически могут использоваться совместно с STLSoft и друг с другом. Важно отметить, что здесь нет не только никакой связанности, но даже не включается никакая часть STLSoft, поскольку пространства имен открыты для расширения (раздел 5.3). Совет. Применяйте прокладки строкового доступа, чтобы достичь высокой сцепленности при минимальной или вовсе отсутствующей связанности.
Вывод из всего сказанного такой: библиотеки типа FastFormat или Pantheios, в которых используются прокладки строкового доступа, с самого начала облада ют высокой степенью обобщенности. Следовательно, их пользователи могут при ступать к работе, затратив минимум усилий; нужно лишь добавлять собственные расширения в виде дополнительных перегруженных вариантов прокладок по мере определения новых типов. Применение прокладок означает, что библиотека Pantheios следует принципам композиции, разнообразия, экономии, расширяемо% сти, генерации, наименьшего удивления, модульности, наибольшего удивления, на% дежности и разделения! (Если вы разочарованы тем, что в этой книге мы уделим так мало внимания такой мощной концепции, то надеюсь, что вы меня простите. Всетаки это книга о расширениях STL, в которых прокладки играют лишь не большую роль, а место дорого. Это не циничная маркетинговая уловка, призван ная склонить вас к покупке моей следующей книги Breaking Up the Monolith, в ко торой будет представлен весь спектр возможностей прокладок. Честное слово!)
Глава 10. Утка и гусь, или Занимательные основы частичного структурного соответствия Иногда меня смущает, что наш мозг так хо% рошо оптимизирует себя для решения стоя% щей в данный момент задачи. Он великолеп% но умеет отбрасывать знания, которые больше не нужны. – Шон Келли Читать код сложнее, чем писать. – Джоэл Спольски
10.1. Соответствие 10.1.1. Соответствие по имени Классически (я имею в виду – до появления шаблонов) считалось, что тип удовлетворяет требованиям, предъявляемым операцией, если у него в точности такое же имя, как указано в объявлении операции. Этот подход называется соот% ветствием по имени. На первый взгляд, совершенно очевидно; прозрение прихо дит только, когда принимаешь во внимание наследование. Все мы знаем из курсов по объектноориентированному программированию, за которые берут по $2000 за день, что тип B, который (открыто) наследует типу A, считается частным случаем типа A. Как принято говорить, B является A. class A {}; void func(A const& a); class B : public A {};
Следовательно, если операция определена в терминах указателей или ссылок на A, то она применима и к экземплярам B. Являясь уточнением A, тип B может наделять другим поведением все или некоторые операции, общие с A. В C++ это называется полиморфизмом и достигается с помощью механизма виртуальных функций (который все известные мне компиляторы реализуют в виде таблицы виртуальных функций и так называемого vptr.)
Утка и гусь, или Занимательные основы
109
class A { public: virtual void print() const { ::puts("A"); } }; class B : public A { public: virtual void print() const { ::puts("Ýòî êëàññ B!"); } }; void func(A const& a) { a.print(); } A a; B b; func(a); // Ïðàâèëüíî. a – ýêçåìïëÿð òèïà A func(b); // Ïðàâèëüíî. b – ýêçåìïëÿð òèïà B, êîòîðûé ÿâëÿåòñÿ ÷àñòíûì // ñëó÷àåì A
Сопоставление запрошенной операции и типа производится строго по имени (с учетом имен родительских классов), а это означает, что два идентичных типа не взаимозаменяемы. Иными словами: class C { public: virtual void print() const{ ::puts("C"); } }; C c; func(c); // Îøèáêà! C – ýòî íå A.
Установление соответствия по имени гарантирует, что тип экземпляра, к ко торому применяется операция, соответствует ожиданиям самой операции. Недо статок заключается в том, что эта операция применима только к типам, связанным отношением наследования с неким общим типом. Отметим, что соответствие по имени не предохраняет от диверсий. Напри мер, можно определить тип D следующим образом: class D : public A { public: virtual void print() const { ::exit(EXIT_FAILURE); } };
Но мы и не ожидаем, что язык программирования будет отслеживать такие вещи. Как мудро отметил один рецензент книги Imperfect C++, комментируя не которые мои «навороченные» попытки гарантировать безопасность: «Мы не рас считываем на Макиавелли». Думается мне, что это очень ценная мысль, иначе игра никогда не закончится. Поэтому примите следующий совет.
110
Основы
Совет. Когда пишете библиотеку, прилагайте разумные усилия к тому, чтобы защитить пользователя от случайных ошибок при работе с ней, но не тратьте время на то, чтобы воспрепятствовать заведомо злонамеренному использованию.
10.1.2. Структурное соответствие Альтернативный принцип структурного соответствия очень важен в обоб щенном программировании. Он утверждает, что сущности, обладающие одинако вой структурой, будут и вести себя одинаково. Имя, а зачастую и тип даже не рас сматриваются. Возьмем, к примеру, следующую обобщенную операцию: template void log(T const& t, int level) { if(T::defaultLevel >= level) { std::cout << t.c_str() << std::endl; } }
В ее сигнатуре нет ограничений на тип передаваемого параметра. Это неизме няемая ссылка на (const) T, где T – параметр шаблона. В теле функции предпола гается, что в этом типе имеется статический член с именем defaultLevel, кото рый можно сравнивать на «больше или равно» с int, и функциячлен c_str(), применимая к экземпляру. Компилятор сочтет пригодным для этого шаблона лю бой тип, удовлетворяющий этим двум требованиям. Поэтому совместно с log() можно использовать такие типы: class M { public: static const int defaultLevel = 10; char const* c_str() const { return "M"; } }; class N { public: static const int defaultLevel = 5; char const* c_str() const { return "N"; } }; M m; N n; log(m, 8); log(n, 8);
В том и в другом имеется константачлен defaultLevel и метод c_str(), ко торый возвращает строку, завершающуюся нулем.
Утка и гусь, или Занимательные основы
111
10.1.3. Утка и гусь Принцип структурного соответствия иногда называют Правилом утки. Правило утки. Если что)то выглядит, как утка, ходит, как утка, и крякает, как утка, то это утка и есть.
Более формально Правило утки применяется при работе с типами в языке Ruby, где называется «утиной типизацией» (Duck Typing). В нем не исследуется тип экземпляра на предмет определения того, можно ли для него вызывать неко торый метод; само наличие или отсутствие метода в этом экземпляре является единственным критерием. В Ruby это проверяется на этапе выполнения с помо щью механизма отражения, а не на этапе компиляции, как в C++. Но хотя этот менее строгий механизм сопоставления и придает обобщенно му программированию всю его мощь, у него есть немало недостатков. Уверен, что любезный читатель уже вообразил некие извращенные формы членов defaultLevel и c_str() в классах M и N. Но возможна и более общая степень син таксического соответствия. Рассмотрим следующие классы. Первый – это объ единение, в котором defaultLevel – перечисление, а c_str – переменнаячлен, имеющая тип вложенного класса, представляющего собой объектфункцию: union X { enum { defaultLevel = 10 }; struct c_str_fn { int operator ()() const { return 0; } }; c_str_fn c_str; };
А теперь напишем структуру, в которой есть статический член defaultLevel и статический метод c_str(): struct Y { public: static int defaultLevel; static char const* c_str() { return 0; } }; int Y::defaultLevel = 0;
Допустима даже следующая конструкция, в которой прочитавшие книгу Imperfect C++ легко узнают статическое доступное только для чтения свойство defaultLevel, реализованное в виде метода, который при обращении вызывает закрытый метод get_defaultLevel(). Полагаю, что многие программисты на
112
Основы
C++ будут удивлены, узнав, что в условном выражении в теле log() можно вызы вать функцию, определенную в типе T. struct Z { private: static int get_defaultLevel(); public: static stlsoft::static_method_property_get< int, int , Z, get_defaultLevel > defaultLevel; int c_str() const; }; stlsoft::static_method_property_get Z::defaultLevel; >
Отметим также, что метод c_str() возвращает не строку, а int. Так как опе ратор вставки в классе std::basic_ostream имеет перегруженный вариант и для int, и для char const*, все работает отлично, но заранее этого никто не ожидал. Однако каждый из типов X, Y и Z согласуется с требованиями функции log(), по этому программа откомпилируется и будет корректно выполнена. Я довольно подробно продемонстрировал, куда может завести необдуманное применение принципа структурного соответствия. Подобные примеры могут показаться надуманными, даже находящимися на грани абсурда, но представля ют собой вполне реальную опасность. Слабость этого принципа в том, что он по зволяет создавать структурно совместимые, но семантически несопоставимые типы. Поскольку компилятор ищет подходящий шаблон, базируясь на именах и типах символов, вопрос семантики его совершенно не занимает. Проблема усу губляется неудачно выбранными и противоречиво используемыми глаголами и прилагательными для имен методов. Например, в стандарте erase() и clear() – глаголы, а empty() – прилагательное. Но это лишь верхушка айсберга. В нетриви альных приложениях шаблонов очень легко не заметить, что утка крякает както странно. Мы встретимся с соответствующими примерами в части III, когда будем рассматривать попытки адаптировать итераторы с несовместимыми категориями ссылок на элементы. Одно из последствий применения Правила утки состоит в том, что в обобщенных шаблонных компонентах необходимо использовать огра ничения и контролировать соблюдение контракта всюду, где возможно, да еще и тщательно их тестировать. Рекомендация. При разработке расширений STL необходимо особенно широко приме) нять ограничения, контроль соблюдения контракта, а также готовить детальные наборы автономных тестов.
В качестве напоминания об опасностях принципа структурного соответ% ствия я сформулировал Правило гуся.
Утка и гусь, или Занимательные основы
113
Правило гуся. Нечто может выглядеть, как утка, ходить, как утка, и иногда даже крякать, как утка, а уткой не быть. Стоит поверить, что это утка, и сам будешь выглядеть гусаком!
10.2. Явное семантическое соответствие Возможно, вам интересно, можно ли объединить характеристики соответ% ствия по имени и структурного соответствия, получив и безопасность первого, и гибкость второго. Да, можно, причем несколькими способами.
10.2.1. Концепции Как вам, наверное, известно, многие алгоритмы в стандартной библиотеке распознают категорию итератора путем перегрузки по типу, характеризующему данный экземпляр итератора. Например, алгоритм std::distance() реализует ся примерно так, как показано в листинге 10.1. Листинг 10.1. Выбор исполняемой функции в зависимости от категории итератора // Â ïðîñòðàíñòâå èìåí std template typename iterator_traits::distance_type distance_impl(I from, I to, random_access_iterator_tag) { return to – from; } template typename iterator_traits::distance_type distance_impl(I from, I to, input_iterator_tag) { typename iterator_traits::distance_type n = 0; for(; from != to; ++from, ++n) {} return n; } template typename iterator_traits::distance_type distance(I from, I to) { return distance_impl( from, to , typename iterator_traits::iterator_category()); }
В листинге 10.2 показаны отношения наследования между типами тегов стан дартных категорий итераторов. Листинг 10.2. Отношения наследования между классами тегов стандартных категорий итераторов struct input_iterator_tag {};
114
Основы
struct output_iterator_tag {}; struct forward_iterator_tag : public input_iterator_tag , public output_iterator_tag {}; struct bidirectional_iterator_tag : public forward_iterator_tag {}; struct random_access_iterator_tag {};
В каждом неуказательном типе итератора с помощью одного из этих классов определяется типчлен iterator_category, который используется в общей фор ме шаблона std::iterator_traits для определения собственного типачлена iterator_category. Частичные специализации std::iterator_traits для ука зателей определяют типчлен iterator_category в виде std::random_access_ iterator_tag. (Детально этот вопрос рассматривается в главе 38.) Следователь но, компилятор может выбрать один из двух перегруженных вариантов функции distance_impl() в зависимости от того, представляет ли тип I итератор с произ вольным доступом или какуюто более слабую категорию.
10.2.2. Пометка с помощью типов"членов Другой способ определения того, какие требования удовлетворяются, осно ван на распознавании типовчленов. Например, поскольку мы постулировали, что STL%набор (раздел 2.2) должен предоставлять диапазоны итераторов с помощью методов begin() и end(), то вправе предположить, что в таком типе должны быть определены один или оба типачлена iterator и const_iterator. Применяя тех нику распознавания типовчленов, описанную в главе 13, мы можем выразить ограничения (глава 8) на типы, приемлемые для наших компонентов. Эта техника широко используется в книге, особенно для определения характеристик базовых типов адаптеров итераторов (глава 41).
10.2.3. Прокладки И в этой книге, и во втором томе мы неоднократно увидим, как принцип структурного соответствия расширяется с помощью прокладок, каждая из кото рых обладает явно выраженной семантикой. Например, определяя перегружен ный вариант прокладки c_str_ptr для некоторого типа, пользователь гаранти рует, что функция принимает константную ссылку на тип и возвращает либо ненулевой указатель на завершающуюся нулем строку символов, содержащую строковое представление экземпляра, либо временный экземпляр объекта, кото рый можно неявно преобразовать в такую строку. Следовательно, само имя про кладки несет в себе семантику. Естественно, при этом требуется, чтобы разработ чик не нарушал семантику прокладки, поэтому имена прокладок нужно выбирать разумно; отсюда и различные соглашения об именовании разных прокладок, упо мянутых в главе 9.
Утка и гусь, или Занимательные основы
115
10.3. Пересекающееся соответствие Закон дырявых абстракций (глава 6) и Правило гуся (раздел 10.1.3) противо речат друг другу. Первый говорит, что всякая абстракция протекает, а второе – что плохая абстракция отомстит за себя. Но я думаю, что этот конфликт можно уладить с помощью принципа пересекающегося соответствия, который положен в основу проектирования библиотек STLSoft (хотя формализован был совсем недавно). Когда несколько сущностей вроде бы структурно соответствуют друг другу, мы смотрим сквозь дырки в абстракциях, пытаясь выяснить, что между ними об щего. Затем мы определяем абстракцию, которая затрагивает только пересекаю щиеся семантически корректные структурные соответствия и допускает незначи тельные разумные уточнения без нарушения правила гуся. При такой методике самые монолитные адаптации остаются на этаже страте гии. Однако границы абстракций оказываются стертыми, а это означает, что пользователи, которым нужна функциональность, поддерживаемая лишь каким то подмножеством компонентов, реализующих данную абстракцию, должны бу дут выйти за ее пределы и тем самым смешать в одну кучу саму абстракцию и конкретные компоненты. Ктото сочтет такое решение беспорядочным, неэлеган тным, даже незрелым, но я полагаю, что, приняв во внимание самую суть C++ – производительность, мы придем к выводу, что этот подход не только приемлем, но зачастую является наиболее разумным способом реализации компонентов, на ходящихся на нижнем и среднем уровнях абстракции. Ну а что все это означает для бедного программиста, желающего писать на дежный, эффективный и переносимый код? Ответ прост: проектируйте и про граммируйте обобщенным образом, если абстракции достаточно четко сформули рованы и эффективны, и переходите к конкретному программированию, когда это не так. Конечно, это проще сказать, чем сделать на практике. Тут в первую очередь необходимо, чтобы у программиста был опыт и желание качественно выполнить свою работу. И то же самое относится к авторам библиотек. Никакой альтернати вы в красивом, элегантном, содержащем патологические ограничения и безна дежно медленном каркасе вы не найдете.
Глава 11. Идиома RAII Я хочу прибраться, потому что тогда у ме% ня будет больше места для игр. – Бен Уилсон – Папа, ты поможешь мне стать не таким капризным? – Гарри Уилсон Можно предположить, что все программисты на C++ знают об идиоме захват ре% сурса есть инициализация (RAII), даже если не пользуются этим термином. Смысл ее в том, чтобы получить ресурс – объект, память, дескриптор файла и т.д. – в кон структоре и освободить его в деструкторе. Классы, в которых так и делается, сле дуют идиоме RAII и часто называются классамиобертками. В главе 3 книги Imperfect C++ я ввел классификацию RAII, которая показа лась мне очень полезной. Она основана на сочетаниях двух характеристик: измен чивости и источника ресурса.
11.1. Изменчивость Если классобертка наделяет экземпляр, которому присваивается ресурс, до полнительными возможностями, я говорю об изменяющей RAII, в противном слу чае – о неизменяющей. С неизменяющей RAII работать проще, так как она не предоставляет никаких методов присваивания. Следовательно, деструктор может предполагать, что ин капсулированный ресурс все еще действителен. Примером может служить класс glob_sequence, рассматриваемый в главе 17. Напротив, классы, реализующие изменяющую RAII, должны предоставлять многие, если не все, из следующих возможностей: конструирование по умолчанию, конструирование и присваивание копированием, а также присваивание ресурса. И, что самое главное, они должны проверять – в деструкторе и в любом методе close(), – не стали ли ссылка на ресурс «нулевой», прежде чем освобождать его.
11.2. Источник ресурса Вторая характеристика классов, реализующих RAII, относится к способу, ко торым они получают управляемый ресурс. В классе типа std::string RAII ини циализируется внутри: класс сам создает ресурс – буфер, содержащий символы, –
Идиома RAII
117
которым управляет. Извне этот ресурс не виден. Напротив, для класса std::auto_ptr мы имеем RAII, инициализированную извне. Управляемый ресурс поступает из клиентского кода. Классы с внутренне инициализированной RAII реализовывать обычно проще, но они и более ограничительны, так как механизм захвата ресурса уже предписан и фиксирован. Однако их легче использовать или, точнее, труднее использовать неправильно, так как почти или даже вовсе невозможно допустить ошибку, кото рая приведет к утечке ресурса. К счастью, в большинстве расширений STL применяется внутренняя инициа лизация. Для многих наборов, не являющихся контейнерами (они не владеют соб ственными элементами), трудно или практически невозможно обеспечить семанти ку значения, поэтому, в отличие от STLконтейнеров, они обычно поддерживают неизменяющую RAII.
Глава 12. Инструменты для работы с шаблонами Палки и камни встречаются в природе, но не в виде рычагов и точек опоры. – Генри Петроски
12.1. Характеристические классы Пользователи STL никак не могут пройти мимо характеристических классов (traits) и, прежде всего, классов char_traits и iterator_traits. Характеристи ческий шаблонный класс (или просто характеристический класс) определяет протокол описания типа, а специализации этого класса описывают конкретный тип. Частичные специализации описывают множество типов, обладающих общи ми характеристиками. Некоторые характеристические классы предназначены для распознавания свойств типов, на основе которых они определяют собственные переменныечле ны и типычлены. Например, класс stlsoft::printf_traits позволяет пользо вателю определять форматные строки для конкретного интегрального типа при работе с семейством функций printf(), а также максимальное количество печа таемых символов (листинг 12.1). Листинг 12.1. Основной шаблон printf_traits //  ïðîñòðàíñòâå èìåí stlsoft template struct printf_traits { enum { size_min // Ñêîëüêî ñèìâîëîâ íåîáõîäèìî äëÿ ìèíèìàëüíîãî çíà÷åíèÿ // (+ nul) , size_max // Ñêîëüêî ñèìâîëîâ íåîáõîäèìî äëÿ ìàêñèìàëüíîãî çíà÷åíèÿ // (+ nul) , size // Ìàêñèìóì èç size_min è size_max }; static char const* format_a(); static wchar_t const* format_w(); };
Имеются специализации для стандартных интегральных типов, а также для интегральных типов, зависящих от компилятора. В листинге 12.2 показано, поче
Инструменты для работы с шаблонами
119
му этот характеристический класс так полезен при написании обобщенного кода, в котором используются функции из семейства printf(). Проблема в том, что в разных компиляторах форматная строка для вывода 64разрядного целого со знаком может записываться как %lld или как %I64d. Значения size_min и size_max определяются путем применения оператора sizeof() к сцепленной форме констант, равных минимальному и максимальному представимому значе нию (для этого необходима директива #define). Листинг 12.2. Специализация printf_traits для 64Zразрядных целых со знаком #define #define #define #define
STLSOFT_STRINGIZE_(x)# x STLSOFT_STRINGIZE(x) STLSOFT_STRINGIZE_(x) STLSOFT_PRTR_SINT64_MIN -9223372036854775808 STLSOFT_PRTR_SINT64_MAX 9223372036854775807
template <> struct printf_traits<sint64_t> { enum { size_min = sizeof(STLSOFT_STRINGIZE(STLSOFT_PRTR_SINT64_MIN)) , size_max = sizeof(STLSOFT_STRINGIZE(STLSOFT_PRTR_SINT64_MAX)) , size = (size_min < size_max) ? size_max : size_min }; static char const* format_a() { #if defined(STLSOFT_CF_64_BIT_PRINTF_USES_I64) return "%I64d"; #elif defined(STLSOFT_CF_64_BIT_PRINTF_USES_LL) return "%lld"; #else # error Íåîáõîäèìî óòî÷íèòü âîçìîæíîñòè êîìïèëÿòîðà #endif /* printf-64 */ } static wchar_t const* format_w(); // Êàê format_a(), íî ñ L"" };
Более сложный пример дает класс stlsoft::adapted_iterator_traits (глава 41), который и обеспечивает гибкость нескольких адаптеров итераторов при определении константности, категории итератора и категории ссылки на эле менты (глава 3), а также в применении выводимой адаптации интерфейса (гла ва 13) для компенсации отсутствующих типов итераторов. Прочие характеристические классы относятся главным образом к функцио нальности, специализация которой определяет реализацию функций, объявлен ных в основном шаблоне. Наглядный пример дает класс winstl:: drophandle_ sequence_traits с таким незатейливым определением: // Â ïðîñòðàíñòâå èìåí winstl template struct drophandle_sequence_traits { static UINT drag_query_file(HDROP hdrop, UINT index , C* buffer, UINT cchBuffer); };
120
Основы
Единственное назначение этого класса – вызвать ту из функций DragQueryFileA() и DragQueryFileW(), определенных в Windows API, которая соответствует символьному типу C.
Большинство характеристических классов предоставляют информацию о ти пе или значении и специализированную функциональность. В компонентах STL характеристики символов применяются преимущественно в виде параметра шаб лона, который по умолчанию совпадает со стандартным характеристическим классом. Например, вот как определен класс итератора ostream_iterator: //  ïðîñòðàíñòâå èìåí std template < typename V // Òèï âñòàâëÿåìîãî â ïîòîê çíà÷åíèÿ , typename C = char // Êîäèðîâêà ñèìâîëîâ , typename T = char_traits // Òèï õàðàêòåðèñòè÷åñêîãî êëàññà > class ostream_iterator;
В данном случае параметр T по умолчанию совпадает со специализацией std::char_traits, потому что обычно пользователь не предоставляет друго го характеристического класса. На практике я никогда не встречался с примерами использования, которые не принимали бы варианта по умолчанию. При написании расширений STL характеристические классы применяются повсеместно. Как и в компонентах из стандартной библиотеки, по умолчанию в качестве параметра принимается тип, ожидаемый большинством предполагае мых пользователей. Если требуется специальное или непредвиденное поведение, пользователь имеет механизм настройки, не требующий модификации исходного кода. В настоящее время в библиотеках STLSoft определено 56 характеристиче ских классов, и ниже мы опишем те, с которыми будем сталкиваться в этой книге.
12.1.1. Класс base_type_traits Я очень часто применяю класс base_type_traits, основной шаблон которого показан в листинге 12.3. (Я предпочитаю включать не связанные между собой константычлены в отдельные перечисления, тем самым отличая их от привыч ных перечислений, служащих для группировки нескольких возможных значений; пример см. в разделе 17.3.1). Листинг 12.3. Основной шаблон base_type_traits // Â ïðîñòðàíñòâå èìåí stlsoft template struct base_type_traits { enum { is_pointer = 0 }; enum { is_reference = 0 }; enum { is_const = 0 }; enum { is_volatile = 0 }; typedef T base_type; typedef T cv_type; };
Инструменты для работы с шаблонами
121
Как следует из самого названия, класс base_type_traits первоначально слу жил механизмом для приведения квалифицированного модификаторами const и volatile (cv) типа к его базовому типу. В процессе эволюции он стал детектором различных возможностей типа, например: является ли он указательным, ссылоч ным, константным и т.д. Полезная работа выполняется в частичных специализа циях. Для иллюстрации идеи в листинге 12.4 приведено определение двух таких специализаций: Листинг 12.4. Частичные специализации шаблона base_type_traits template struct base_type_traits { enum { is_pointer = 1 }; enum { is_reference = 0 }; enum { is_const = 0 }; enum { is_volatile = 1 }; typedef T base_type; typedef T volatile cv_type; }; template struct base_type_traits { enum { is_pointer = 0 }; enum { is_reference = 1 }; enum { is_const = 1 }; enum { is_volatile = 0 }; typedef T base_type; typedef T volatile cv_type; }; . . . // È òàê äàëåå äëÿ ïðî÷èõ ñî÷åòàíèé ìîäèôèêàòîðîâ const è volatile
Этот
класс
используется
в
нескольких
компонентах,
в
частности:
comstl::enumerator_sequence (раздел 28.5) и stlsoft::member_selector_ iterator (раздел 38.3).
12.1.2. Класс sign_traits Класс sign_traits применяется для распознавания знаковой и беззнаковой формы интегрального типа и получения противоположной формы. В листинге 12.5 показан основной шаблон и две его специализации. Листинг 12.5 Основной шаблон и две специализации класса sign_traits // Â ïðîñòðàíñòâå èìåí stlsoft template struct sign_traits; template <> struct sign_traits<sint32_t> { typedef sint32_t type; typedef sint32_t signed_type; typedef uint32_t unsigned_type; typedef uint32_t alt_sign_type;
122
Основы
}; template <> struct sign_traits { typedef uint32_t type; typedef sint32_t signed_type; typedef uint32_t unsigned_type; typedef sint32_t alt_sign_type; };
Стоит отметить два существенных момента. Вопервых, основной шаблон не определен, а только объявлен. Это означает, что его можно использовать только с типами, для которых определена явная специализация, например, с двумя пока занными выше. Совет. Объявляйте, но не определяйте основной шаблон для тех характеристических классов, которые используются с ограниченным и предсказуемым множеством совмес) тимых типов.
Вовторых, типычлены signed_type и unsigned_type одинаковы для ин тегрального типа одного и того же размера, тогда как типы type и alt_sign_type поменяны местами.
12.1.3. Свойства типа: мини"характеристики Характеристические классы очень удобны, когда нужно сообщить большой объем информации о типе. Но иногда нужно одно какоето свойство, и тогда по лезно определить тип, который я робко назвал минихарактеристикой.
12.1.4. Класс is_integral_type В стандарте (C++03: 3.9.1) определены четыре целочисленных типа со знаком: signed char, short int, int и long int, и четыре целочисленных типа без знака, unsigned char, unsigned short int, unsigne dint и unsigned long int. Они, а также bool, char и wchar_t в совокупности называются интегральными типа%
ми. В листинге 12.6 показан характеристический класс для распознавания таких типов. Листинг 12.6. Основной шаблон класса is_integral_type // Â ïðîñòðàíñòâå èìåí stlsoft template struct is_integral_type { enum { value = 0 }; typedef no_type type; };
В общем случае специализации is_integral_type обозначают, что специа лизирующий тип не является интегральным, так как константачлен value равна
Инструменты для работы с шаблонами
123
0, а типчлен type совпадает с no_type. (yes_type и no_type – два различных
типа в STLSoft, которые используются как булевские значения в метапрограмми ровании.) Чтобы этот характеристический класс был полезен, нам нужно специа лизировать его для интегральных типов, как показано в листинге 12.7. Листинг 12.7 Примеры специализации шаблона is_integral_type template <> struct is_integral_type<signed char> { enum { value = 1 }; typedef yes_type type; }; template <> struct is_integral_type { enum { value = 1 }; typedef yes_type type; };
В этих специализациях value равно 1, а type совпадает с yes_type. Наличие одновременно значения и типа упрощает использование минихарактеристик, из бавляя от необходимости вычислять одну характеристику по другой. Отметим, что ненулевое значение всегда задается равным 1, но проверяется на отличие от 0. Благодаря этому, если случайно указать не 1 в качестве ненулевого значения, ни чего страшного не произойдет. Совет. Всегда представляйте булевские константы для метапрограммирования значе) ниями 0 и 1. Но проверяйте их, сравнивая с 0.
Писать такие конструкции быстро надоедает (да и ошибку можно ненароком допустить), поэтому при всей своей нелюбви к макросам я всетаки определил один для специализации классов минихарактеристик: #define STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE(TR, T, V, MT) \ template <> struct TR \ { enum { value = V }; typedef MT type; };
Этот макрос используется следующим образом: // Â ôàéëå stlsoft/meta/is_integral_type.hpp STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE( is_integral_type , bool, 1, yes_type) STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE( is_integral_type , char, 1, yes_type) STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE( is_integral_type , wchar_t, 1, yes_type) . . . // È òàê äàëåå äëÿ signed+unsigned char, short, int, long
Большинство компиляторов поддерживают дополнительные типы целых чи сел, в том числе 64разрядных, поэтому в заголовочных файлах есть такие услов ные определения:
124
Основы
#ifdef STLSOFT_CF_64BIT_INT_SUPPORT STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE( is_integral_type , sint64_t, 1, yes_type) STLSOFT_GEN_TRAIT_SPECIALISATION_WITH_TYPE( is_integral_type , uint64_t, 1, yes_type) #endif /* STLSOFT_CF_64BIT_INT_SUPPORT */
Определив все необходимые специализации, мы сможем с помощью этого класса различать типы на этапе компиляции, что пригодится при метапрограмми ровании. В частности, это полезно в сочетании со статическими утверждениями (раздел 8.2) для определения ограничений (глава 8): template class UseIntegersOnly { . . . ~UseIntegersOnly() { STATIC_ASSERT(0 != is_integral_type::value); } };
Я предпочитаю пользоваться деструктором, поскольку он может быть всего один и в большинстве случаев, скорее всего, будет инстанцирован.
12.1.5. Класс is_signed_type Еще один минихарактеристический класс – is_signed_type. Делает он то же самое, что класс is_integral_type для целочисленных типов со знаком (C++03: 3.9.1) и других интегральных типов со знаком, дополнительно предос тавляемых конкретными компиляторами, а также для типов с плавающей точкой: float, double, и long double. Он используется в разделах 23.3.4 и 32.6.1 для реа лизации ограничений.
12.1.6. Класс is_fundamental_type Минихарактеристики можно комбинировать, как в классе is_fundamental_ type, который объединяет интегральные типы, типы с плавающей точкой, bool и void: Листинг 12.8. Определение составного характеристического класса is_fundamental_type // Â ïðîñòðàíñòâå èìåí stlsoft template struct is_fundamental_type { enum { value = is_integral_type::value || is_floating_point_type::value
Инструменты для работы с шаблонами
125
|| is_bool_type::value || is_void_type::value }; typedef typename select_first_type_if::type
type;
};
Константачлен value определена как логическое ИЛИ значений констант value в каждом из составной специализаций. Подошла бы и операция арифмети ческого ИЛИ, но логическое ИЛИ гарантирует, что если в какойнибудь мини характеристике истинное значение по ошибке определено как ненулевое зна чение, отличное от 1, то значение value в классе is_fundamental_type тем не менее будет равно 1, что и требуется. Совет. Применяйте логические операции И и ИЛИ, чтобы булевские константы в мета) программировании принимали значения 0 и 1 даже, если в составных типах значение true определено неправильно.
При определении типачлена type используется шаблон select_first_type_if, выступающий в роли предложения IF на этапе компиляции. Он обсуждается в главе 13.
12.1.7. Класс is_same_type Этот шаблонный класс определяет, совпадают ли два типа: Листинг 12.9. Определение шаблона is_same_type // Â ïðîñòðàíñòâå èìåí stlsoft template struct is_same_type { enum { value = 0 }; typedef no_type type; }; template struct is_same_type { enum { value = 1 }; typedef yes_type type; };
В общем случае значение константычлена value равно 0. Для частичных спе циализаций value равно 1. Хотя это простейшая минихарактеристика, которую только можно себе представить, она на удивление полезна и используется как в библиотеках STLSoft, так и в нескольких компонентах, описываемых в этом томе (главы 24 и 41).
126
Основы
12.2. Генераторы типов Генератор типа – это шаблон класса, единственное назначение которого – рас познать некоторые грани типов или значений, которыми он специализируется, и в соответствии с этим определить некий типчлен. Среди прочего, генераторы типов компенсируют отсутствие в языке конструкции typedef для шаблонов (псевдонимов шаблонов).
12.2.1. Класс stlsoft::allocator_selector Хороший пример генератора типа дает шаблон stlsoft::allocator_selector, применяемый для выбора подходящего распределителя по умолчанию в боль шинстве шаблонов классов в библиотеках STLSoft. Его определение приведено в листинге 12.10. Листинг 12.10. Определение шаблонаZгенератора allocator_selector // Â ïðîñòðàíñòâå èìåí stlsoft template struct allocator_selector { #if defined(STLSOFT_ALLOCATOR_SELECTOR_USE_STLSOFT_MALLOC_ALLOCATOR) typedef malloc_allocator allocator_type; #elif defined(STLSOFT_ALLOCATOR_SELECTOR_USE_STLSOFT_NEW_ALLOCATOR) typedef new_allocator allocator_type; #elif defined(STLSOFT_ALLOCATOR_SELECTOR_USE_STD_ALLOCATOR) typedef std::allocator allocator_type; #else /* êàêîé ðàñïðåäåëèòåëü? */ # error Îøèáêà ðàñïîçíàâàíèÿ . . . #endif /* allocator */ };
Шаблон allocator_selector применяется вместо std::allocator в списке параметров шаблона для многих компонентов, как показано ниже на примере шаблона auto_buffer (раздел 16.2): template< typename T // Òèï çíà÷åíèÿ , size_t N = 256 , typename A = typename allocator_selector::allocator_type > class auto_buffer;
В большинстве случаев после обработки приведенного выше определения пре процессором allocator_type оказывается определен как std::allocator. Но благодаря дополнительному уровню косвенности, обеспечиваемому шабло ном allocator_selector, можно выбрать и другой распределитель памяти, кото рый будет использоваться по умолчанию во всех компонентах, которые предъяв ляют какието экзотические требования; для этого достаточно всего лишь определить один символ препроцессора.
Инструменты для работы с шаблонами
127
12.3. Истинные typedef Истинные typedef подробно описаны в главе 18 книги Imperfect C++, поэтому здесь я буду краток. Как вам, несомненно, известно, typedef в C++ (как и в C) – не более чем псевдоним. Он не определяет нового типа. Например, следующий фраг мент содержит ошибку: typedef int typedef int
type_1; type_2;
void fn(type_1 v) {} void fn(type_2 v) {}
// Îøèáêà: fn(int) óæå îïðåäåëåíà!
Здесь не определяется два перегруженных варианта функции fn(): для типов type_1 и type_2. Но сделать это можно, если воспользоваться истинными typedef’ами. В шаблонном классе stlsoft::true_typedef используется тот факт, что каждая отличающаяся от других специализация шаблона (кроме свя занных отношением наследования) определяет уникальный тип. Поэтому решить поставленную задачу можно следующим образом: typedef stlsoft::true_typedef type_1; typedef stlsoft::true_typedef type_2;
Второй тип может быть любым при условии, что все сочетания обоих специа лизирующих типов различны. Теперь type_1 и type_2 – уникальные типы, кото рыми можно перегрузить функцию fn(): void fn(type_1 v) {} void fn(type_2 v) {} // Ïðàâèëüíî!
Глава 13. Выводимая адаптация интерфейса: адаптации типов с неполными интерфейсами на этапе компиляции Лучше молчать и казаться дураком, чем от% крыть рот и развеять все сомнения. – Авраам Линкольн Но нам нет необходимости знать латин% ский бит. Почему все всегда возвращаются к латинам? Это же было так давно. – Карл Пилкингтон
13.1. Введение Шаблонные адаптерные классы применяются для преобразования интерфей са существующего класса или группы взаимосвязанных классов к другому виду. Рассмотрим шаблонный класс std::stack, который применяется для адаптации последовательных контейнеров к интерфейсу, в котором имеются стековые опе рации push() и pop() (листинг 13.1). Этот подход работает, потому что все мето ды std::stack реализованы в терминах открытого интерфейса адаптируемого типа: типовчленов size_type и value_type и методов back() и push_back(). Листинг 13.1. Пример шаблонной функции и тестовый код для нее template void test_stack(S& stack) { stack.push(101); stack.push(102); stack.push(103); assert(3 == stack.size() && 103 == stack.top()); stack.pop(); assert(2 == stack.size() && 102 == stack.top()); stack.pop(); assert(1 == stack.size() && 101 == stack.top()); stack.pop(); assert(0 == stack.size()); } std::stack > deq;
Выводимая адаптация интерфейса
129
std::stack > vec; std::stack > lst; test_stack(deq); test_stack(vec); test_stack(lst);
В этой главе рассматривается вопрос о том, что делать, когда шаблон адаптера предъявляет требования, которым адаптируемый тип не может удовлетворить непосредственно. Можно ли расширить спектр адаптируемых типов, сделав адап тор более гибким? Я познакомлю вас с техникой выводимой адаптации интерфей са (inferred interface adaptation – IIA), в которой применяются три приема ме тапрограммирования: распознавание типа, исправление типа и выбор типа. Как будет ясно в главе 41, IIA полезна и для других вещей, в частности, чтобы заста вить код одинаково работать как с новой, так и старой реализацией стандартной библиотеки (именно так я и придумал технику IIA несколько лет назад).
13.2. Адаптация типов с неполными интерфейсами Рассмотрим шаблон класса sequence_range (листинг 13.2), который реали зует паттерн Iterator для STLнаборов (тех, что предоставляют STLитераторы с помощью методов begin() и end()), то есть для продвижения итератора вперед и получения текущего элемента используются методы advance() и current(). (Это урезанная версия одноименного компонента из библиотеки RangeLib; под робно мы будем рассматривать ее в томе 2.) Листинг 13.2. Первоначальная версия шаблонного адаптерного класса sequence_range //  ïðîñòðàíñòâå èìåí rangelib template class sequence_range { public: // Òèïû-÷ëåíû typedef typename C::referencereference; typedef typename C::const_reference const_reference; typedef typename C::iterator iterator; public: // Êîíñòðóèðîâàíèå sequence_range(C& c) : m_current(c.begin()) , m_end(c.end()) {} public: // Ìåòîäû ïàòòåðíà Iterator reference current() { return *m_current; } const_reference current() const { return *m_current; }
Основы
130 bool is_open() const { return m_current != m_end; } void advance() { ++m_current; } private: // Ïåðåìåííûå-÷ëåíû iterator m_current; iterator m_end; };
Чтобы поддержать изменяющий и неизменяющий доступ к элементам адап тируемого набора, метод current() перегружен. Изменяющий вариант возвра щает значение (неконстантного) типа reference, и неизменяющий – значение типа const_reference. Таким образом, допустимы такие три способа вызова: typedef sequence_range<std::vector > range_t; void f1(range_t& r); // Âûçûâàåòñÿ r.current() void f2(range_t const& r);// Âûçûâàåòñÿ r.current() range_t &r = . . . const range_t &cr = . . . f1(r); // íå-const ïåðåäàåòñÿ êàê íå-const - ïðàâèëüíî f2(r); // íå-const ïåðåäàåòñÿ êàê const - ïðàâèëüíî f2(cr); // const ïåðåäàåòñÿ êàê const - ïðàâèëüíî f1(cr); // const ïåðåäàåòñÿ êàê íå-const – îøèáêà êîìïèëÿöèè
Константные методы – вещь абсолютно необходимая, поэтому отсутствие столь типичного поведения в классе адаптера было бы неоправданным ограниче нием. Но, как мы увидим, удовлетворить такому элементарному требованию не такто просто.
13.3. Адаптация неизменяемых наборов Как будет показано в части II, многие реальные STLнаборы не предоставля ют изменяющих операций. Если адаптируемый набор не поддерживает изменяе мых (неconst) ссылок, то при реализации адаптера sequence_range из предыду щего раздела возникают проблемы. Посмотрим, что получится, если взять класс glob_sequence (раздел 17.3), который раскрывает неизменяющий интерфейс, показанный в листинге 13.3. Листинг 13.3. ТипыZчлены класса glob_sequence class glob_sequence { public: // Òèïû-÷ëåíû typedef char typedef char_type const* typedef value_type const& typedef value_type const* typedef glob_sequence
char_type; value_type; const_reference; const_pointer; class_type;
Выводимая адаптация интерфейса
131
typedef const_pointer const_iterator; typedef std::reverse_iterator const_reverse_iterator; . . .
Если попытаться адаптировать этот класс с помощью шаблона sequence_ range, то мы получим ошибки компиляции в определении типовчленов sequence_range. Точнее, компилятор сообщит, что в адапти руемом классе нет типовчленов reference и iterator. Нам нужно, чтобы адаптер на этапе компиляции понял, что он используется с типом, который не поддерживает изменяющих операций, и определил подходя щие замены, основываясь на открытом интерфейсе адаптируемого класса. Други ми словами, при адаптации класса glob_sequence адаптер должен вывести типы члены reference и iterator для класса sequence_range, как типычлены const_reference и const_iterator класса glob_sequence. В результате долж но получиться такое определение sequence_range. Листинг 13.4. Результирующее определение шаблона sequence_range template class sequence_range { . . . const_reference current() { return *m_current; } const_reference current() const; . . . private: // Ïåðåìåííûå-÷ëåíû const_iterator m_current; const_iterator m_end; };
13.4. Выводимая адаптация интерфейса Вывод адаптации интерфейса состоит из трех шагов: 1. Вывести интерфейс адаптируемого типа, пользуясь механизмом распозна вания типа (раздел 13.4.2). 2. Устранить несовместимости, пользуясь механизмом исправления типа (раздел 13.4.3). 3. Определить интерфейс типа адаптера в терминах реальных или исправ ленных типов адаптируемого типа, пользуясь механизмом выбора типа (раздел 13.4.1). Прежде чем начать разбираться в том, как работает IIA, посмотрим на резуль тат. В листинге 13.5 показано, как можно использовать IIA для вывода подходя щего типачлена iterator. (По просьбе рецензентов, для которых английский – не родной язык, уточняю, что слово putative означает «предполагаемый», «канди дат на роль».)
132
Основы
Листинг 13.5. Первое определение членов iterator в шаблоне sequence_range template class sequence_range { private: // Òèïû-÷ëåíû . . . // 1. Ðàñïîçíàâàíèå òèïà enum { C_HAS_MUTABLE_INTERFACE = . . . ???? . . . }; // 2. Èñïðàâëåíèå òèïà typedef typename typefixer_iterator::iterator putative_iterator; public: typedef typename C::const_iterator const_iterator; // 3. Âûáîð òèïà typedef typename select_first_type_if::type iterator; . . .
Значениечлен C_HAS_MUTABLE_INTERFACE – это константа времени компи ляции, которая показывает, предоставляет ли тип адаптируемого набора C изме няющий интерфейс. Это распознавание типа. Определение этого механизма мы дадим чуть ниже. Далее используется шаблон typefixer_reference, с помощью которого определяется типчлен putative_iterator, – это исправление типа. Наконец, шаблон select_first_type_if выбирает один из типов putative_ iterator или const_iterator для определения типачлена iterator – это вы бор типа.
13.4.1. Выбор типа Начнем с простой части – выбора типа. В метапрограммирование шаблонов это устоявшаяся идиома, состоящая из основного шаблона и частичной специали зации. В библиотеках STLSoft для этого предназначен шаблон выбора типа select_first_type_if, показанный на рис. 13.6. Листинг 13.6. Определение шаблона выбора типа select_first_type_if // Â ïðîñòðàíñòâå èìåí stlsoft template struct select_first_type_if { typedef T1 type; // Ïåðâûé òèï }; template struct select_first_type_if { typedef T2 type; // Âòîðîé òèï };
Выводимая адаптация интерфейса
133
Если третий булевский параметр равен true, выбирается первый тип, а если false – второй. Следовательно, select_first_type_if::type равно int, а select_first_type_if::type равно char.
13.4.2. Распознавание типа Следующая стоящая перед нами задача, пожалуй, самая головоломная. При ее решении используется принцип SFINAE для определения шаблона, который мо жет распознать типычлены. Аббревиатура SFINAE расшифровывается как Substitution Failure Is Not an Error (неудача при подстановке не есть ошибка). Этот механизм применяется компиляторами при идентификации шаблонов функций. По сути, принцип SFINAE утверждает, что если специализация шаблона функции заданным аргументом могла бы привести к ошибке, то это не считается ошибкой, коль скоро существует подходящая альтернатива. (К счастью, глубоко понимать принцип SFINAE необязательно для того, чтобы успешно им пользоваться, что убедительно демонстрируется способностью автора забывать тонкие детали через пять минут после того, как он в них разобрался, и тем не менее писать зависящий от них адаптивный код.) В STLSoft есть целый ряд компонентов для распознава ния типа, в том числе has_value_type (листинг 13.7), который определяет, опре делен ли в классе типчлен value_type. Листинг 13.7. Определение шаблонного класса для проверки наличия типаZчлена has_value_type // Â ïðîñòðàíñòâå èìåí stlsoft typedef struct { char ar[1]; } one_t; // sizeof(one_t) . . . typedef struct { one_t ar[2]; } two_t; // . . . != sizeof(two_t) template one_t has_value_type_function(...); template two_t has_value_type_function(typename T::value_type const volatile*); template struct has_value_type { enum { value = sizeof(has_value_type_function(0)) == sizeof(two_t) }; }; template<> struct has_value_type { enum { value = 0 }; };
Хотя выглядит этот код как неудобоваримая мешанина, на самом деле, разоб равшись с отдельными частями, мы поймем, что он не так уж сложен. Специализа
Основы
134
ция has_value_type типом T включает в том числе определение того, какое инстанцирование шаблона функции has_value_type_function() наилучшим образом соответствует аргументу 0. Второй шаблон, имеющий аргумент typename T::value_type const volatile* более специфичен, чем первый, кото рый принимает любые аргументы (в C/C++ это обозначается многоточием …), и может быть сопоставлен с 0 (так как 0 – это и указательный литерал, и целочис ленный литерал) для любого типа T, который имеет типчлен value_type. Это и есть распознавание типа, так как has_value_type::value будет равно 1, если в T есть типчлен value_type, и 0 – в противном случае. Если в T не определен типчлен value_type, то будет выбран вариант с многоточием, а принцип SFINAE говорит, что такая специализация не приводит к ошибке компиляции. (Обратите внимание на специализацию шаблона has_value_type типом void. В этой главе она не используется, но будет нужна для приложения IIA в главе 41.) Теперь можно посмотреть, как определяется значение C_HAS_MUTABLE_ INTERFACE. Мы выбираем типчлен, который должен быть присутствовать только в изменяемом наборе, – скажем, iterator – и распознаем его с помощью подходя щим образом определенного детектора типа has_iterator: enum { C_HAS_MUTABLE_INTERFACE = has_iterator::value };
Учитывая несовершенство некоторых реализаций стандартной библиотеки и расширений STL, мы поступим разумно, проявив осторожность, и попытаем ся распознать несколько типовчленов, характерных только для изменяемых наборов: enum { C_HAS_MUTABLE_INTERFACE =
has_iterator::value && has_pointer::value };
13.4.3. Исправление типа Теперь мы умеем распознавать, предоставляет ли набор изменяющий интер фейс, и знаем, как выбрать тип. Осталось только исправить типы. Наивная попыт ка приведена в листинге 13.8. Листинг 13.8. Неправильное определение членов reference в шаблоне sequence_range enum { C_HAS_MUTABLE_INTERFACE =
has_iterator::value && has_pointer::value }; typedef typename select_first_type_if::type reference; typedef typename C::const_reference const_reference;
Проблема в том, что шаблон select_first_type_if специализируется типа ми C::reference и C::const_reference. Если в типе C не определен типчлен reference, то такая специализация select_first_type_if, а, следовательно, и sequence_range в целом недопустима, и компилятор выдаст ошибку. На помощь
Выводимая адаптация интерфейса
135
снова приходит частичная специализация шаблона, на этот раз в форме основного шаблона fixer_reference и его частичной специализации (листинг 13.9). Листинг 13.9. Определения шаблонного класса для исправления типа fixer_reference // Â ïðîñòðàíñòâå èìåí stlsoft template struct fixer_reference { typedef typename T::reference reference; }; template struct fixer_reference { typedef void reference; };
Первый параметр T – это тип набора. Второй параметр указывает, есть ли в этом наборе типчлен reference. В основном шаблоне класса типчлен reference определен по типучлену reference из типа набора. В той частичной специализации, где второй параметр равен false (то есть T не имеет типачлена reference), тип reference определяется с помощью typedef как void. Это и есть исправление типа. Располагая этим механизмом, мы можем ссылаться на тип член reference для таких типов наборов, в которых этот типчлен не определен. Следующее выражение: typedef typename typefixer_reference< C , C_HAS_MUTABLE_INTERFACE >::reference putative_reference;
компилируется вне зависимости от того, равна ли константа C_HAS_MUTABLE_ INTERFACE true или false. Если она равна true, то typefixer_ reference::reference вычисляется как C::reference. Следовательно, выражение select_first_type_if< putative_reference , const_reference , C_HAS_MUTABLE_INTERFACE >::type
принимает вид: select_first_type_if< C::reference , C::const_reference , true >::type
а это, в свою очередь, оказывается равным C::reference. Напротив, если C_HAS_MUTABLE_INTERFACE равно false, то typefixer_reference::reference вычисляется как void. Следова тельно, выражение:
136
Основы
select_first_type_if< putative_reference , const_reference , C_HAS_MUTABLE_INTERFACE >::type
принимает вид: select_first_type_if< void , C::const_reference , false >::type
а это выражение равно C::const_reference. Ни в какой точке не возникает несуществующий тип – вместо него подставля ется тип void, – поэтому код остается приемлемым для компилятора. Разумеется, если в адаптируемом типе не определены типычлены const_iterator или const_reference, то компилятор все равно будет «ругаться». Но ожидать, что адаптер сможет справиться с таким случаем – это уже идеализм; вполне разумно потребовать, чтобы пользователи применяли адаптер sequence_range только к таким типам, в которых есть, по крайней мере, типычлены const_iterator и const_reference, а также методы begin() и end().
13.5. Применение IIA к диапазону Включив все рассмотренное выше в шаблон класса sequence_range, мы по лучим определение, показанное в листинге 13.10. Листинг 13.10. Окончательное определение членов iterator и reference в шаблоне sequence_range private: // Òèïû-÷ëåíû enum { C_HAS_MUTABLE_INTERFACE = has_iterator::value && has_pointer::value }; typedef typename typefixer_reference< C , C_HAS_MUTABLE_INTERFACE >::reference putative_reference; typedef typename typefixer_iterator< C , C_HAS_MUTABLE_INTERFACE >::iterator putative_iterator; public: typedef typename C::const_reference const_reference; typedef typename select_first_type_if::type reference; typedef typename C::const_iterator const_iterator; typedef typename select_first_type_if::type iterator; . . . reference current()
Выводимая адаптация интерфейса
137
{ return *m_current;
// Òåïåðü ðàáîòàåò äëÿ èçìåíÿåìûõ è íåèçìåíÿåìûõ // íàáîðîâ
} const_reference current() const; . . .
Теперь адаптер работает для типов, которые поддерживают изменяющие и неизменяющие операции, а также для типов, поддерживающих только неизме няющие операции. В реальном определении шаблона sequence_range в библио теке RangeLib есть дополнительные ухищрения, необходимые для того, чтобы адаптер можно было параметризовать константными типами наборов, но они так же решаются путем использования принципа IIA. Можете сами посмотреть на реализацию. Мы еще встретимся с этой техникой при рассмотрении характеристического класса adapted_iterator_traits (глава 41) – повторно используемого в мета программировании компонента, который обеспечивает дополнительную гиб кость при написании адаптеров итераторов.
Глава 14. Гипотеза Хенни, или Шаблоны атакуют! Несогласованность – причина ненужного умственного напряжения в труде разработ% чика. – Скотт Мейерс Чтобы написать хорошую библиотеку шаблонов на C++, нужно соблюсти тонкое равновесие между невероятной мощью, скрытой в программировании шаблонов, и излишней усложненностью и непонятными интерфейсами, которые могут по ставить пользователя в тупик. Кевлин Хенни сформулировал соотношение, в ко тором ухвачена самая суть этого баланса; я называю его гипотезой Хенни. Гипотеза Хенни. При добавлении каждого [обязательного] параметра шаблона число потенциальных пользователей уменьшается вдвое.
Слово «обязательный» – мой личный скромный вклад в эту гипотезу. Думаю, что такое уточнение важно, поскольку тяжкой ношей является именно количе ство параметров шаблона, которые пользователь обязан понимать, чтобы им вос пользоваться. Например, употребляя шаблон std::map, вы не отшатываетесь в ужасе при взгляде на четыре его параметра: тип ключа (K), тип отображенного значения (MT), предикат для сравнения ключей и распределитель памяти. После дние два по умолчанию равны std::less и std::allocator<MT>, и в боль шинстве случаев этого достаточно. То же самое можно сказать о шаблонах функ ций. Возьмем, к примеру, функцию dl_call() (раздел 16.6) с N параметрами; она реализована в подпроектах UNIXSTL и WinSTL библиотек STLSoft. В листинге 14.1 приведены объявления перегруженных вариантов этой функции с 2 и 32 па раметрами. Листинг 14.1. Гетерогенные параметры шаблона обобщенной функции template < typename R, typename L, typename FD , typename A0, typename A1 > R dl_call(L const& library, FD const& fd , A0 a0, A1 a1); template < typename R, typename L, typename FD , typename A0, . . ., typename A30, typename A31
Гипотеза Хенин, или Шаблоны атакуют!
139
> R dl_call(L const& library, FD const& fd , A0 a0, . . . , A30 a30, A31 a31);
На первый взгляд складывается впечатление, что это крайняя степень нару шения гипотезы. Однако компилятор сам выведет типы аргументов library, де скриптора функции fd и «фактических» аргументов ( a0 . . . a(N-1)). От пользо вателя требуется только задать тип возвращаемого значения: CHAR name[200]; DWORD cch = dl_call( "KERNEL32", "S:GetSystemDirectoryA" , &name[0], STLSOFT_NUM_ELEMENTS(name));
В части II мы увидим несколько компонентов, нарушающих гипотезу Хенни, в частности, string_tokeniser (раздел 27.6). Его интерфейс абсолютно понятен при использовании в типичных ситуациях, например для разбиения строки на лексемы, когда разделителем является одиночный символ,: stlsoft::string_tokeniser<std::string, char> tokens("ab|cde||", '|');
Понятнее просто не придумаешь, и ваш код выглядит совершенно прозрачно. Но в других, тоже разумных случаях применения, скажем когда строка разбивает ся по любому из набора разделительных символов, интерфейс превращается в творение безумной вязальщицы; см. листинг 14.2. (Вам будет приятно узнать, что есть и более пристойные способы разбить строку по набору символов; см. раз делы 27.8.1 и 27.8.2.) Листинг 14.2. Пример нарушения гипотезы Хенни template struct charset_comparator; // Êîìïàðàòîð (ñì. ðàçäåë 27.7.5) stlsoft::string_tokeniser<std::string , std::string , stlsoft::skip_blank_tokens<true> , std::string , stlsoft::string_tokeniser_type_traits<std::string , std::string > , charset_comparator<std::string> > tokens("ab\tcde \n", " \t\r\n");
Я буду отмечать все случаи нарушения гипотезы Хенни, и мы посмотрим, как можно избежать последствий такого попирания закона или хотя бы сгладить их. А в томе 2 мы обсудим продвинутые приемы метапрограммирования, которые по зволят окоротить чрезмерно разросшиеся списки параметров шаблона. Надеюсь, что вы примете во внимание наблюдение Келвина в собственной работе и будете учитывать, как оно влияет на количество пользователей, а, стало быть, и на успешность вашей библиотеки. Для себя я вывел такое правило: если приходится обращаться к документации, чтобы понять смысл более одного пара метра шаблона, то интерфейс необходимо доработать или предложить альтерна тивный (см. раздел 27.8).
Глава 15. Независимые автономии друзей equal() Настоящая дружба не бывает безоблачной. – Маркиз де Савиньи В своей «Этике» Аристотель писал о дружбе между равными и неравными друзь ями и рассуждал о взаимных обязательствах, необходимых для поддержания доб рых отношений. В этой главке я покажу, как отказ от дружбы может упростить реализацию и помочь избежать ненужного нарушения инкапсуляции.
15.1. Опасайтесь неправильного использования функций"друзей, не являющихся членами Принцип Скотта Мейерса, гласит, что использование функций, не являющих ся членами класса, повышает степень инкапсуляции по сравнению с функциями членами. Он достоин всяческих похвал и широко применяется. И я следую ему всюду, где это оправдано. Но распространен – и совершенно напрасно – один слу чай неправильного применения этого принципа; речь идет об операторах сравне ния. Взгляните на следующее возможное определение операторов равенства (или неравенства) для класса basic_path из подпроекта UNIXSTL: Листинг15.1. Определение операторов в классе basic_path //  ïðîñòðàíñòâå èìåí unixstl template < typename C // Òèï ñèìâîëà , typename T = filesystem_traits , typename A = std::allocator > class basic_path { public: // Òèïû-÷ëåíû typedef basic_path class_type; . . . public: // Ñðàâíåíèå bool operator ==(class_type const& rhs) const; bool operator ==(C const* rhs) const; . . .
Независимые автономии друзей equal()
141
Вроде бы все нормально, но такое определение означает, что при любом срав нении с экземпляром класса basic_path этот экземпляр должен находиться в ле вой части оператора: unixstl::basic_path p1; unixstl::basic_path p2; p1 == p2; p1 == "some-file-name"; "other-file-name" == p2;
// Ïðàâèëüíî // Ïðàâèëüíî // Îøèáêà êîìïèëÿöèè
Чтобы последняя синтаксическая форма тоже была допустима, оператор сле дует определить как функцию, не являющуюся членом класса. Часто при этом употребляют ключевое слово friend, как показано в листинге 15.2. Листинг 15.2. Определение класса basic_path, в котором операторы сравнения являются свободными дружественными функциями template <. . .> class basic_path { . . . public: // Ñðàâíåíèå friend bool operator ==(class_type const& lhs , class_type const& rhs) const; friend bool operator ==(class_type const& lhs , C const* rhs) const; friend bool operator ==(C const* lhs , class_type const& rhs) const; . . .
А теперь вспомните о существовании неочевидных и тонких правил, касаю щихся отношений между классами, дружественными им свободными функция ми, пространствами имен в C++, компоновщиком и т.д. и т.п. Некоторые компи ляторы требуют, чтобы функция была определена внутри тела класса. Некоторые настаивают на опережающем объявлении. Не могу растолковать все такие прави ла, потому что сам их не знаю! И не случайно. Всякий раз, как я пытался их усво ить, а потом применить к нескольким компиляторам, у меня портилось настрое ние. (На компактдиске есть пример программы, на котором демонстрируется возникающая путаница.) Но очень просто, не уклоняясь от рекомендации Мейерса, написать класс с лаконичным определением, строгой инкапсуляцией и понятным интерфей сом. Вместо того чтобы определять методы, как показано выше, я определю от крытую неоператорную функциючлен – назовем ее equal() или compare(), – а затем реализую через нее функции сравнения, не являющиеся членами. Для шаблонного класса basic_path реализация операторов == и != показана в ли стинге 15.3.
Основы
142 Листинг 15.3. Класс basic_path с методом equal() и свободными операторными функциями template <. . .> class basic_path { . . . public: // Ñðàâíåíèå bool equal(class_type const& rhs) const; bool equal(C const* rhs) const; . . . }; template bool operator ==( basic_path const& , basic_path const& { return lhs.equal(lhs); } template bool operator ==( basic_path const& , C const* { return lhs.equal(rhs); } template bool operator ==( C const* , basic_path const& { return rhs.equal(lhs); } . . . // Àíàëîãè÷íî äëÿ îïåðàòîðà !=()
lhs rhs)
lhs rhs)
lhs rhs)
Аналогичные реализации в терминах метода compare() можно написать для любых классов, в которых нужны операции <, <=, >, или >=. Все же уточним, что такой прием нарушает букву принципа Скотта, так как добавляется одна или не сколько функцийчленов, в данном случае equal(). Но в результате получается совершенно прозрачная реализация, в которой дружественность становится не нужной, и тем самым удается избежать проблем с переносимостью изза сложных и не всегда хорошо поддержанных правил, касающихся определений friend функций. Эта техника позволила мне свести число употреблений ключевого сло ва friend в библиотеках STLSoft к минимуму (не более сотни в общей сложно сти). Я применяю ее во всех примерах, приведенных в этой книге. Совет. Избегайте неправильного использования дружественности при написании опе) раторных функций сравнения, не являющихся членами. Вместо этого определяйте не)операторную неизменяющую унарную открытую функцию)член, в терминах которой можно выразить не)дружественные бинарные операторы сравнения, не являющиеся членами.
Независимые автономии друзей equal()
143
15.2. Наборы и их итераторы Есть один типичный случай, когда дружественность, на мой взгляд, полезна при определении наборов и ассоциированных с ними классов итераторов. Итера торам часто необходим доступ к ресурсам, которые не должны быть видны клиен тскому коду. Естественно, что соответствующие члены объявляются закрытыми. Набор передает такие ресурсы экземпляру итератора с помощью конструктора преобразования. Но если бы конструктор преобразования был открытым, то кли ентский код мог бы использовать его для некорректной инициализации итерато ра. Поэтому конструктор преобразования делают закрытым, а класс набора, кото рому только и есть до него дело, объявляют другом класса итератора. Примерами могут служить наборы readdir_sequence (глава 19), Fibonacci_sequence (гла ва 23) и environment_map (глава 25). В других случаях, где я использовал дружественность, уже и так наличество вала тесная связанность, поэтому употребление слова friend не ухудшало ситуа цию. В качестве примеров упомяну итераторы вывода, основанные на паттерне Dereference Proxy (глава 35), класс CArray_adaptor_base и определенные в нем адаптеры класса и экземпляра (глава 24), а также классы адаптеров распределите лей, которые будут рассмотрены во втором томе.
Глава 16. Важнейшие компоненты Желание победить – ничто без желания подготовиться. – Юма Иканга Программисты готовы работать очень усердно для того, чтобы один раз решить задачу и никогда к ней больше не возвра% щаться. – Шон Келли
16.1. Введение В этой главе описываются пять компонентов из библиотек STLSoft, которые нашли применение при реализации многих расширений, рассматриваемых в час тях II и III. В их число вошли один интеллектуальный указатель, в котором идио ма RAII применяется к произвольным типам (stlsoft::scoped_handle), два компонента для работы с памятью (stlsoft::auto_buffer и unixstl/winstl:: file_path_buffer), характеристический класс для абстрагирования различий между файловыми системами в двух ОС (unixstl/winstl::filesystem_traits) и инструментарий для безопасного вызова функций из динамически загружае мых библиотек (unixstl/ winstl::dl_call).
16.2. Класс auto_buffer Выделение блока памяти из стека производится очень быстро, но размер бло ка должен быть известен на этапе компиляции. На выделение памяти из кучи ухо дит гораздо больше времени, но зато размер блока может быть произвольным и определяется на этапе выполнения. Шаблонный класс auto_buffer обеспечивает оптимизированное выделение памяти в локальной области видимости, сочетая быстроту стековой памяти с динамичностью кучи. (Хотя массивы переменной длины в стандарте C99 и нестандартная функция alloca()пытаются достичь того же компромисса, но этих попыток недостаточно для большинства целей C++; полное обсуждение см. в главе 32 книги Imperfect C++). В классе auto_buffer применена простая уловка – поддерживается внутрен ний буфер фиксированного размера, из которого и выделяется память, если это возможно. Если размер запрошенного блока превышает размер буфера, память
Важнейшие компоненты
145
выделяется из кучи с помощью параметризованного распределителя. Взгляните на следующий код: size_t n = . . . stlsoft::auto_buffer<wchar_t, 64> buff(n); std::fill_n(&buff[0], L’\0', buff.size());
Если n не больше 64, никакого выделения из кучи не будет, а выражение &buff[0] равно адресу первого элемента 64элементного массива элементов типа wchar_t – внутреннего буфера. Поскольку этот массив представляет собой пере меннуючлен buff, то память для него выделена в том же кадре стека, что и для buff (оттуда же распределена и память для n). Если n больше 64, то внутренний буфер не используется, а конструктор buff пытается получить внешний буфер с помощью своего распределителя. Либо этот запрос будет удовлетворен, либо конструктор возбудит исключение std::bad_alloc. Перед классом auto_buffer стоит двоякая цель. 1. Он обеспечивает простую абстракцию областей неформатированной памя ти динамического размера, которая необходима для реализации расшире ний STL. 2. Он существенно ускоряет выделение памяти в типичном случае, когда для большинства запросов нужны блоки небольшого заранее известного раз мера. Особенно полезно это при работе с C API, и, как вы неоднократно будете убеждаться, этот компонент используется чрезвычайно широко.
16.2.1. Это не контейнер! Класс auto_buffer предоставляет методы, которые обычно имеются у STL контейнеров, но не является совместимым со стандартом контейнером. Он не инициализирует и не уничтожает свое содержимое. Хотя auto_buffer и допус кает изменение размера, его содержимое копируется побитово (с помощью memcpy()), в не методом конструирования на месте и уничтожения, как того требует стандарт от контейнеров. Метод swap() в классе auto_buffer имеется, но ни конструктор копирования, ни копирующий оператор присваивания не оп ределены. Далее, в объектах auto_buffer можно хранить только простые типы (POD plain old data). (PODтипом называется любой тип, который можно представить в языке C.) Это гарантируется следующим ограничением (глава 8) в конструкторе: template <. . .> auto_buffer::auto_buffer(. . .) { stlsoft_constraint_must_be_pod(value_type); . . .
Интерфейс класса включает методы empty(), size(), resize(), swap(), а также изменяющие и неизменяющие формы begin() и end() (и rbegin(), rend()), но они служат лишь для удобства реализации классов в терминах
146
Основы
auto_buffer, а не как намек на то, что его можно или следует использовать в ка
честве STL%контейнера. В главе 22 показано, что наличие такого интерфейса по зволяет очень просто и прозрачно реализовывать типы наборов на основе auto_buffer.
16.2.2. Интерфейс класса В листинге 16.1 приведено определение интерфейса класса auto_buffer. Листинг 16.1. Определение шаблона класса auto_buffer template< typename T // Òèï çíà÷åíèÿ , size_t N = 256 , typename A = typename allocator_selector::allocator_type > class auto_buffer : protected A { public: // Òèïû-÷ëåíû . . . // Ðàçëè÷íûå îáùåóïîòðåáèòåëüíûå òèïû: value_type, pointer è ò.ä. public: // Êîíñòðóèðîâàíèå explicit auto_buffer(size_type dim); ~auto_buffer() throw(); public: // Îïåðàöèè void resize(size_type newDim); void swap(class_type& rhs); public: // Ðàçìåð bool empty() const; size_type size() const; static size_type internal_size(); public: // Äîñòóï ê ýëåìåíòàì reference operator [](size_type index); const_reference operator [](size_type index) const; pointer data(); const_pointer data() const; public: // Èòåðàöèÿ iterator begin(); iterator end(); const_iterator begin() const; const_iterator end() const; reverse_iterator rbegin(); reverse_iterator rend(); const_reverse_iterator rbegin() const; const_reverse_iterator rend() const; private: // Ïåðåìåííûå-÷ëåíû pointer m_buffer; size_type m_cItems; value_type m_internal[N]; private: // Íå ïîäëåæèò ðåàëèçàöèè auto_buffer(class_type const&); class_type& operator =(class_type const&); };
Важнейшие компоненты
147
Только два метода влияют на размер, а значит, и на внутреннюю организацию: resize() и swap(). Статический метод internal_size() возвращает размер
внутреннего буфера для данной специализации шаблона. Семантика всех осталь ных методов такая же, как можно ожидать от контейнера (хотя класс auto_ buffer таковым и не является). Оба перегруженных варианта оператора индекси рования в своем предусловии проверяют, что переданный индекс имеет допусти мое значение. Отметим, что здесь используется обсуждавшийся в разделе 12.2.1 шаблонге нератор allocator_selector с тем, чтобы выбрать распределитель, подходящий для данного компилятора, библиотеки или контекста. Для простоты можете счи тать, что этот параметр шаблона просто равен std::allocator.
16.2.3. Копирование В классе auto_buffer не определен конструктор копирования, и тому есть ос новательная причина: наличие конструктора копирования позволило бы компи лятору генерировать неявные конструкторы копирования для классов, в которых есть член типа auto_buffer. Но, поскольку этот класс управляет неинициализи рованной, а точнее инициализированной внешней программой памятью для PODтипов, то это привело бы к ошибкам в тех случаях, когда просто копировать элементы недостаточно, например, когда элементы – это указатели. Мы встре тимся с такой ситуацией при рассмотрении класса unixstl::glob_sequence (глава 17). Коль скоро конструктор копирования в классе auto_buffer запрещен, то авторы построенных на его основе типов вынуждены будут думать о послед ствиях, как, скажем, в определении копирующего конструктора в классе winstl::pid_sequence (раздел 22.2).
16.2.4. Воспитанные распределители идут последними В определении auto_buffer в версиях STLSoft, предшествующих 1.9, список параметров шаблона выглядел так: template< typename T , typename A = typename allocator_selector::allocator_type , size_t N = 256 > class auto_buffer;
Не буду ходить вокруг да около, а прямо скажу, что это откровенная ошибка. И чуть ли не всякий раз, пользуясь auto_buffer, я проклинаю себя за эту ошибку, поскольку размер практически всегда задается явно, а распределитель – очень редко. Совет. Старайтесь делать распределитель памяти последним в списке параметров шаб) лона, если не существует каких)либо противопоказаний.
148
Основы
Внесение этого изменения при переходе от версии 1.8 к 1.9 потребовало нема ло усилий, особенно в библиотеках, зависящих от STLSoft. Но в этом и в других случаях меня выручила одна вещь, которую я делаю всегда, – помещаю информа цию о версии в виде распознаваемых препроцессором символов во все исходные файлы. Заглянув в любую из моих библиотек, вы увидите в заголовках примерно такие строки: // File: stlsoft/memory/auto_buffer.hpp #define STLSOFT_VER_STLSOFT_MEMORY_HPP_AUTO_BUFFER_MAJOR #define STLSOFT_VER_STLSOFT_MEMORY_HPP_AUTO_BUFFER_MINOR #define STLSOFT_VER_STLSOFT_MEMORY_HPP_AUTO_BUFFER_REVISION #define STLSOFT_VER_STLSOFT_MEMORY_HPP_AUTO_BUFFER_EDIT
5 0 5 146
Обратная совместимость других библиотек, в которых использовался компо нент auto_buffer, была обеспечена путем ветвления на основе информации о версии. Совет. Помещайте распознаваемую препроцессором информацию о версии в заголо) вочные файлы библиотек, чтобы пользователям было проще обеспечить обратную совме) стимость.
16.2.5. Метод swap() Поскольку класс auto_buffer – низкоуровневый компонент, в нем определен метод swap() для обеспечения эффективности и безопасности относительно ис ключений. Однако важно понимать, что не всегда он гарантирует постоянное вре мя выполнения. В случае, когда один или оба обмениваемых экземпляра задей ствуют собственные локальные буферы, содержимое последних необходимо обменять путем копирования. Впрочем, это не так плохо, как кажется на первый взгляд, так как функция memcpy() оптимизирована под современные процессоры и, что даже более существенно, размер внутреннего буфера невелик (или должен быть таковым!) по определению (см., например, раздел 16.4).
16.2.6. Производительность Поскольку один из основных побудительных мотивов для появления auto_buffer – эффективность, было бы упущением – или, по крайней мере, неха
рактерной для меня скромностью – не упомянуть о том, насколько он может быть эффективным. Тесты показали, что в тех случаях, когда запрос на выделение памяти может быть удовлетворен из внутреннего буфера, время работы auto_buffer в среднем составляет 3% (а для некоторых компиляторов достигает и 1%) от времени работы функций malloc()/free(). Если память выделяется па раметризованным распределителем, то время работы auto_buffer составляет в среднем 104% (а для некоторых компиляторов 101%) от времени работы malloc()/free(). Следовательно, если размер буфера выбран удачно, то можно добиться заметного увеличения производительности.
Важнейшие компоненты
149
16.3. Класс filesystem_traits В нескольких подпроектах STLSoft определены характеристические клас сы, помогающие абстрагировать различия между операционными системами и их API, а также между вариантами API для различных схем кодирования симво лов. В UNIXSTL и WinSTL определен шаблон характеристического класса filesystem_traits, который среди прочего абстрагирует работу со строками, манипулирование именами в файловой системе, проверку состояния файловой системы, управляющие операции и т.д.
16.3.1. Типы"члены При определении характеристических классов самый важный шаг – выбрать типычлены. В шаблон filesystem_traits включены типы, показанные в лис тинге 16.2. Листинг 16.2. ТипыZчлены для шаблона filesystem_traits //  ïðîñòðàíñòâå èìåí unixstl / winstl template struct filesystem_traits { public: // Òèïû-÷ëåíû typedef C char_type; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef ???? stat_data_type; typedef ???? fstat_data_type; typedef ???? file_handle_type; typedef ???? module_type; typedef ???? error_type; typedef filesystem_traits class_type; . . .
Типы, обозначенные ????, зависят от операционной системы (и кодировки символов). Они приведены в таблице 16.1. Таблица 16.1. Типычлены, зависящие от операционной системы и кодировки символов ТипZчлен stat_data_type fstat_data_type file_handle_type module_type error_type
UNIX
Windows ANSI/Многобайтовая Unicode struct stat WIN32_FIND_DATAA WIN32_FIND_DATAW struct stat BY_HANDLE_FILE_INFORMATION int HANDLE void* HINSTANCE int DWORD
Здесь параметризующий символьный тип (C) представлен типомчленом char_type, а не value_type, поскольку никакого осмысленного типа значения для класса filesystem_traits не существует.
150
Основы
16.3.2. Работа со строками Первый набор составляют функции общего назначения для работы со строка ми, которые просто обертывают функции из стандартной библиотеки C, как пока зано в таблице 16.2. Не буду приводить их код, так как сигнатуры точно соответ ствуют тому, что каждая функция обертывает. Таблица 16.2. Стандартные и зависящие от операционной системы строковые функции, используемые в характеристическом классе Метод filesystem_traits
str_copy str_n_copy str_cat str_n_cat str_compare str_compare_no_case (только WinSTL) str_len str_chr str_rchr str_str
Эквивалентная функция из стандартной библиотеки C Специализация Специализация для char для wchar_t strcpy wcscpy strncpy wñsncpy strcat wcscat strncat wcsncat strcmp wcscmp lstrcmpiA lstrcmpiW strlen wcslen strchr wcschr strrchr wcsrchr strstr wcsstr
В классе winstl::filesystem_traits нет функции str_compare_no_case(), поскольку имена файлов в Windows не чувствительны к регистру. Возможно, вас удивили эти уродливые длинные имена. Дело в том, что стан дарт C оговаривает (C99: 7.26.10, 7.26.11), что в заголовки <stdlib.h> и <string.h> в будущем могут быть добавлены любые имена функций, начинаю щиеся с str, mem или wcs и записанные строчными буквами; иными словами, они зарезервированы. Кроме того, в стандарте сказано (C99: 7.1.3), что все имена мак росов и идентификаторов, упоминаемые в любой части стандарта, тоже зарезер вированы. Совет. Когда пишете библиотеку, ознакомьтесь с ограничениями, которые стандарт на) лагает на допустимые имена символов, и избегайте употреблять в своих библиотеках имена, совпадающие с теми, что уже есть в стандартных библиотеках.
Не всегда возможно вообще обойтись без употребления символов, присут ствующих в стандарте, но если стандарт четко оговаривает, что какихто имен сле дует избегать, было бы глупо это игнорировать.
16.3.3. Работа с именами из файловой системы В разных операционных системах приняты различные соглашения о файло вой системе и различные API для манипуляции ей. Абстрагироваться от этих раз
Важнейшие компоненты
151
личий на 100 процентов не всегда получается, даже приблизиться к этому показа телю – сложная задача. Тем не менее, место для полезных абстракций остается. В следующем наборе методов, которые широко используются в подпроектах UNIXSTL и WinSTL, делается попытка скрыть большинство различий. public: // Èìåíà â ôàéëîâîé ñèñòåìå static char_type path_separator(); static char_type path_name_separator(); static size_type path_max(); static char_type const* pattern_all();
Эти четыре метода возвращают зависящие от операционной системе значе ния, необходимые для манипулирования именами путей к файлам. path_separator() возвращает символ, которым пути отделяются друг от друга: ':' в UNIX и ';' в Windows. path_name_separator() возвращает символ, кото рым разделяются компоненты пути, то есть имена файлов, каталогов и томов: '/' в UNIX и '\\' в Windows. path_max() возвращает максимальную длину пути в данной системе. pattern_all() возвращает комбинацию метасимволов с се мантикой «все»: в UNIX '*' распознается и оболочкой и API glob (глава 17); в Windows, "*.*" распознается API FindFirstFile/FindNextFile (глава 20). Одна из неприятностей, с которыми приходится иметь дело при манипулиро вании путями, заданными в виде Cстрок, – это проверка наличия или отсутствия завершающего разделителя. Чтобы сформировать правильное имя, иногда при ходится добавлять или удалять разделитель. Для решения этой проблемы пред назначены три функции. has_dir_end() проверяет, заканчивается ли путь раз делителем; ensure_dir_end() добавляет разделитель, если он отсутствует, а remove_dir_end() удаляет разделитель, если он присутствует. Каждая из этих функций принимает завершающуюся нулем строку. ensure_dir_end() добавля ет не более одного символа, а вызывающая программа должна убедиться, что в буфере для него есть место. static bool has_dir_end(char_type const* dir); static char_type* ensure_dir_end(char_type* dir); static char_type* remove_dir_end(char_type* dir);
Следующие функции проверяют различные свойства путевых имен. static static static static static
bool bool bool bool bool
is_dots(char_type const* dir); is_path_name_separator(char_type ch); is_path_rooted(char_type const* path); is_path_absolute(char_type const* path); is_path_UNC(char_type const* path);
Функция is_dots() проверяет, совпадает ли переданная строка с одним из имен "." или "..". (Как она используется, мы увидим в главах 17 и 19.) Функция is_path_name_separator() проверяет, является ли заданный символ разделите лем путевых имен. Возникает вопрос, зачем она нужна, если уже есть функция path_name_separator(). Дело в том, что символ / во многих случаях приемлем и в Windows. Поэтому is_path_name_separator() в какойто мере защищает абст ракцию файловой системы от протекания.
152
Основы
В UNIX все имена в файловой системе начинаются от одного корня /. В Windows же есть три вида неотносительных путей. Связано это с наличием раз личных дисков и поддержкой сетевых соединений, для которых применяется нотация универсального соглашения об именовании (UNC). Полный путь, включающий указание диска, начинается с буквы диска, за которой следует дво еточие, разделитель компонентов и оставшаяся часть пути, например: H:\Publishing\Books\XSTL (или H:/Publishing\Books/XSTL). Путь от корня начинается просто с разделителя компонентов без указания диска, например: \Publishing\Books\ImpC++. UNCпуть начинается с двух символов \, за кото рыми следует имя сервера, символ \, имя общей папки и оставшаяся часть пути, например: \\JeevesServer\PublicShare1/ Directory0\Directory01. Отме тим, что первые три символа косой черты в UNCпути могут быть только обрат ными (\). Путаницы добавляет и тот факт что квалифицировать буквой диска можно и относительные пути; например, путь H:abc\def отсчитывается относи тельно текущего рабочего каталога на диске H. Различные виды путей от корня обслуживаются тремя оставшимися метода ми семейства is_path_??(). Метод is_path_rooted() возвращает true, если данный путь является любой из трех возможных разновидностей путей от корня. Метод is_path_absolute() возвращает true, только если данный путь содержит букву диска или является UNCпутем. is_path_UNC() возвращает true, только если данный путь является UNCпутем. Оказалось, что, имея эти три функции, можно писать эквивалентный код для UNIX и Windows на весьма высоком уровне абстракции (см. раздел 10.3). (Естественно, в UNIX is_path_absolute() просто вызывает is_path_rooted(), а is_path_UNC() всегда возвращает false.) Оставшиеся три метода, описываемые в этом разделе, служат для преобразо вания относительных путей в абсолютные: static size_type get_full_path_name( char_type const* fileName , size_type cchBuffer, char_type* buffer , char_type** ppFile); static size_type get_full_path_name( char_type const* fileName , size_type cchBuffer, char_type* buffer); static size_type get_short_path_name(char_type const* fileName , size_type cchBuffer, char_type* buffer);
Читатели, знакомые с путевыми именами на платформе Windows, сразу разбе рутся в назначении этих функций. Первый из перегруженных вариантов get_full_path_name() принимает имя файла, записывает его абсолютную фор му в буфер, определяемый параметрами buffer и cchBuffer, и возвращает указа тель на последний компонент имени в параметре *ppFile (при условии, что дли на буфера достаточна для возврата полного имени, в противном случае в *ppFile записывается NULL). Второй вариант –то же самое, но без параметра ppFile. Ме тод get_short_path_name() возвращает в Windows эквивалентное короткое имя, например, H:\AAAAAA~2 для H:\aaaaaaaaaaaaaaaaaaaaaaa. В UNIX он про сто вызывает get_full_path_name(). Отметим, что эти функции не гарантируют приведения к каноническому пути, то есть могут не удалять "/./" и не подставлять путь вместо "/../". Они
Важнейшие компоненты
153
также не требуют, чтобы путь действительно существовал, поэтому версия в UNIXSTL не реализована в терминах функции realpath().
16.3.4. Операции с состоянием объектов файловой системы Как ни любопытно заниматься именами файлов, сами файлы представляют гораздо больший интерес. Следующая группа методов предоставляет средства для опроса отдельных объектов файловой системы: public: // Ñîñòîÿíèå îáúåêòà ôàéëîâîé ñèñòåìû static bool file_exists(char_type const* path); static bool is_file(char_type const* path); static bool is_directory(char_type const* path); static bool stat(char_type const* path , stat_data_type* stat_data); static bool fstat(file_handle_type fd , fstat_data_type* fstat_data);
Все они получают информацию о конкретном объекте файловой системы. file_exists() возвращает true, если path именует существующий объект. is_file() и is_directory() возвращает true, если путь path существует и отно сится к объекту соответствующего типа. (is_file() и is_directory() в UNIX обра щаются к системному вызову stat(), а не lstat(). Чтобы проверить, является ли объект ссылкой, пользуйтесь функцией unixstl::filesystem_traits::lstat().) Метод stat() возвращает информацию об объекте с путем path, заполняя поля объекта типа stat_data_type (см. раздел 16.3.1), если ее удалось получить. fstat() возвращает информацию об открытом файле в структуре fstat_data_ type. Методы file_exists(), is_file() и is_directory() реализованы с по мощью stat().
Эти функции покрывают большинство запросов о состоянии, но не всегда достаточны. Вопервых, довольно часто приходится для одного и того же файла вызывать более одной функции из группы file_exists(), is_file() и is_directory(), то есть выполнять несколько системных вызовов. Эффективнее было бы один раз обратиться к функции stat(), но структуры struct stat и WIN32_FIND_DATA сильно различаются как по составу полей, так и по интерпрета ции флагов, описывающих состояние файла. Поэтому в характеристических классах определены еще четыре (для UNIX, где структуры stat_data_type и fstat_data_type идентичны) или восемь (для Windows, где это разные типы) методов, принимающих указатель на структуру с информацией и возвращающих булевское значение. Это переносимый способ опросить сразу несколько свойств объекта файловой системы, выполнив лишь один системный вызов. static bool_type is_file(stat_data_type const* stat_data); static bool_type is_directory(stat_data_type const* stat_data); static bool_type is_link(stat_data_type const* stat_data);
Основы
154 static bool_type is_readonly(stat_data_type const* stat_data); static static static static
bool_type bool_type bool_type bool_type
is_file(fstat_data_type const* stat_data); is_directory(fstat_data_type const* stat_data); is_link(fstat_data_type const* stat_data); is_readonly(fstat_data_type const* stat_data);
16.3.5. Операции управления файловой системой Имеется еще одна группа операций, которые воздействуют на файловую сис тему и модифицируют ее состояние или связь с данным процессом. Шесть из них не требуют пояснений, и я лишь отмечу, что все, кроме одной, возвращают булев ский признак успеха, как и большинство методов характеристического класса. public: // Óïðàâëåíèå ôàéëîâîé ñèñòåìîé static size_type get_current_directory(size_type cchBuffer , char_type* buffer); static bool set_current_directory(char_type const* dir); static bool create_directory(char_type const* dir); static bool remove_directory(char_type const* dir); static bool unlink_file(char_type const* file); static bool rename_file(char_type const* currentName , char_type const* newName);
16.3.6. Типы возвращаемых значений и обработка ошибок Возможно, вы обратили внимание на то, что многие характеристические мето ды возвращают булевские значения. Поскольку аналогичные функции в разных операционных системах поразному сообщают об ошибках, этот способ абстраги рования успеха и ошибки оказывается наиболее переносимым. Если пользовате лю нужна подробная информация об ошибке, он может воспользоваться функ циями get_last_error() и set_last_error(): static error_type get_last_error(); static void set_last_error(error_type er = error_type());
(Отметим, что эти функции принимают во внимание потоки, если они поддер живаются абстрагируемой операционной системой; на практике это относится к любой операционной системе, для которой имеется характеристический класс.)
16.4. Класс file_path_buffer В некоторых операционных системах максимальная длина пути к файлу огра ничена, в других таких ограничений нет. Чтобы абстрагировать это различие и пользоваться всюду, где возможно, эффективными (небольшими) буферами фик сированного размера, в библиотеках STLSoft реализованы классы для работы с буфером файлового пути. Требования к ним идеально сочетаются с классом auto_buffer (раздел 16.2), который инициализируется размером, подходящим
Важнейшие компоненты
155
для хранения любого допустимого в данной операционной системе пути. И в UNIXSTL, и в WinSTL определен шаблонный класс basic_file_path_buffer, показанный в листинге 16.3. Листинг 16.3. Объявление класса basic_file_path_buffer //  ïðîñòðàíñòâå èìåí unixstl / winstl template< typename C // Òèï ñèìâîëà , typename A = typename allocator_selector::allocator_type > class basic_file_path_buffer { public: // Òèïû è êîíñòàíòû-÷ëåíû . . . // Typedef äëÿ value_type, allocator_type, class_type è ò.ä. enum { internalBufferSize = . . . }; public: // Êîíñòðóèðîâàíèå basic_file_path_buffer() : m_buffer(1 + calc_path_max_()) {} basic_file_path_buffer(class_type const& rhs) : m_buffer(rhs.size()) { std::copy(rhs.m_buffer.begin(), rhs.m_buffer.end() , &m_buffer[0]); } class_type& operator =(class_type const& rhs) { std::copy(rhs.m_buffer.begin(), rhs.m_buffer.end() , &m_buffer[0]); return *this; } public: // Îïåðàöèè void swap(class_type& rhs) throw(); public: // Ìåòîäû äîñòóïà value_type const* c_str() const; reference operator [](size_t index); const_reference operator [](size_t index) const; size_type size() const; private: // Ðåàëèçàöèÿ static size_t calc_path_max_(); private: // Ïåðåìåííûå-÷ëåíû stlsoft::auto_buffer m_buffer; };
В конструкторе по умолчанию класса auto_buffer член m_buffer инициа лизируется указателем на буфер, размер которого равен максимальной длине пути на данной платформе плюс 1 для завершающего нуля. Поскольку auto_buffer возбуждает исключение std::bad_alloc, если запрошенная в кон структоре область памяти длиннее внутреннего буфера и не может быть выделена распределителем, то гарантируется, что в сконструированном экземпляре буфер достаточно велик для хранения любого допустимого на данной платформе пути.
156
Основы
Отметим, что конструктор по умолчанию не инициализирует содержимое m_buffer и даже не записывает в начальную позицию '\0' – буфер для файлово го пути следует рассматривать как неформатированный массив символов данного типа, имеющий подходящий размер. (При компиляции отладочной версии буфер заполняется символами '?' путем обращения к функции memset(), чтобы отло вить любые ложные допущения.) Конструктор копирования и копирующий оператор присваивания вручную копируют содержимое, так как auto_buffer намеренно не поддерживает семан тику копирования (раздел 16.2.3). Метод swap() реализован путем прямого обра щения к auto_buffer::swap(), а все три метода доступа – посредством auto_buffer::data(). Единственные два неизвестных в этой картине – это принимаемый по умолчанию размер внутреннего буфера для m_buffer (обозначенный internalBufferSize) и поведение метода calc_path_max_(). То и другое зави сит от операционной системы и будет рассмотрено в следующих подразделах.
16.4.1. Класс basic_?? Конечно, вы обратили внимание, что шаблон на самом деле называется basic_file_path_buffer. Здесь я следую принятому в стандартной библио
теке соглашению давать шаблонам, основной параметр которых – тип симво ла, имена, начинающиеся с basic_ : basic_path, basic_findfile_sequence, basic_string_view и т.д. Совет. Используйте префикс basic_ для шаблонов классов, основной параметр кото) рых – тип символа.
Отметим, что во всех упоминаниях таких типов слово basic_ обычно опуска ется –говоря findfile_sequence, я имею в виду basic_findfile_sequence<>, – а находятся они в файлах, имена которых следуют тому же соглашению, напри мер, . Некоторое время я тянул с использованием суффиксов _a и _w для предопре деленных специализаций таких типов, например: drop_handle_sequence_a (basic_drophandle_sequence), path_w (basic_path<wchar_t>) и т.д. Но теперь стараюсь следовать стандарту, употребляя имя без basic_ для специа лизации типом char, например unixstl::path (unixstl::basic_path), и имя с префиксом w для специализации типом wchar_t, например inetstl::wfindfile_sequence (inetstl::basic_findfile_sequence<wchar_t>). Исключение из этого правила составляет проект WinSTL, в котором, как и в заго ловочных файлах Windows, используется простое имя для специализации типом TCHAR и суффиксы a и w для кодировок ANSI/многобайтовая и Unicode соответ ственно. Хотя, на первый взгляд, это непоследовательно, зато интуитивно очевид но при программировании в Windows, и проблем с такой схемой именования у меня никогда не возникало.
Важнейшие компоненты
157
16.4.2. UNIX и PATH_MAX В некоторых UNIXсистемах, где максимальная длина пути фиксирована, оп ределен символ препроцессора PATH_MAX, равный максимальному числу байтов в пути без учета завершающего нуля. В других вариантах UNIX лимит на этапе компиляции не определен, но возвращается функцией pathconf(), которая ис пользуется и для получения других лимитов, относящихся к файловой системе. long pathconf(char const* path, int name);
Чтобы узнать максимальную длину пути, следует во втором параметре (name) указать константу _PC_PATH_MAX. В результате будет возвращена максимальная длина относительно заданного пути path. Если получить этот или какойто дру гой лимит не удается, функция возвращает -1. Следовательно, чтобы узнать дли ну максимального пути в системе, нужно указать корневой каталог "/" и добавить к результату 1 (если он неотрицателен). Исходя из этих соображений, мы опреде ляем размер буфера по умолчанию и метод calc_path_max_(), как показано в листинге 16.4. Листинг 16.4. Вычисление размеров для шаблонного класса basic_file_path_buffer в UNIXSTL . . . enum { #ifdef PATH_MAX internalBufferSize = 1 + PATH_MAX #else /* ? PATH_MAX */ internalBufferSize = 1 + 512 #endif /* PATH_MAX */ }; enum { indeterminateMaxPathGuess = 2048 }; . . . static size_t calc_path_max_() { #ifdef PATH_MAX return PATH_MAX; #else /* ? PATH_MAX */ int pathMax = ::pathconf("/", _PC_PATH_MAX); if(pathMax < 0) { pathMax = indeterminateMaxPathGuess; } return static_cast<size_t>(pathMax); #endif /* PATH_MAX */ }
Константачлен indeterminateMaxPathGuess – это значение, которое мы вы бираем произвольно в случае, когда pathconf() не может вернуть максимальную
158
Основы
длину пути относительно корня. Таким образом, может случиться, что в UNIXSTL размер буфера окажется недостаточен для хранения любого допустимого пути. Поэтому при работе с буферами для файловых путей принято включать специфи кацию размера (size()). Кроме того, класс в UNIXSTL предлагает еще метод grow(), которого нет в аналоге для WinSTL. Этот метод пытается при каждом вызове удвоить размер выделенной памяти.
16.4.3. Windows и MAX_PATH В заголовочных файлах Windows константа MAX_PATH определена как 260, и большинство функций Windows API, предназначенных для работы с объектами файловой системы, предписывают выделять буфер именно такого размера. Сис темы семейства Windows 9x не поддерживает более длинных путей. Что же каса ется семейства Windows NT, то там поддерживаются пути длиной до 32767 бай тов. Однако для работы с такими длинными путями необходимо использовать «широкие» версии функций – CreateFileW(), CreateDirectoryW() и т.д. – и на чинать имя пути с префикса "\\?\". При работе с ANSIверсиями функций API, к примеру CreateFileA() CreateDirectoryA(), вы попрежнему ограничены 260 байтами. Следовательно, емкость буфера должна быть равна 32767 + 4 только при ком пиляции для «широких» строк в системах семейства NT. Режим компиляции лег ко определить, проверив размер типа символа (sizeof(C)), а семейство ОС – оп росив старший бит значения, возвращенного функцией GetVersion() (см. листинг 16.5). Листинг 16.5. Вычисление размеров для шаблонного класса basic_file_path_buffer в WinSTL . . . enum { internalBufferSize = 1 + PATH_MAX }; . . . static size_t calc_path_max_() { if( sizeof(C) == sizeof(CHAR) || // ñïåöèàëèçàöèÿ äëÿ ANSI (::GetVersion() & 0x80000000)) // Windows 9x { // Windows 9x èëè NT ñ êîäèðîâêîé ANSI return _MAX_PATH; } else { return 4 + 32767; } }
Важнейшие компоненты
159
16.4.4. Использование буферов Пользоваться буферами просто. Если это локальная переменная или перемен наячлен, то успешно сконструированный буфер следует рассматривать как обычный массив символов: unixstl::file_path_buffer buff; ::getcwd(&buff[0]. buff.size());
и winstl::basic_file_path_buffer<WCHAR> buff; ::GetCurrentDirectoryW(buff.size(), &buff[0]);
Мы будем постоянно встречаться с такими буферами, поскольку они дают удобную абстракцию для нетривиальных вычислений длины пути, работающую в разных операционных системах. А в большинстве случаев они обеспечивают еще и оптимизацию по скорости a la auto_buffer.
16.5. Класс scoped_handle И напоследок я хочу рассказать о шаблонном классе интеллектуального указате ля scoped_handle, который применяется для гарантированной очистки ресурса в данной области видимости путем обращения к функции, указанной вызывающей программой. Этот класс можно использовать для управления временем жизни ресур сов (FILE*), открытых с помощью унаследованного от C API файловых потоков: { FILE* file = ::fopen("file.ext", "r"); stlsoft::scoped_handleh2(file, ::fclose); throw std::runtime_error("Íàì ãðîçèò óòå÷êà?"); } // â õîäå ðàñêðóòêè ñòåêà ïðè îáðàáîòêå èñêëþ÷åíèÿ âûçûâàåòñÿ fclose(file)
или ресурсов (void*), выделенных с помощью API работы с памятью (тоже из библиотеки C): { stlsoft::scoped_handle h3(::malloc(100), ::free); ::memset(h3.get(), 0, 100); } // çäåñü âûçûâàåòñÿ free()
Этот класс может работать с ресурами, чье «нулевое» состояния отлично от 0 (или NULL), как показано в следующем фрагменте: int fh = ::open("filename.ext", O_WRONLY | O_CREAT , S_IREAD | S_IWRITE); if(-1 != fh) { stlsoft::scoped_handle h1(fh, ::close, -1); . . . // Èñïîëüçóåì fh } // çäåñü âûçûâàåòñÿ close(fh)
160
Основы
Работает он и с функциями, следующими различным принятым в Windows соглашениям о вызове: cdecl, fastcall и stdcall: { void* vw = ::MapViewOfFile(. . .); stlsoft::scoped_handle h4(vw, ::UnmapViewOfFile); } // çäåñü âûçûâàåòñÿ ôóíêöèÿ UnmapViewOfFile(vw) ñ ñîãëàøåíèåì î âûçîâå stdcall
Функция UnmapViewOfFile() следует соглашению stdcall. Для учета разли чий в соглашениях о вызове предусмотрено несколько перегруженных вариантов конструктора шаблона. У шаблона scoped_handle имеется специализация для типа описателя void. В этом случае он может вызывать функции без параметров, как в следующем коде, который выполняет инициализацию и гарантированную деинициализацию биб лиотеки WinSock: WSADATA wsadata; if(0 != ::WSAStartup(0x0202, &wsadata)) { stlsoft::scoped_handle h4(::WSACleanup); . . . // Çäåñü èñïîëüçóåòñÿ WinSock API } // Çäåñü âûçûâàåòñÿ WSACleanup().
Обсуждение реализации шаблона scoped_handle выходит за рамки данной книги. Но хочу отметить, что в ней не используются ни виртуальные функции, ни макросы, а также не выделяется память. И она исключительно эффективна, по скольку сводится в основном к приведению указателей на функции к вырожден ной форме и их сохранению вместе с описателем ресурса в виде переменныхчле нов для последующего вызова в деструкторе объекта. Используемое приведение не подчиняется правилам языка C++, но только при работе с платформенно%за% висимыми соглашениями о вызове, которые сами по себе являются нарушением правил. При работе с функциями, для которых соглашение о вызове явно не ука зано, реализация ни в чем не отступает от правил языка. Отметим, что использование любой формы идиомы RAII – будь то обоб щенный компонент типа scoped_handle или конкретный класс (скажем, AcmeFileScope) – для принудительного закрытия файла имеет нетривиальные последствия, обсуждение которых выходит за рамки данной книги. То же можно сказать и о других ресурсах, функция очистки которых может завершаться с ошибкой. Класс scoped_handle лишь гарантирует, что эта функция будет выз вана, но никак не помогает при обработке возможных ее ошибок.
16.6. Функция dl_call() В подпроектах UNIXSTL и WinSTL имеется группа перегруженных функций dl_call(), применяемых для вызова функций из динамически загружаемых биб
лиотек с использованием естественного синтаксиса. В обеих реализациях приме
Важнейшие компоненты
161
няются прокладки строкового доступа (раздел 9.3.1) для обеспечения совмести мости с разными типами, а в версии для Windows также учитываются все три рас пространенных соглашения о вызове: cdecl, fastcall и stdcall. Пусть, например, мы хотим динамически вызвать функцию GetFileSizeEx() из Windows API, которая следует соглашению stdcall и находится в динамической библиотеке KERNEL32.DLL. Она имеет такую сигнатуру: BOOL __stdcall GetFileSizeEx(HANDLE hFile, LARGE_INTEGER* pSize);
Чтобы вызвать ее динамически, можно написать такой код: LARGE_INTEGER size; HANDLE h = ::CreateFile(. . .); if(!winstl::dl_call("KERNEL32", "S:GetFileSizeEx", h, &size)) { . . .
Первый аргумент функции dl_call() определяет динамическую библиоте ку, из которой загружается функция. Он должен быть либо строкой (типа char const* или любого другого, для которого определена прокладка строкового дос тупа c_str_ptr), либо описателем уже загруженной библиотеки (void* в UNIX или HINSTANCE в Windows). Второй аргумент – идентификатор функции в данной библиотеке. Это должна быть строка типа char const* или любого другого, для которого определена прокладка строкового доступа c_str_ptr), либо дескриптор функции (см. ниже). Если идентификатор функции – строка, то ей может предшествовать специ фикатор соглашения о вызове, отделяемый двоеточием. Допустимы следующие спецификаторы: "C" (или "cdecl") для cdecl, "F" (или "fastcall") для fastcall и "S" (или "stdcall") для stdcall. Если спецификатор не задан, по умолчанию предполагается cdecl. (Это соглашение по умолчанию принимается во всех ком пиляторах C/C++, если в командной строке явно не указано противное.) Следовательно, можно было бы вызвать dl_call() и так: winstl::dl_call("KERNEL32", "stdcall:GetFileSizeEx", h, &size)
но не так: winstl::dl_call("KERNEL32", "GetFileSizeEx", h, &size)
поскольку в этом случае неминуем крах изза неправильной интерпретации стека, ибо имеет место расхождение между истинным соглашением о вызове данной функции (stdcall) и указанным (cdecl). Все последующие аргументы передаются самой динамической функции, как если бы она вызывалась естественным образом. Магия шаблонов внутри dl_call() все делает за вас. В обеих версиях – UNIXSTL и WinSTL – поддер живается от 0 до 32 аргументов, этого должно хватить в абсолютном большинстве случаев. Если всетаки окажется мало, то имеется написанный на Ruby сценарий, который поможет подогнать реализацию под ваши требования. (Впрочем, если вы пишете или используете динамическую библиотеку, в которой есть функции,
162
Основы
принимающие более 32 аргументов, то самое время обратиться к людям в белых халатах.) Так как мы знаем, что функция GetFileSizeEx() следует соглашению stdcall, то можем немного сэкономить на разборе соглашения о вызове (и избежать потен циальной ошибки в написании идентификатора), воспользовавшись дескрипто ром функции. Для удобства предусмотрена порождающая шаблонная функция fn_desc(), которую можно применять в одной из двух форм: на этапе компиля ции: winstl::dl_call("KERNEL32" , winstl::fn_desc<STLSOFT_STDCALL_VALUE>("GetFileSizeEx") , h, &size)
или на этапе выполнения: winstl::dl_call("KERNEL32" , winstl::fn_desc(STLSOFT_STDCALL_VALUE, "GetFileSizeEx") , h, &size)
Первая чуть эффективнее и более удобна, когда вы заранее знаете соглашение о вызове, как оно обычно и бывает. Последняя предназначена для тех редких слу чаев, когда разные библиотечные функции написаны в предположении различ ных соглашений о вызове, например, если речь идет о старой и новой версии под ключаемого модуля для некоторого приложения. Использовать функцию dl_call() для уже загруженной библиотеки столь же просто: HINSTANCE hinst = ::LoadLibrary("PSAPI.DLL"); winstl::dl_call(hinst, "S:GetFileSizeEx", h, &size);
И, конечно, любая уважающая себя библиотека, под которой я готов поста вить свое имя, обязана быть эффективной. Реализация этих функций довольно сложна, так как им приходится проделать много работы, чтобы разобраться с раз личными видами библиотек и дескрипторов функций. Но большая часть кода встроена, а оставшийся – ничто по сравнению с затратами на загрузку и коррек цию адресов, не говоря уже о времени работы самих вызываемых функций. И на этом мы завершаем краткий обзор важнейших компонентов. Это после дняя глава, в которой не было сокровенной информации об STL вперемежку с моими жалкими потугами на остроумие.
Часть II. Наборы Значительная, если не основная часть усилий при расширении STL тратится на адаптацию API различных наборов к понятию STL%набора (раздел 2.2). Поэтому и данная часть книги, которая целиком посвящена этому вопросу, получилась са мой объемной. Одна из глав в ней (глава 24) посвящена адаптации настоящего контейнера, а в остальных описываются адаптации API операционной системы и сторонних библиотек. При расширении STL приходится учитывать многое: тип набора, категории итераторов, категории ссылок на элементы, сцепленность элементов, получив шихся в результате адаптации, в сравнении с исходным представлением, опреде ление категории итератора во время выполнения (глава 28), специализацию стан дартных алгоритмов (глава 31), недействительность внешних итераторов (главы 26 и 33) и поведение итераторов, не укладывающихся в рамки привычных катего рий (главы 26 и 28). Наборы встречаются в таких разнородных сферах, как файловые системы (главы 1721), бесконечные математические последовательности (глава 23), раз биение строки на лексемы (глава 27), энумераторы и наборы COM (главы 2830), сетевые коммуникации и ввод/вывод (глава 31), системные процессы, перемен ные окружения и конфигурация (главы 22 и 25, раздел 33.3), элементы управле ния в графических интерфейсах (глава 33.2) и управление Zпорядком (глава 26). Я старался выбирать темы так, чтобы представить возможно более широкий спектр проблем, возникающих при расширении STL, не слишком усложняя мате риал и не отклоняясь далеко от основной темы. Если не считать главы 23, посвя щенной числам Фибоначчи, все расширения взяты из практики и широко приме няются в открытых и коммерческих проектах. Эта часть состоит из семнадцати глав и интелюдий. (Еще две интелюдии име ются на компактдиске.) В главе 17 описывается адаптация группового API (раздел 2.2) к неизменяе мому STLнабору (раздел 2.2.1) glob_sequence с непрерывными итераторами (раздел 2.3.6). Демонстрируется, какой выразительности, надежности и произво дительности можно добиться с помощью расширения STL. В последующей ин терлюдии, главе 18, обсуждаются ошибки, допущенные при первоначальном про ектировании класса glob_sequence, и механизмы повышения гибкости его шаблонных конструкторов, которые, вообще говоря, можно применить к широко му диапазону компонентов. В главе 19 описывается адаптация поэлементного API (раздел 2.2) к неизменяемому STLнабору readdir_sequence с итераторами ввода (раздел 1.3.1). Показано, что адаптация такой слабой категории итераторов
164
Наборы
оказывается на деле сложнее, так как требуется реализовать общее состояние. В главе также описывается адаптация поэлементного API к неизменяемому STLна бору, но на этот раз в виде шаблонного класса набора basic_findfile_sequence, который допускает различные кодировки символов на платформе Windows. Тема адаптации файловой системы завершается интерлюдией в главе 21. Здесь описы вается API перебора для протокола FTP, который синтаксически схож с API пере бора файловой системы из главы 20, но с семантикой, требующей совершенно иного подхода к адаптации. Первое знакомство с адаптацией API операционной системы состоится в гла ве 22. Сама задача довольно проста – предоставить неизменяемые STLнаборы с непрерывными итераторами, но, чтобы добиться единообразной реализации с учетом различий в операционных системах и компиляторах, требуется проявить изобретательность. Единственная вычисляемая последовательность, которую мы рассмотрим (в главе 23), – это последовательность чисел Фибоначчи. На первый взгляд, это очень простой компонент, но практические ограничения на диапазон представи мых целых чисел (и чисел с плавающей точкой) приводят к ряду интересных во просов. Обсуждаются различные возможные реализации, их плюсы и минусы. Окончательное решение опирается на использование простой, но мощной техни ки работы с шаблонами, которая помогает компилятору различить логически раз ные типы. Глава 24 относится к другому концу спектра адаптации расширений STL. Речь идет о прозаической материи: адаптации нестандартного контейнера для эмуляции синтаксиса и семантики стандартного, в данном случае std::vector. Показывается, что изза существенных различий в схеме выделения памяти, представлении элементов и обработке ошибок адаптация оказывается нетриви альным делом, приходится идти на компромиссы и накладывать ограничения на семантику конечного результата. И все же, как демонстрируется в этой главе, при наличии толики изобретательности и неколебимой решимости из нестандартного контейнера можно таки получить полезный и почти совместимый с STL контей нер. На компактдиске имеется относящаяся к этой теме интерлюдия «Опасай тесь непрерывных итераторов, не являющихся указателями», где описываются некоторые особенности компиляторов, изза которых адаптация может стать еще более сложной проблемой, чем представляется в главе 24. Глава 25 посвящена адаптации еще одного системного API. В ней затрагивают ся два важных вопроса. Один простой – как адекватно абстрагировать различия в API доступа к переменным окружения. Это достигается за счет использования характеристических классов (раздел 12.1). Куда более сложная проблема – как на дежно реализовать совместный доступ к диапазону элементов, хранящихся в гло бальной на уровне процесса переменной. Решение основано на использовании ите раторов с подсчетом ссылок и разделяемых снимков набора. На компактдиске имеется дополнительная интерлюдия «Укрощение строптивой ADL», в которой описывается, как заставить некорректно написанные компиляторы правильно ис кать не являющиеся членами операторные функции нешаблонных классов.
Наборы
165
В главе 26 речь пойдет о сложностях адаптации набора, порядок и состав эле ментов в котором могут асинхронно изменяться. Основная проблема здесь – это несоответствие итераторов такого набора любой из известных категорий, а реше ние ее на удивление эгоцентрично. Нарушение гипотезы Хенни (глава 14), непреднамеренная эволюция хороше го программного обеспечения и противоречивая природа компонента с хорошим интерфейсом класса и плохим шаблонным интерфейсом – вот темы главы 27. По путно мы увидим, как невелики могут быть накладные расходы при адаптации STLнаборов. Модель компонентных объектов (COM) – это языковонезависимый двоич ный стандарт программных компонентов, в котором определены собственные мо дели наборов и их перебора, сильно отличающиеся от принятых в STL. В главе 28 – самой длинной в этой книге – рассматривается адаптация интерфейсов энумера торов COM IEnumXXXX к STLнабору enumerator_sequence, имеющему итера торы ввода или однонаправленные итераторы. Здесь возникают следующие сложности: обеспечение безопасной работы с интерфейсами на базе счетчиков ссылок, управление COMресурсами, обеспечение безопасности относительно исключений, кэширование элементов, корректная обработка неконстантности в COM и противоречие между определением возможности клонирования энуме раторов COM на этапе выполнения и заданием клонируемости итераторов STL на этапе компиляции. Хотя реализация заведомо нетривиальна, конечный резуль тат адаптации безопасен относительно исключений, не допускает утечки ресур сов, лаконичен, гибок, понятен и по сравнению с прямолинейной реализацией на C/C++ весьма выразителен. В последующей интерлюдии – главе 29 – обсуждает ся как ошибку, допущенную в исходном варианте проекта enumerator_sequence, оказалось легко исправить с помощью механизма выведения типа, рассмотренно го в главе 13. В главе 30 обсуждается модель наборов COM и ее адаптация к кон цепции STLнабора в виде компонента collection_sequence. Здесь рассказано о том, как организовать работу с дополнительными возможностями адаптирован ных наборов на этапах компиляции и выполнения, а также иллюстрируется, как, немного зная о внутреннем устройстве класса enumerator_sequence, можно упростить реализацию набора, не жертвуя надежностью. Глава 31 посвящена тому, как бескомпромиссно сочетать абстракцию с эф фективностью при использовании высокопроизводительного API ввода/вывода. Помимо описания механизма линеаризации нескольких несмежных блоков памя ти, здесь иллюстрируется техника работы с низкоуровневыми функциями быст рой поблочной передачи данных в сочетании со стандартными алгоритмами по средством допустимой специализации элементов из стандартной библиотеки. В главе 32 ощущается влияние на C++ таких сценарных языков, как Python и Ruby. Показано, как можно воспользоваться шаблонами, чтобы представить на бор в виде линейного массива с целыми индексами или ассоциативного массива со строковыми индексами. Последняя в этой части глава 33 посвящена общей проблеме внешнего изме нения набора (адаптированного к STL) во время его обхода, неважно, вызвано ли
166
Наборы
оно побочными эффектами текущего потока, действиями, выполняемыми в дру гом потоке того же процесса, или даже совсем в другом процессе. Возникающие проблемы (и их решения) иллюстрируются на примерах из области графических интерфейсов пользователя (ГИП), организации системных реестров и XMLбиб лиотек.
Глава 17. Адаптация API glob Умение находить компромиссы и приспосаб% ливаться не перестает быть нужным и пос% ле того, как проектирование программы за% вершено. – Генри Петроски Лениться вроде бы легко, но как трудно это дается. – Автор неизвестен
17.1. Введение В этой главе мы рассмотрим API glob, предоставляемый в системе UNIX. Это первый из четырех API перебора, которые изучаются в части II. Он позволяет вы полнять поиск в файловой системе, пользуясь теми же мощными средствами со поставления с образцами, которые применяются в оболочках UNIX. Хотя функ ция glob() обладает весьма развитыми возможностями и довольно сложна по сравнению с другими API просмотра файловой системы, ее интерфейс сравни тельно прост для адаптации к STL, поэтому мы с нее и начнем.
17.1.1. Мотивация Представьте, что вам нужно написать инструмент для автоматического доку ментирования своей библиотеки. Требуется программно найти все содержащие алгоритмы файлы в различных подпроектах, а затем передать результаты двум разным операциям, причем во второй они должны обрабатываться в обратном по рядке. Предположим, что операции объявлены следующим образом: void Operation1(char const* entry); void Operation2(char const* entry);
C помощью функции glob() задачу можно было бы решить примерно так, как показано в листинге 17.1. Листинг 17.1. Обход файловой системы с помощью API glob 1 2 3
std::string libraryDir = getLibDir(); glob_t gl; int res = ::glob( (libraryDir + "/*/*algo*").c_str()
Наборы
168 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
, GLOB_MARK , NULL , &gl); if(GLOB_NOMATCH == res) { return 0; // Íåò ïîäõîäÿùèõ ýëåìåíòîâ } else if(0 != res) { throw some_exception_class("îøèáêà glob()", res); } else { // Ïåðâàÿ îïåðàöèÿ { for(size_t i = 0; i < gl.gl_pathc; ++i) { struct stat st; if( 0 == ::stat(gl.gl_pathv[i], &st) && S_IFREG == (st.st_mode & S_IFREG)) { Operation1(gl.gl_pathv[i]); } }} // Âòîðàÿ îïåðàöèÿ { for(size_t i = 0; i < gl.gl_pathc; ++i) { char const* item = gl.gl_pathv[gl.gl_pathc – (i + 1)]; size_t len = ::strlen(item); if('/' != item[len - 1]) // Íå êàòàëîã { Operation2(item); } }} size_t n = gl.gl_pathc; ::globfree(&gl); return n; }
В следующем разделе мы разберемся, как этот код работает, выявим все про блемы и посетуем, сколько строк пришлось написать для решения такой простой задачи. Но сначала взгляните на версию в духе STL, написанную с помощью клас са glob_sequence из подпроекта UNIXSTL: Листинг 7.2. Обход файловой системы с помощью класса glob_sequence 1 2 3 4 5 6 7
using unixstl::glob_sequence; glob_sequence gls(getLibDir(), "*/*algo*", glob_sequence::files); std::for_each(gls.begin(), gls.end(), std::ptr_fun(Operation1)); std::for_each(gls.rbegin(), gls.rend(), std::ptr_fun(Operation2)); return gls.size();
Адаптация API glob
169
Полагаю, вы согласитесь, что этот вариант куда лучше с точки зрения понят ности, выразительности и гибкости клиентского кода и что он служит убедитель ным доказательством того, как расширение STL может принести дивиденды. Ложкой дегтя в бочке меда могла бы стать производительность. Пришлось ли нам заплатить за эту абстракцию? Чтобы выяснить это, я написал тестовую програм му. (Функции Operation1() и Operation3() просто вызывали strlen() для пе реданной строки.) Результаты приведены в таблице 17.1. Оба клиента запускались несколько сотен раз, и время работы было усреднено по десяти наихудшим прогонам. Таблица 17.1. Производительность системного API и адаптированного к STL класса Операционная система
Системный API
Класс glob_sequence
Linux (700MHz, 512MB Ubuntu 6.06; GCC 4.0) Win32 (2GHz, 1GB, Windows XP; VC++ 7.1)
1,045 мс 15,337 мс
1,021 мс 2,517 мс
Как видите, мы не только ничего не проиграли в производительности, но даже выиграли примерно 2%. Неплохо однако. Ради интереса я запустил тестовую программу также в Windows, воспользо вавшись библиотекой для эмуляции UNIX (имеется на компактдиске). Обратите внимание, как дорого на этой платформе обходятся обращения к ядру. В следую щих главах мы уделим внимание этому факту.
17.1.2. API glob API glob состоит из одной структуры и двух функций, объявления которых приведены в листинге 17.3. Функция glob() ищет соответствия заданному образ цу pattern с учетом флагов flags. Если чтото было найдено, то результат в виде массива указателей на Cстроки помещается в предоставленную вызывающей про граммой структуру, на которую указывает аргумент pglob. Дополнительно вызы вающая программа может указать функцию, которая будет вызываться для каждо го найденного объекта. При этом ей передаются путь к объекту и код errno для тех объектов, посещение которые закончилось ошибкой. Функция globfree() осво бождает ресурсы, захваченные glob(). Листинг 17.3. Типы и функции, составляющие API glob struct glob_t { size_t gl_pathc; char** gl_pathv; size_t gl_offs; }; int glob( char const* pattern
Наборы
170 , int , int , glob_t*
flags (*errfunc)(char const* errPath, int eerrno) pglob);
void globfree(glob_t*
pglob);
Отметим, что структура glob_t поразному определяется в разных вариантах UNIX. Иногда вместо size_t указывается тип int, иногда имеются дополнитель ные поля. Я буду рассматривать только приведенную выше версию, которая опре делена в стандарте POSIX. Если glob() завершается успешно, она возвращает 0. Возможны следующие коды ошибки: GLOB_NOSPACE (нехватка памяти), GLOB_ABORTED (ошибка чтения) и GLOB_NOMATCH (соответствие не найдено). Можно задавать различные флаги, в частности, следующие, определенные в POSIX. GLOB_ERR: прекращать просмотр после первой же ошибки чтения (альтер натива – игнорировать ошибку и продолжить поиск); GLOB_MARK: добавлять косую черту в конец имени каждого найденного ка талога; GLOB_NOSORT: не сортировать пути. По умолчанию сортировка производит ся, что сопровождается накладными расходами, которых можно избежать, задав этот флаг; GLOB_DOOFFS: зарезервировать в начале списка строк gl_pathv место для нескольких указателей (их число задается в поле gl_offs до вызова). По лезно для подготовки массива аргументов, передаваемых функции execv; GLOB_NOCHECK: если ничего не было найдено, вернуть в качестве един ственного результата сам образец поиска; GLOB_APPEND: считать, что pglob указывает на результат предыдущего об ращения к glob(), и дописывать результаты поиска в конец буфера; GLOB_NOESCAPE: метасимволы нельзя экранировать символом \; В некоторых вариантах UNIX поддерживаются также нестандартные флаги, в частности: GLOB_ALTDIRFUNC: использовать альтернативные функции поиска файлов (задаваемые с помощью дополнительных полей в структуре glob_t, кото рые не показаны в ее определении выше). Это позволяет искать не только на диске, а, скажем, в ленточном архиве; GLOB_BRACE: разрешено использование фигурных скобок для задания аль тернатив, например, "{*.cpp,makefile*}" эквивалентно двум вызовам с указанием образцов "*.cpp" и "makefile*"; GLOB_NOMAGIC: если образец не содержит метасимволов, вернуть его в каче стве результата в случае, когда в файловой системе нет точного соответствия; GLOB_TILDE: выполнять подстановку вместо тильды, то есть разрешается указывать образцы вида "~/*.rb" или "~/sheppie/*.[rp][py]"; GLOB_ONLYDIR: искать только каталоги. Это считается лишь рекомендаци ей; полагаться на то, что будут пропущены все объекты, не являющиеся ка талогами, нельзя;
Адаптация API glob
171
GLOB_TILDE_CHECK: если задан флаг GLOB_TILDE и образец содержит тиль ду, игнорировать флаг GLOB_NOCHECK и вернуть GLOB_NOMATCH, когда ниче го не найдено; GLOB_PERIOD: обычно файлы и каталоги, имена которых начинаются с точ ки, пропускаются при поиске, если образец начинается с метасимвола. Если этот флаг задан, то такие файлы объекты тоже проверяются; GLOB_LIMIT: ограничить количество найденных объектов числом, задан ным в дополнительном поле gl_matchc. Если лимит превышен, возвраща ется код GLOB_NOSPACE, но структура glob_t содержит правильные дан ные (и должна освобождаться обращением к globfree()). Поскольку glob() может просматривать значительную часть файловой сис темы, при поиске могут возникать ошибки, например, запрет доступа к некоторым каталогам. Чтобы вызывающая программа могла получать о них информацию, не прерывая поиска, предусмотрен третий параметр glob() – указатель на функцию обработки ошибок. К сожалению, в glob() нельзя передать заданный пользовате лем контекст (например, параметр типа void*), который передавался бы без из менения функции обработки ошибок, и потому этот механизм в многопоточной программе практически бесполезен.
17.2. Анализ длинной версии Вернемся к длинной версии кода (листинг 17.1) и обратим внимание на инте ресные аспекты и проблемы. Строка 1: функция getLibDir() возвращает каталог, в котором находятся ваши заголовочные файлы. Строка 2: переменная gl объявлена, но не инициализирована; это нормально, поскольку при вызове glob() не указываются флаги GLOB_APPEND и GLOB_LIMIT. Строка 3: уродливая на вид конструкция (libraryDir + "/*/*algo*"). c_str() необходима для того, чтобы получить составной образец для поиска от каталога с вашей библиотекой. Если мы готовы модифицировать экземпляр libraryDir, то можно записать чуть более красиво: libraryDir.append ("\\*.h").c_str(), так как функция std::basic_string::append() возвра щает ссылку на объект, от имени которого вызвана. В любом случае без обраще ния к c_str() не обойтись. Это классический пример ситуации, в которой про кладки строкового доступа могут сделать клиентский код более простым, гибким и прозрачным, в чем мы убедимся, когда в следующем разделе приступим к напи санию класса расширения. Строка 4: задается флаг GLOB_MARK, который поможет нам в строках 31–33 исключить из результата каталоги. Строки 8–16: мы должны сами проверять код возврата, чтобы понять, как за вершился поиск: возникла ошибка, ничего не было найдено или был найден один либо несколько файлов. В случае ошибки я воспользовался гипотетическим клас сом исключения, который может нести в себе код и сообщение об ошибке.
172
Наборы
Мы могли бы объединить проверки в строках 8 и 12 и продолжить обработку со строки 19, если получен код GLOB_NOMATCH, но такое допущение было бы некор ректным. Хотя мне не встречалась реализация glob(), которая в случае ошибки не записывала бы в поля gl_pathc и gl_pathv структуры glob_t соответственно 0 и NULL, но я не нашел подтверждения этому в документации. Поэтому мы выби раем приведенный вариант. В любом случае альтернатива, хоть и короче, но менее понятна читателю (то есть бедному программисту, которому приходится сопро вождать код, а это могли бы быть и вы!). Строки 19–27: здесь мы обходим массив указателей, возвращенный функцией glob(), и для каждого файла вызываем Operation1(). Поскольку мы обрабаты ваем только файлы, а не каталоги, то перед тем как передавать имя функции Operation1(), должны проверить тип объекта. Функция stat() получает ин формацию о файле, зная путь к нему, и в частности возвращает тип в поле st_mode структуры struct stat. Проверив, поднят ли флаг S_IFREG в этом поле, мы можем отфильтровать все, кроме обычных файлов. Отметим, что файловая система – вещь динамическая, поэтому вполне может случиться, что объект, воз вращенный glob(), к моменту вызова stat() или Operation1() уже удален. Строки 29–37: здесь мы обходим массив указателей, возвращенный функцией glob(), и для каждого файла вызываем Operation2(). В этом цикле использует ся альтернативный метод фильтрации объектов. Если при вызове glob() задан флаг GLOB_MARK (строка 4), то в конец имени каждого каталога будет дописан символ '/'. Это избавит вас от необходимости добавлять его самостоятельно при построении путей. Но в данном случае мы воспользуемся этой возможностью для пропуска каталогов; достаточно сравнить последний символ имени с косой чер той. Хотя при этом вызывается функция strlen(), которая выполняет линейный поиск для каждого объекта, разумно будет предположить, что это гораздо быстрее обращения к stat(), поскольку не делается никакого системного вызова. Если добавление косой черты не вступает в противоречие с тем, как клиентский код собирается использовать результаты, то такая техника позволяет сократить на кладные расходы. Строки 39–41: Чтобы код возвращал количество обработанных элементов, нужно сохранить значение gl.gl_pathc перед вызовом globfree(). В докумен тации по API glob обычно говорится чтото в таком роде: «Функция globfree() освобождает память, динамически выделенную в момент предыдущего обраще ния к glob()». Мы могли бы заключить, что она не изменяет поля типа size_t, и вернуть gl.gl_pathc после обращения к globfree(), но это все же не безопасно. То, что я включил в программу два цикла с разными механизмами фильтра ции каталогов, могло бы показаться неуместным педагогическим вывертом, если бы у меня не было на то серьезных оснований: либо следует производить фильтра цию на каждой итерации цикла, либо полученный результат нужно скопировать для последующего использования. Таким образом, у исходного API glob есть ряд серьезных недостатков. Мы должны подготовить и предъявить образец для поиска в виде одной строки. Мы должны проверять возвращенное значение и локально реагировать на ошибки.
Адаптация API glob
173
Мы должны отфильтровывать ненужные объекты по типу. Мы должны вручную скопировать из возвращенной структуры интересующую нас информацию, преж де чем освобождать выделенную для нее память. Мы должны выполнять фильт рацию при каждом обходе результатов. И это еще не полная картина. В представ ленном коде имеются и не столь очевидные проблемы. Вопервых, если getLibDir() возвращает имя каталога с завершающей косой чертой, то образец будет содержать два символа косой черты подряд. Хотя на моей основной Linuxмашине glob благополучно «съедает» (и возвращает) пути с двой ной косой чертой, например /home/matthew//recls, я не читал в документации, что такое поведение обязательно для всех реализаций glob(). И вряд ли можно предполагать, что клиентские функции Operation1(), Operation2() и им подоб ные будут столь же снисходительны. Вовторых, glob() не приводит абсолютные пути к каноническому виду. Иными словами, если задан образец поиска "../*", а текущим является каталог /home/matthew/Dev, то вы получите имена, начинающиеся с "../*", а не с /home/ matthew. Хотя это ни в коем случае не ошибка, но иногда отсутствие такой функ циональности вызывает неудобства. Втретьих, массив указателей в структуре glob_t имеет тип char**, а не char const**. Следовательно, плохо написанный клиентский код может затереть его содержимое. Можно с большой долей уверенности предположить, что некоторые или даже большинство реализаций glob() не будут против этого возражать (при условии, что не было записи за пределами массива), но все же это потенциальный источник трудноуловимых ошибок при переносе. Лучше исключить такую воз можность в принципе. Хотя в примере это и не показано, но при поиске имен, начинающихся с точки, необходимо вручную отфильтровывать "." (текущий каталог) и ".." (родитель ский каталог) в тех случаях, когда вас интересуют каталоги. Иначе возможна двойная обработка или зацикливание. Мой опыт показывает, что большинству приложений, в которых приходится обходить файловую систему, эти два каталога не интересны. Наконец, представленный код не безопасен относительно исключений. Если Operation1() или Operation2() возбуждает исключение, то ресурсы, ассоцииро ванные с gl, не освобождаются. Эту проблему можно было бы решить с помощью класса, вводящего область видимости, удалив явное обращение к globfree() в строке 40 и добавив после строки 18 предложение, в котором используется шаб лон scoped_handle (раздел 16.5): stlsoft::scoped_handle r(&gl, ::globfree);
Но таким образом мы устраним только две проблемы: небезопасность относи тельно исключений и необходимость явно копировать поле gl.gl_pathc. И зас тавим пользователя помнить о том, что вместе с API glob нужно обязательно ис пользовать подобный класс. Так что по существу это не решение. Что нам нужно, так это класс, реализующий полноценный фасад (паттерн Facade или Wrapper) для glob(), который устранит все замеченные недостатки.
Наборы
174
17.3. Класс unixstl::glob_sequence Начиная с самой первой версии, в библиотеке UNIXSTL был класс, обертыва ющий glob() – glob_sequence, хотя со временем он претерпел ряд важных изме нений. Прежде чем знакомиться с его интерфейсом и реализацией, сделаем паузу и подумаем, как особенности API glob должны отразиться на природе соответ ствующего STLнабора. Поскольку glob() возвращает массив указателей на Cстроки, можно заклю чить, что мы имеем непрерывный итератор (раздел 2.3.6), и, следовательно, обход в обратном направлении поддерживается автоматически. Из спецификации API (точнее, из пробелов в ней) можно предположить, что, вопреки тому факту, что gl_pathv имеет тип char** (а не char const** или char* const* или даже char const* const*), мы не должны пытаться писать в области памяти, на которые указывают отдельные элементы этого массива, а также перемещать элементы с одного места на другое. Следовательно, glob_sequence – неизменяемый набор. Очевидно, что набор glob_sequence должен владеть собственными ресурса ми и применять идиому RAII (глава 11); конкретно это выражается в обращении к globfree() из деструктора. Коли так, то glob_sequence поддерживает фикси рованные ссылки на элементы (раздел 3.3.2). Наконец, поскольку состояние файловой системы может измениться в любой момент в результате действий произвольного процесса, то данный класс будет представлять лишь ее мгновенный снимок. Положение, которое отражает набор, может стать неактуальным, пока мы с ним работаем.
17.3.1. Открытый интерфейс Теперь рассмотрим открытый интерфейс класса glob_sequence. Он опреде лен в пространстве имен unixstl. В листинге 17.4 показано минимальное опреде ление класса, разбитое на логические секции. Листинг 17.4. Объявление класса glob_sequence //  ïðîñòðàíñòâå èìåí unixstl class glob_sequence { public: // Òèïû-÷ëåíû typedef char typedef char_type const* typedef value_type const& typedef value_type const* typedef glob_sequence typedef const_pointer typedef std::reverse_iterator typedef std::allocator typedef size_t typedef ptrdiff_t private:
char_type; value_type; const_reference; const_pointer; class_type; const_iterator; const_reverse_iterator; allocator_type; size_type; difference_type;
Адаптация API glob typedef filesystem_traits public: // Êîíñòàíòû-÷ëåíû enum { includeDots = 0x0008 , directories = 0x0010 , files = 0x0020 , noSort = 0x0100 , markDirs = 0x0200 , absolutePath = 0x0400 , breakOnError = 0x0800 , noEscape = 0x1000 , matchPeriod = 0x2000 , bracePatterns = 0x4000 , expandTilde = 0x8000 }; public: // Êîíñòðóèðîâàíèå template explicit glob_sequence(S const& pattern, int template< typename S1 , typename S2 > glob_sequence(S1 const& directory, S2 const& , int flags ~glob_sequence() throw(); public: // Ðàçìåð size_type size() const; bool empty() const; public: // Äîñòóï ê ýëåìåíòàì const_reference operator [](size_type index) public: // Èòåðàöèÿ const_iterator begin() const; const_iterator end() const; reverse_const_iterator rbegin() const; reverse_const_iterator rend() const; . . . private: // Ïåðåìåííûå-÷ëåíû . . . };
175 traits_type;
flags = noSort);
pattern = noSort);
const;
Структура класса понятна и элегантна; кроме конструкторов, в нем всего семь методов, и все они неизменяющие.
17.3.2. Типы"члены Если glob_t вообще можно назвать контейнером, то тип хранящихся в нем зна чений – char*. Однако, как мы уже говорили, это позволяет клиентскому коду зате реть содержимое буфера, поэтому в качестве типа значения для glob_sequence мы выбрали char const*. Мы увидим, что изза такого решения в нескольких местах потребуется выполнять приведение типа, но лучше сделать это в библиотеке, чем возлагать такое бремя на пользователей. Коль скоро glob_sequence является неизменяемым набором, типычлены reference, pointer и iterator не предоставляются. (О том, что влечет за собой такое решение, см. главу 13.)
Наборы
176
17.3.3. Переменные"члены Состояние экземпляра glob_sequence представлено пятью переменными членами, показанными в листинге 17.5: Листинг 17.5. ПеременныеZчлены класса glob_sequence private: // Ïåðåìåííûå-÷ëåíû typedef stlsoft::auto_buffer const int m_flags; char_type const** m_base; size_t m_numItems; buffer_type_ m_buffer; glob_t m_glob; };
buffer_type_;
Помимо члена m_glob типа glob_t, имеется еще четыре члена. m_flags содер жит проверенное сочетание флагов, переданных конструктору, m_numItems – ги потетическое число элементов в последовательности; изза механизма фильтра ции, предоставляемого классом glob_sequence, реальное число элементов может отличаться от того, что записано в поле m_glob.gl_pathc. Оставшиеся два члена – самые интересные. m_base указывает на первый ука затель на элемент, который должен быть доступен пользователю. Как и в случае m_numItems, его значение необязательно совпадает с тем, что хранится в поле m_glob.gl_pathv. Член m_buffer типа auto_buffer (раздел 16.2) используется в том случае, когда с массивом, возвращенным функцией glob(), нужно чтото сделать. Если потребуется, то каждый элемент массива m_glob.gl_pathv (указатель, а не строка, не забывайте об этом), который должен быть доступен пользователю, копируется сюда, после чего их можно безопасно сортировать.
17.3.4. Флаги При написании STLнаборов, для которых можно задавать флаги, у нас есть две возможности: либо принять любые сочетания флагов, определенных в ис ходном API, и насквозь передать их обертываемой функции, либо определить специфичные для расширения флаги, которые будут транслироваться во флаги API, и принимать только такие. Хотя это не сразу очевидно, попытка одновре менно поддержать обе формы безнадежна, так как API развиваются, и в любой момент может быть добавлен флаг, который уже определен как константачлен вашего класса. В данном случае выбор прост. Некоторые средства, предлагаемые классом glob_sequence, не находят прямого отражения в семантике glob(); речь идет о флагах directories, files и absolutePath. Мы уже говорили выше, что неко торые, но не все реализации поддерживают флаг GLOB_ONLYDIR, однако нет ни одной, которая позволяла бы возвращать только обычные файлы. Поэтому
Адаптация API glob
177
glob_sequence поддерживает фильтрацию файлов или каталогов в зависимости от того, задал ли пользователь флаги directories или files. Кроме того, glob() возвращает пути относительно заданного образца, тогда как glob_sequence мо жет возвращать абсолютные пути, если указан флаг absolutePath. Правило. Если фасад, обертывающий API, предусматривает флаги, то либо передавайте их без изменения функциям API, либо определяйте собственные флаги в отдельном про) странстве значений и транслируйте их во флаги API. Не смешивайте два подхода.
Все прочие поддерживаемые флаги – noSort (GLOB_NOSORT), markDirs (GLOB_MARK), breakOnError (GLOB_ERR), noEscape (GLOB_NOESCAPE), matchPeriod (GLOB_PERIOD), bracePatterns (GLOB_BRACE), expandTilde (GLOB_TILDE) – транслируются в соответствующие флаги API glob и передаются без дополни тельной обработки. Но те флаги, которые поддерживаются не на всех платфор мах, определены условно, как показано в листинге 17.6. Листинг 17.6. КонстантыZчлены класса glob_sequence public: // Êîíñòàíòû-÷ëåíû enum { . . . #ifdef GLOB_PERIOD , matchPeriod = 0x2000 #endif /* GLOB_PERIOD */ #ifdef GLOB_BRACE , bracePatterns = 0x4000 #endif /* GLOB_BRACE */ #ifdef GLOB_TILDE , expandTilde = 0x8000 #endif /* GLOB_TILDE */
Честно говоря, это не слишком красивый прием, но разумной альтернативы не видно. Можно было бы вообще запретить использование таких флагов в glob_sequence, но тем самым мы без всякой необходимости подрезали бы кры лья тем пользователям, которые работают на платформе, где они поддерживают ся. Или можно было бы сопоставить таким флагам фиктивное значение 0 в пере числении либо просто игнорировать их, но тогда поведение класса во время выполнения отличалось бы от объявленного на соответствующей платформе ин терфейса. Ни тот, ни другой вариант не завоюют много сторонников. Пусть уж лучше в этом случае абстракция будет дырявой. Совет. Не притворяйтесь, что фасад поддерживает функции, которые реализованы в раз) ных вариантах обертываемого API со значительными семантическими различиями. Будь) те осмотрительны, решаясь поддержать функции, которые существенно различаются по эффективности или по сложности.
Наборы
178
Отметим, что для такого классаобертки, как glob_sequence, некоторые флаги вообще не годятся, например: GLOB_DOOFFS, GLOB_APPEND и т.д. Они и не поддерживаются. Переданные конструктору флаги проверяются в закрытом ста тическом методе validate_flags_(), который приведен в листинге 17.7. Воз можно, многословность этого кода не приведет вас в восторг, но я предпочитаю соблюдать порядок и выравнивание; мне это помогает, когда необходимо некото рое дублирование (например, членов перечисления). Трюк с нулем в начале и в конце упрощает модификацию таких списков (ручную или автоматизирован ную). Работает он потому, что x | 0 == x для любого x. (При построении списков, объединяемых &, используйте ~0, так как x & ~0 == x для любого x.) Листинг 17.7. Проверка переданных конструктору флагов в методе validate_flags_() /* static */ int glob_sequence::validate_flags_(int flags) { const int validFlags = 0 | includeDots | directories | files | noSort | markDirs | absolutePath | breakOnError | noEscape #ifdef GLOB_PERIOD | matchPeriod #endif /* GLOB_PERIOD */ #ifdef GLOB_BRACE | bracePattern #endif /* GLOB_BRACE */ #ifdef GLOB_TILDE | expandTilde #endif /* GLOB_TILDE */ | 0; UNIXSTL_MESSAGE_ASSERT( "Çàäàíû íå ïîääåðæèâàåìûå ôëàãè" , flags == (flags & validFlags)); if(0 == (flags & (directories | files))) { flags |= (directories | files); } if(0 == (flags & directories)) { // Çà÷åì îáðàáàòûâàòü '.' è '..' ïî îòäåëüíîñòè, åñëè âñå êàòàëîãè // âñå ðàâíî îòôèëüòðîâûâàþòñÿ. flags |= includeDots; // Ïîñêîëüêó ìû íå ñîáèðàåìñÿ âîçâðàùàòü ïîëüçîâàòåëþ êàòàëîãè, // à äîâåðèòüñÿ ìåõàíèçìó ïîìåòêè êàòàëîãîâ, ðåàëèçîâàííîìó â glob(), // ýôôåêòèâíåå, ÷åì âûçûâàòü stat(), äîáàâèì ôëàã markDirs. flags |= markDirs; } return flags; }
Адаптация API glob
179
Эта функция решает три важные задачи. Вопервых, проверяет переданные конструктору флаги, сверяя их с теми, что поддерживаются на данной платформе. Контроль оформлен в форме предусловия (раздел 7.1), записанного в виде макро са UNIXSTL_MESSAGE_ASSERT(). По умолчанию этот макрос просто вызывает assert(), но пользователь может переопределить его по своему усмотрению, так чтобы его следы остались и в выпускной версии. Вовторых, если ни один из флагов files и directories не задан, то по умол чанию включаются оба. Это не более чем полезная услуга пользователю, который может задать только флаги, модифицирующие поведение, считая, что «все» будет искаться и так. Наконец, здесь же реализована некая оптимизация. Если glob_sequence не возвращает пользователю каталоги, то к заданным флагам мы добавляем еще includeDots и markDirs. Это позволяет нам не сравнивать имя каталога с "." и ".." (с помощью strcmp()) и не опрашивать тип файла (с помощью stat()), поскольку мы все равно отфильтровываем все имена, заканчивающиеся косой чертой. Как это работает, мы увидим позже при рассмотрении реализации.
17.3.5. Конструирование Благодаря работе, проделанной в validate_flags_() и еще в одном закры том методе init_glob_() (раздел 17.3.8), реализация конструкторов и деструк тора оказывается совсем простой: Листинг 17.8. Конструкторы и деструктор класса glob_sequence typename glob_sequence::glob_sequence(S const& pattern, int flags) : m_flags(validate_flags_(flags)) , m_buffer(1) { m_numItems = init_glob_(NULL, stlsoft::c_str_ptr(pattern)); UNIXSTL_ASSERT(is_valid()); } typename< typename S1 , typename S2 > glob_sequence::glob_sequence(S1 const& directory, S2 const& pattern , int flags) : m_flags(validate_flags_(flags)) , m_buffer(1) { m_numItems = init_glob_(stlsoft::c_str_ptr(directory) , stlsoft::c_str_ptr(pattern)); UNIXSTL_ASSERT(is_valid()); } . . . inline glob_sequence::~glob_sequence() throw() { UNIXSTL_ASSERT(is_valid()); ::globfree(&m_glob); }
180
Наборы
В обоих конструкторах для инициализации члена m_flags используется ме тод validate_flags_(), после чего в теле конструктора вызывается метод init_glob_(). Так делается для того, чтобы избежать проблем с упорядочением списков инициализации членов, так как для правильной работы init_glob_() необходимо, чтобы m_flags и m_buffer уже были инициализированы. Подобное смешение списка инициализации с кодом в теле конструктора обычно нежела тельно и должно вызывать подозрение у разработчика. Отчасти этим и вызвана проверка инварианта класса (метод is_valid()) в конце каждого конструктора и в начале деструктора (см. главу 7). Параметры directory и pattern передаются методу init_glob_() через прокладку строкового доступа c_str_ptr. Это позволяет использовать любой тип, для которого определена прокладка c_str_ptr, включая char const*, std::string и т.д. После конструирования содержимое glob_sequence не изменяется до момен та вызова деструктора; glob_sequence – неизменяемый набор. Следовательно, деструктору остается только вызвать globfree() для освобождения ресурсов, захваченных в glob(); член m_buffer приберет за собой самостоятельно. (Примечание. Это не все, что следует сказать о конструкторах glob_ sequence, как станет ясно в главе 18 – интерлюдии, следующей за данной главой. Там описывается общий прием, используемый, когда шаблонный конструктор применяется в сочетании с аргументомперечислением, и помогающий не попасть в западню неявных преобразований.)
17.3.6. Размер и доступ к элементам Методы size(), empty() и operator []() (листинг 17.9) элементарны, это следствие простоты представления данных для API glob. Листинг 17.9. Методы, относящиеся к размеру size_t glob_sequence::size() const { UNIXSTL_ASSERT(is_valid()); return m_numItems; } bool glob_sequence::empty() const { UNIXSTL_ASSERT(is_valid()); return 0 == size(); } const_reference glob_sequence::operator [](size_type index) const { UNIXSTL_MESSAGE_ASSERT( "â glob_sequence èíäåêñ âíå äèàïàçîíà" , index < 1 + size()); UNIXSTL_ASSERT(is_valid()); return m_base[index]; }
Адаптация API glob
181
Хотя в этом классе нет изменяющих методов, все равно в согласии с принци пом программирования по контракту лучше проверять инварианты класса во всех открытых методах. Совет. Проверяйте инварианты классы в начале (и в конце) всех открытых методов, в том числе и неизменяющих, чтобы повысить вероятность раннего обнаружения ошибок в дру) гих местах программы, которые привели к непреднамеренному изменению памяти.
У такой тактики есть и отрицательная сторона – создается впечатление, будто ваш код может содержать ошибки, тогда как на самом деле он, будучи добропоря дочным гражданином государства C++, просто предупреждает пользователей о том, что где%то произошла ошибка. Наша первейшая цель – правильность, а не политика, но на всякий случай можете повсюду носить с собой фотокопию этой страницы. Как известно, начальники становятся особенно бестолковыми, когда дело доходит до нюансов программирования по контракту.
17.3.7. Итерация Типы и методы итераторов в классе glob_sequence не вызывают особых воп росов. Поскольку glob() возвращает непрерывный блок указателей, то должна быть возможность поддержать и непрерывный итератор. Другими словами, тип const_iterator – это псевдоним (typedef) типа const_pointer (т.е. char const* const*), а const_reverse_iterator – псевдоним типа std::reverse_iterator . Именно поэтому я и выбрал для первого знакомства этот STLнабор, ведь применение идей STL к функции glob() абсолютно прозрачно (хотя на реализацию не относящейся к этой теме функциональности приходится затрачивать немалые усилия). Итак, методы итерирования очень просты и пока заны в листинге 17.10. Листинг 17.10. Методы итерирования const_iterator glob_sequence::begin() const { return m_base; } const_iterator glob_sequence::end() const { return m_base + m_numItems; } const_reverse_iterator glob_sequence::rbegin() const { return const_reverse_iterator(end()); } const_reverse_iterator glob_sequence::rend() const { return const_reverse_iterator(begin()); }
Наборы
182
17.3.8. Метод init_glob_() Вся оставшаяся часть реализации сосредоточена в закрытом методе экземпля ра init_glob_(), который приведен в листинге 17.11. Листинг 17.11. Общая структура метода init_glob_() size_t glob_sequence::init_glob_( char_type const* directory , char_type const* pattern) { int glob_flags = 0; file_path_buffer scratch; // Âðåìåííûé áóôåð äëÿ õðàíåíèÿ êàòàëîãà èëè // îáðàçöà size_t numItems; . . . // Ïîñòðîèòü àáñîëþòíûé ïóòü, åñëè íåîáõîäèìî . . . // Òðàíñëèðîâàòü ôëàãè if(0 != ::glob(. . . , &m_glob)) { throw glob_sequence_exception(); } stlsoft::scoped_handle cleanup(&m_glob, ::globfree); . . . // . . . // . . . // //
Îòôèëüòðîâàòü èìåíà, ñîñòîÿùèå èç òî÷åê, åñëè íåîáõîäèìî Îòôèëüòðîâàòü îñòàëüíûå ôàéëû èëè êàòàëîãè Ïåðåñîðòèðîâàòü ýëåìåíòû, åñëè çàòðåáîâàíà ñîðòèðîâêà è ÷òî-òî áûëî îòôèëüòðîâàíî
cleanup.detach(); return numItems; }
Обратите внимание на класс scoped_handle. Может возникнуть вопрос, по чему нужно применять идиому RAII к переменнойчлену m_glob, если класс glob_sequence сам управляет этим ресурсом, вызывая для него globfree() в де структоре. Причина в том, что в этот момент мы все еще находимся внутри конст руктора glob_sequence. C++ гарантирует автоматическое уничтожение только для полностью сконструированных объектов, поэтому, если исключение возник нет внутри конструктора класса, то деструктор не будет вызван. Поэтому мы бе рем ответственность на себя до тех пор, пока init_glob_() не вернет управление (успешно) конструктору. (Разумеется, после этого конструктор не должен вы полнять никаких действий, которые потенциально могли бы возбудить исключе ние.) Вызов scoped_handle::detach() гарантирует, что деструктор объекта cleanup ничего не будет делать; тем самым мы передаем ответственность за него экземпляру glob_sequence.
Адаптация API glob
183
Совет. Помните, что C++ автоматически вызывает деструктор только для полностью сконструированных объектов. Обращайтесь к классам, вводящим область действия, для объектов)членов, а если в конкретной ситуации это чересур накладно или почему)либо не годится, не забывайте при возникновении исключения явно освобождать уже захвачен) ные ресурсы в теле конструктора.
Определив общую структуру функции, перейдем к деталям. Начнем с построе ния абсолютного пути. Листинг 17.12. Построение абсолютного пути в init_glob_() if( NULL == directory && absolutePath == (m_flags & absolutePath)) { static const char_type s_thisDir[] = "."; directory = s_thisDir; } // Åñëè êàòàëîã çàäàí, òî ... if( NULL != directory && '\0' != *directory) { size_t len; // ... åñëè òðåáóåòñÿ, ïðåîáðàçóåì ïóòü â àáñîëþòíûé ... if(absolutePath == (m_flags & absolutePath)) { len = traits_type::get_full_path_name(directory , scratch.size(), &scratch[0]); } else { traits_type::str_copy(&scratch[0], directory); len = traits_type::str_len(scratch.c_str()); } // ... äîáàâèì ïðè íåîáõîäèìîñòè çàâåðøàþùèé ðàçäåëèòåëü êîìïîíåíòîâ ïóòè traits_type::ensure_dir_end(&scratch[0] + (len ? len - 1 : 0)); // ... è äîïèøåì êàòàëîã â íà÷àëî îáðàçöà. traits_type::str_cat(&scratch[0] + len, pattern); pattern = scratch.c_str(); }
Если задан флаг absolutePath, а сам каталог не задан, то directory указыва ет на неизменяемую статическую строку, состоящую из одной точки. Поскольку строка статическая, то она существует на протяжении всего времени работы про граммы (или, по крайней мере, единицы компоновки), так что использовать ее вполне безопасно. А поскольку это локальная статическая переменная, то ее не обязательно определять в файле реализации, так что компонент может целиком находиться в заголовочном файле, и с компоновщиком можно вообще не связы ваться.
184
Наборы
Совет. Пользуйтесь неизменяемыми локальными статическими строковыми литерала) ми, чтобы компоненты могли получить доступ к хорошо известным значениям без не) удобств, сопутствующих отдельному определению.
Далее каталог directory, если он задан, дописывается в начало строки pattern. Если флаг absolutePath задан, то предварительно directory преобра зуется в абсолютный путь во временном буфере scratch методом get_full_ path_name() класса traits_type (unixstl::filesystem_traits; см. раздел 16.3). В противном случае directory просто копируется в scratch. Еще один ме тод класса traits_type – ensure_dir_end() – дописывает в конец косую черту, если ее еще не было, а затем результат конкатенируется с pattern. Таким обра зом, мы решаем проблему двойной косой черты. Обратите внимание, что функции strcat() передается смещение в буфере (&scratch[0] + len), чтобы избежать потерь времени на поиск завершающего нуля от начала буфера. Повторное использование strcpy() привело бы к ошибке в случае, когда была добавлена косая черта. Поэтому мы вызываем strcat(), ко торая найдет нуль в первой или во второй из сканируемых позиций. Трансляция флагов, переданных конструктору glob_sequence, в значения, понятные API glob, – это просто набор предложений if, причем флаги, не опреде ленные в стандарте POSIX, окружены условными директивами препроцессора. Единственный нетривиальный случай – флаг GLOB_ONLYDIR, который задается, если флаг directories указан, а флаг files – нет (листинг 17.13). Листинг 17.13. Обработка флагов в init_glob_() if(m_flags & noSort) { glob_flags |= GLOB_NOSORT; } if(m_flags & markDirs) { glob_flags |= GLOB_MARK; } . . . #ifdef GLOB_ONLYDIR // Åñëè ýòîò ôëàã íå çàäàí, ïîëàãàåìñÿ íà stat if(directories == (m_flags & (directories | files))) { glob_flags |= GLOB_ONLYDIR; } #endif /* GLOB_ONLYDIR */ #ifdef GLOB_TILDE if(m_flags & expandTilde) { glob_flags |= GLOB_TILDE; } #endif /* GLOB_TILDE */
Теперь пришло время вызвать функцию glob(). Если она возвращает не 0, то возбуждается исключение glob_sequence_exception, которое передает полу
Адаптация API glob
185
ченный от glob() код возврата вызывающей программе. В противном случае на чинается обработка полученных от glob() результатов, в ходе которой решаются две основные задачи: исключение каталогов "." и ".." и фильтрация прочих файлов и каталогов. Если то или другое нужно делать, то предварительно все со держимое m_glob.gl_pathv копируется в буфер m_buffer, где им можно будет безопасно манипулировать. При этом размер m_buffer, который первоначально был равен 1, увеличивается до значения, равного числу в поле m_glob.gl_pathc (листинг 17.14). Листинг 17.14. Копирование элементов во внутренний буфер в методе init_glob_() if( 0 == (m_flags & includeDots) || (directories | files) != (m_flags & (directories | files))) { m_buffer.resize(numItems); ::memcpy(&m_buffer[0], base, m_buffer.size() * sizeof(char_type*)); }
Если операция resize() завершается с ошибкой и возбуждает исключение, то метод cleanup класса scoped_handle гарантирует вызов ::globfree() до того, как исключение будет передано программе, вызвавшей конструктор glob_sequence. Заполнив буфер, содержимым которого мы можем спокойно манипулиро вать, можно заняться отбрасыванием каталогов "." и ".." (листинг 17.15). Листинг 17.15. Отбрасывание каталогов "." и ".." в методе init_glob_() char** base = &m_buffer[0]; if(0 == (m_flags & includeDots)) { bool bFoundDot1 = false; bool bFoundDot2 = false; char** begin = base; char** end = begin + numItems; for(; begin != end && (!bFoundDot1 || !bFoundDot2); ++begin) { bool bTwoDots = false; if(is_dots_maybe_slashed_(*begin, bTwoDots)) { if(begin != base) { std::swap(*begin, *base); } ++base; —numItems; (bTwoDots ? bFoundDot2 : bFoundDot1) = true; } } }
186
Наборы
Мы поочередно просматриваем каждый элемент массива и проверяем, совпа дает ли он с одним из каталогов "." и "..". Закрытый статический метод is_dots_maybe_slashed_() возвращает true, если путь ведет на один из этих ка талогов, например ".", "../" или "/home/petshop/../", но не ".bashrc", и со общает, на какой именно. Если мы нашли интересующий каталог, то обмениваем его с тем, что находится по адресу *base, и увеличиваем base на единицу. Как только оба каталога будут найдены, просмотр прекращается, поскольку мы точно знаем, что ни одна уважающая себя операционная система не вернет более одного каталога каждого вида. В результате оба имени, содержащие только точки, ока жутся в начале m_buffer, а base будет указывать на первый из оставшихся ката логов. Таким образом, мы сумели убрать не интересующие нас каталоги, не вызы вая memove() и не прибегая к перераспределению памяти. Из серьезных задач осталось только отфильтровать файлы или каталоги. Механизм такой же, как при отбрасывании "." и ".." – обмен с base и сдвиг на следующий элемент, но по ходу дела приходится проверять тип объекта (лис тинг 17.16). Листинг 17.16. Фильтрация файлов и каталогов в методе init_glob_() if((m_flags & (directories | files)) != (directories | files)) { file_path_buffer scratch2; char_type** begin = base; char_type** end = begin + numItems; for(; begin != end; ++begin) { struct stat st; char_type const* entry = *begin; if(files == (m_flags & (directories | files))) { UNIXSTL_ASSERT(markDirs == (m_flags & markDirs)); if(!traits_type::has_dir_end(entry)) { continue; // Íåïîìå÷åííûé ýëåìåíò, òî åñòü ôàéë; îñòàâëÿåì } } else { if(markDirs == (m_flags & markDirs)) { if(traits_type::has_dir_end(entry)) { continue; // Ïîìå÷åííûé ýëåìåíò, òî åñòü êàòàëîã; îñòàâëÿåì } } else if(0 != ::stat(entry, &st)) { // Ìîæíî áû áû çäåñü âîçáóäèòü èñêëþ÷åíèå, íî âäðóã ýòî ñëó÷èëîñü // ïîòîìó, ÷òî ôàéë áûë óäàëåí ïîñëå âêëþ÷åíèÿ â ñïèñîê glob? // Ïîýòîìó ðàçóìíåå ïðîñòî óäàëèòü åãî èç ñïèñêà. }
Адаптация API glob
187
else if(S_IFDIR == (st.st_mode & S_IFDIR)) { continue; // Êàòàëîã, îñòàâëÿåì } } // Îáìåíÿòü ñ òåì, ÷òî íàõîäèòñÿ â ïîçèöèè base[0] std::swap(*begin, *base); ++base; —numItems; } }
Если пользователь запрашивал только файлы (задан флаг files), то каталоги будут помечены, и мы можем просто проверять наличие завершающей косой чер ты. Если пользователь запрашивал только каталоги (задан флаг directories), то надо еще посмотреть, просил ли он дописывать в конец косую черту. Если нет, то придется вызывать stat() и проверять, поднят ли флаг S_IFDIR. Отметим, что если stat() завершается с ошибкой, то мы предполагаем, что соответствующий объект был удален или недоступен. В этом случае логично пропустить его, обме няв с элементом в начале массива. И последний шаг – восстановить сортировку (если требуется) и присвоить члену m_base значение base (листинг 17.17). Листинг 17.17. Необязательная сортировка элементов в методе init_glob_() if( 0 == (m_flags & noSort) && numItems != static_cast<size_t>(m_glob.gl_pathc)) { std::sort(base, base + cItems); }
Осталось только вызвать метод detach() объекта scoped_handle и вернуть число элементов (уже было показано в листинге 17.11).
17.4. Анализ короткой версии Поняв, как работает класс glob_sequence, мы можем вернуться к короткой версии программы (листинг 17.2). Строка 1: используем определение glob_sequence в пространстве имен unixstl. Можно было бы указать полностью квалифицированное имя, но тогда пришлось бы квалифицировать тип gls и флаг files для фильтрации файлов, так что лучше воспользоваться usingобъявлением и сэкономить себе время. Строка 2: конструируем экземпляр класса glob_sequence, передавая то, что вернула функция getLibDir(), образец поиска и флаг files. Шаблонный конст руктор вызовет прокладку c_str_ptr для обоих аргументов: имени каталога (std::string) и образца (char const*), чтобы преобразовать их в Cстроки (char const*). Параметр flags пропускается через метод validate_flags_(), который добавит флаги includeDots и markDirs, чтобы минимизировать наклад ные расходы на фильтрацию. Затем обе строки и флаги передаются методу
188
Наборы
init_glob_(), который вызывает glob() и обрабатывает результат. Изза спосо ба обработки аргументов внутри init_glob_() для нас несущественно, добавила getLibDir() завершающую косую черту или нет; результат все равно будет сформирован правильно. Если init_glob_() завершается успешно, то объект glob_sequence полностью сконструирован и владеет своим ресурсом, храня щимся в члене m_glob, который, следовательно, будет освобожден в деструкторе, вызываемом после строки 7. Если внутри init_glob_() произойдет ошибка, то
он возбудит исключение, которое будет передано вызывающей программе. Строка 4: получаем от gls итераторы для всего диапазона с помощью методов glob_sequence::begin() и glob_sequence::end() и передаем их алгоритму std::for_each() вместе с привязанным к std::ptr_fun адресом функции Operation1(). std::for_each() обходит диапазон, передавая каждый элемент в Operation1(). Строка 5: обход в обратном направлении достигается за счет вызова методов glob_sequence::rbegin() и glob_sequence::rend() и передачи полученных обратных итераторов вместе с привязанной к std::ptr_fun функции Operation2() алгоритму std::for_each(). Строка 7: для возврата количества найденных объектов файловой системы вызываем метод glob_sequence::size(). Так как вызвать его для уже уничто женного экземпляра невозможно, проблема получения устаревшего значения не возникает.
17.5. Резюме После такого долгого обсуждения приличествует оглядеться и понять, чего мы достигли. Корректность относительно const. Пользователи класса glob_sequence защищены от необдуманных действий, нарушающих константность. Инкапсуляция. Пользователям класса glob_sequence не нужно думать о том, что элементы поступили в один массив, а затем, возможно, были пе ремещены в другой. Они работают на уровне общепринятых идиом STL с помощью итераторов или индексов. Идиома RAII. Ресурсы управляются экземпляром glob_sequence и авто матически освобождаются в деструкторе. Фильтрация. Класс самостоятельно отбирает только файлы или только каталоги и отбрасывает (обычно нежелательные) каталоги '.' и '..'; пользователям об этом печалиться не надо. Гибкость. Экземпляр glob_sequence можно сконструировать из любого типа, для которого определена прокладка строкового доступа. Мощь. Класс glob_sequence предоставляет дополнительные по сравне нию с glob() средства, а именно может приводить путь к каталогу поиска к абсолютной форме и автоматически гарантирует, что имя каталога и об разец правильно объединены.
Адаптация API glob
189
Эффективность. Тип значения равен char const*; такие меры, как исполь зование флага char const*, помогают провести оптимизацию, не вызывая никаких дополнительных накладных расходов в случае, когда оптимиза ция невозможна. Затраты на конструирование (вместе с неотъемлемым от него поиском) разложены на сколь угодно большое количество операций доступа к результирующей последовательности (в прямом, обратном или произвольном порядке). Честно говоря, реализация glob_sequence довольно сложна. Это типично для библиотечных компонентов общего назначения, посколько они должны рабо тать (и правильно работать) в разнообразных контекстах. Я так подробно все опи сывал в частности потому, что полагаю чрезвычайно важным показывать реаль ные примеры расширения STL, а реальность никогда не обходится без темных уголков. В этой книге я собираются осветить все такие уголки, но не для того, что бы отвлечь вас от основной темы, а чтобы постоянно напоминать вам (и себе само му), как такого рода сложности могут оказывать (и оказывают) влияние на проек тирование расширений, их семантику, надежность и, конечно же, эффективность. Помните, в главе 6 мы говорили, что все абстракции протекают. В данном случае практически вся сложность сосредоточена в методе init_glob_() и обусловлена взаимодействием с обертываемым API glob. Относя щиеся к STL детали – непрерывные итераторы, фиксированные ссылки на элемен ты, неизменяемые наборы и так далее – не представляют никаких проблем. Даже тем, кто не любит библиотеку STL или не пользуется ей, класс glob_sequence все равно покажется удобным. Поскольку он поддерживает произвольный доступ, к их услугам функция size() и оператор индексирования. В главе 19 мы столкнемся с полярно противоположной ситуацией. Рассматри вая еще один UNIX API с очень простой семантикой, мы обнаружим, что расши рение STL для него оказывается довольно сложным, с более строгими ограниче ниями. Но сначала короткая интерлюдия.
Глава 18. Интерлюдия: конфликты в конструкторах и дизайн, который не то чтобы плох, но мало подходит для беспрепятственного развития Ценность хорошего проекта превышает его стоимость. – Томас К. Гэйл Должен признаться, что конструкторы, показанные в листинге 17.8 и описанные в разделе 17.3.5, отличаются от настоящих. Чтобы понять, как они устроены на самом деле и почему я отклонился в тексте от идеала, нужно обратиться к истории этого класса. В первоначальной версии было всего два нешаблонных конструктора: glob_sequence(char_type const* directory , int flags = noSort); // NT1 glob_sequence(char_type const* directory , char_type const* pattern , int flags = noSort); // NT2
Хорошо это или плохо, но при обновлении класса следует стремиться к обес печению обратной совместимости. На первый взгляд, конструкторы, обсуждав шиеся в разделе 17.3.5, хорошо отвечают этому требованию: template explicit glob_sequence(S const& pattern, int flags = noSort); // T1 template< typename S1 , typename S2 > glob_sequence(S1 const& directory, S2 const& pattern , int flags = noSort); // T2
Действительно, во втором варианте есть два параметра шаблона, поддержи вающие произвольное сочетание строковых типов в аргументах directory и pattern. Совет. Старайтесь делать шаблоны функций и методов максимально гибкими, задавая разные типы для каждого параметра.
Конфликты в конструкторах и дизайн
191
К сожалению, наличие параметров по умолчанию может приводить к нео днозначности при интерпретации некоторых конструкций. Рассмотрим следую щие объявления: string_type s = "*"; glob_sequence gls1("*"); // Ðàáîòàåò ñ NT1 è T1 glob_sequence gls2("*" , glob_sequence::noSort); // Ðàáîòàåò ñ NT1, íî íå ñ T1 glob_sequence gls3("*", glob_sequence::noSort | glob_sequence::markDirs); // Ðàáîòàåò ñ NT1 è T1
Если типы T1 и T2 доступны, то компилятор при конструировании gls2 вы берет форму с тремя параметрами. Причина довольно тонкая, но ее обязан пони мать каждый, кто хочет писать переносимые и гибкие библиотеки. Хотя пере числения могут быть неявно преобразованы в тип int, они не являются экземплярами типа int. На самом деле glob_sequence::noSort имеет тип glob_sequence::_anonymous_enum_, где _anonymous_enum_ – сгенерированное имя, зависящее от компилятора. (В компиляторах Comeau 4.3.3 и Intel 8 этот тип называется glob_sequence::, в Digital Mars – glob_sequence::__ unnamed, в GCC – glob_sequence::. Пожалуй, самое осмыс ленное, но, безусловно, менее полезное имя принято в Visual C++ 7.1, где этот тип называется просто ' '.) Таким образом, увидев любой тип, кроме int, компилятор выберет второй из перегруженных вариантов. Поскольку результат арифметической операции над членами перечисления имеет тип int, как в случае noSort | markDirs, то конст руирование gls3 проходит нормально. Проявив упорство, мы можем добиться того, что и конструирование gls2 будет компилироваться, по крайней мере, боль шинством компиляторов: glob_sequence gls2("*" , glob_sequence::noSort | 0); // Ðàáîòàåò ñ NT1 è T1
Но, конечно, не стоит ожидать, что ктонибудь будет пользоваться библиоте кой, которая требует таких мер. Разумный подход состоит в том, чтобы опреде лить дополнительные перегрузки, которые позволят задавать один член перечис ления. Для этого нужно присвоить перечислению имя, на которое можно будет сослаться в сигнатуре функции (листинг 18.1). Листинг 18.1. Усовершенствование glob_sequence для повышения гибкости class glob_sequence { . . . public: // Êîíñòàíòû-÷ëåíû enum search_flags { . . . }; public: // Êîíñòðóèðîâàíèå template explicit glob_sequence(S const& pattern, int flags = noSort); // T1
192
Наборы
template explicit glob_sequence(S const& pattern, search_flags flag); // T1b template< typename S1 , typename S2 > glob_sequence(S1 const& directory, S2 const& pattern , int flags = noSort); // T2 template< typename S1 , typename S2 > glob_sequence(S1 const& directory, S2 const& pattern , search_flags flag); // T2b . . .
Совет. Определяйте перегруженные варианты для аргумента типа int и типа перечис) ления, если такие аргументы могут выступать в роли флагов, допускающих комбини) рование.
Глава 19. Адаптация API opendir/readdir Не пишите 200 строк кода, когда хватит и 10. – Хэл Фултон STL – плоть и кровь твоей диссертации, не бросай ее и относись с такой же любовью, как сердечники к сливочному маслу. – Джордж Фразье
19.1. Введение В этой главе мы займемся простым UNIX API opendir/readdir и увидим, что, несмотря на гораздо более простую семантику, чем у API glob (глава 17), написать для него работоспособное расширение STL куда сложнее, а семантика получаю щегося набора оказывается более ограничительной. В этом расширении мы впервые встретимся с итераторами типа класса, а эта концепция очень важна. Поэтому сначала я продемонстрирую неправильный спо соб написания таких классов, а потом выведу вас на путь истинный.
19.1.1. Мотивация Предположим, что нам необходимо перебрать все подкаталоги каталога при ложения, путь к которому возвращает функция getWorkplaceDirectory(), и со хранить полный путь к каждому из них в векторе строк для последующего ис пользования, например, чтобы показать их пользователю в диалоговом окне. В листинге 19.1 показано, как решить эту задачу с помощью API opendir/readdir. Листинг 19.1. Перебор каталогов с помощью API the opendir/readdir 1 std::vector<std::string> getWorkplaceSubdirectories() 2 { 3 std::string searchDir = getWorkplaceDirectory(); 4 std::vector<std::string> dirNames; 5 DIR* dir = ::opendir(searchDir.c_str()); 6 if(NULL == dir) 7 { 8 throw some_exception_class("Íå ìîãó ïåðåáðàòü êàòàëîãè", errno); 9 } 10 else
Наборы
194 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 }
А
{ struct dirent* entry; if('/' != searchDir[searchDir.size() - 1]) { searchDir += '/'; } for(; NULL != (entry = ::readdir(dir)); ) { if( '.' == entry->d_name[0] && ( '\0' == entry->d_name[1] || // "." ( '.' == entry->d_name[1] && '\0' == entry->d_name[2]))) // ".." { // Êàòàëîã '.' èëè '..', ïðîïóñêàåì } else { struct stat st; std::string entryPath = searchDir + entry->d_name; if(0 == ::stat(entryPath.c_str(), &st)) { if(S_IFDIR == (st.st_mode & S_IFDIR)) { dirNames.push_back(entryPath); } } } } ::closedir(dir); } return dirNames;
теперь
сравните
с
версией,
в
которой
используется
класс
readdir_sequence:
Листинг 19.2. Перебор каталогов с помощью класса readdir_sequence 1 2 3 4 5 6 7
std::vector<std::string> getWorkplaceSubdirectories() { using unixstl::readdir_sequence; readdir_sequence rds(getWorkplaceDirectory() , readdir_sequence::directories | readdir_sequence::fullPath); return std::vector<std::string>(rds.begin(), rds.end()); }
Как и в случае glob_sequence (раздел 17.3), она побивает исходный вариант и по числу строк, и по надежности (в особенности с точки зрения безопасности относительно исключений), и по выразительности, и по понятности. Сомнение может вызвать только производительность. Но и тут нас поджидает приятный сюрприз. В таблице 19.1 приведены результаты, полученные таким же способом, как для glob_sequence; но на этот раз мы перебирали содержимое начального каталога дистрибутива STLSoft.
Адаптация API opendir/readdir
195
Таблица 19.1. Производительность системного API и адаптированного к STL класса Операционная система
Системный API Класс readdir_sequence
Linux (700MHz, 512MB Ubuntu 6.06; GCC 4.0) Win32 (2GHz, 1GB, Windows XP; VC++ 7.1)
2,487 мс 16,040 мс
2,091 мс 15,790 мс
И снова выигрыш, куда ни глянь. Посмотрим, как мы этого достигли.
19.1.2. API opendir/readdir API opendir/readdir включает четыре стандартных и две нестандартных функ ции и одну структуру (в которой имеется всего одно обязательное поле). Листинг 19.3. Типы и функции, составляющие API opendir/readdir struct dirent { char d_name[]; // Èìÿ êàòàëîãà . . . // Ïðî÷èå íåñòàíäàðòíûå ïîëÿ }; struct DIR; // Íåïðîçðà÷íûé òèï, îïèñûâàþùèé òåêóùóþ òî÷êó â ïðîöåññå // ïðîñìîòðà êàòàëîãà DIR* opendir(const char* dir); // Íà÷èíàåò ïðîñìîòð êàòàëîãîâ int closedir(DIR*); // Çàêàí÷èâàåò ïðîñìîòð struct dirent* readdir(DIR*); // ×èòàåò ñëåäóþùèé ýëåìåíò void rewinddir(DIR*); // Âîçîáíîâëåíèå ïðîñìîòðà ñ íà÷àëà long int telldir(DIR*); // Ïîëó÷èòü òåêóùóþ ïîçèöèþ void seekdir(DIR*, long int); // Ïåðåéòè ê óêàçàííîé ïîçèöèè
Четыре функции – opendir(), closedir(), readdir() и rewinddir() – оп ределены в стандарте POSIX; telldir() и seekdir() – расширения. В этой главе мы будем иметь дело только с первыми тремя функциями. opendir() начинает просмотр каталога с указанным путем и при успешном завершении возвращает ненулевое значение непрозрачного типа DIR*, которое передается остальным функциям и описывает состояние просмотра. Функция readdir() читает следу ющий элемент каталога и возвращает NULL, если больше элементов не осталось или произошла ошибка. closedir() завершает просмотр и освобождает ресурсы, выделенные внутри opendir() и readdir(). Значение, возвращенное readdir(), – это указатель на структуру типа struct dirent, в которой должно быть по мень шей мере поле d_name, являющееся либо массивом символов, либо указателем на такой массив. В это поле записывается имя текущего элемента каталога.
19.2. Анализ длинной версии Вернемся к длинной версии кода (листинг 19.1) и обратим внимание на инте ресные аспекты и проблемы. Строка 3. Мы получаем изменяемую копию рабочего каталога, поэтому при необходимости можем дописать в конец косую черту (строки 13–16), чтобы при
196
Наборы
конкатенации с очередным именем получился правильно сформированный путь для передачи в stat() (строка 30). Строка 4. Объявляем экземпляр std::vector<std::string>, в которой бу дем помещать обнаруженные в каталоге элементы. Строки 5–9. Вызываем opendir(), чтобы начать поиск, и возбуждаем исклю чение в случае ошибки. Строка 13. Если getWorkplaceDirectory() вернет пустую строку, то здесь будет предпринята попытка обратиться к элементу с индексом size_type(0)—1. Он равен 0xFFFFFFFF или какомуто другому столь же огромному числу в зависи мости от размера size_type. В любом случае это приведет к нарушению защиты памяти и краху программы. Чтобы сделать эту строку безопасной, нужно срав нить searchDir.size() с нулем. Не знаю, как вас, а меня такие вещи всегда раз дражают. Строки 19–25. Некоторые версии readdir() «видят» каталоги "." и "..", по этому мы проверяем, не получили ли их, но при этом не отфильтровываем имена, которые просто начинаются с одной или двух точек (вряд ли это комуто понрави лось бы, правда?). Строка 29. Конкатенируем имя просматриваемого каталога и текущего эле мента, формируя полный путь. Отметим, что для каждого элемента мы создаем новый экземпляр std::string, что подразумевает выделение (и освобождение) памяти для хранения результата. Строки 30–32. Обращаемся к системному вызову stat(), чтобы проверить, является ли только что полученный элемент каталогом. Передавать stat() одно лишь имя можно только тогда, когда просматривается текущий каталог, а в дан ном случае это очевидно не так. Строка 34. Помещаем полный путь в контейнер. Строка 39. Завершаем поиск, освобождая все выделенные для него ресурсы. Строка 41. Возвращаем профильтрованный результат вызывающей программе. Как и в случае API glob из предыдущей главы, длинная форма многословна, неудобна, неэффективна и содержит трудноуловимые ошибки. Добавление косой черты в конец searchDir, ручное сравнение имен с "." и ".." и необходимость вызывать c_str() для экземпляров строк не добавляют ни удобства, ни лаконич ности. Обращение к stat() для фильтрации каталогов – это тоже вещь, которую в идеале лучше бы оставить библиотеке. Самая очевидная причина неэффектив ности заключается в том, что для создания нового экземпляра entryPath нужно по крайней мере один раз выделить память из кучи для каждого элемента катало га, но есть и более тонкая проблема – тот факт, что переменная dirNames объявле на явно, означает, что при возврате из функции возможна только оптимизация именованного возвращаемого значения (named return value optimization – NRVO), а не оптимизация возвращаемого значения (return value optimization – RVO). (По сравнению с затратами времени на просмотр файловой системы невоз можность RVOоптимизации не так уж существенна. Но аналогичная ситуация может возникнуть и при переборе, для которого относительные накладные расхо ды меньше, поэтому я решил привлечь к ней ваше внимание.)
Адаптация API opendir/readdir
197
Даже если со всем прочим можно примириться, остается небезопасность этого кода относительно исключений. Исключения могут возникнуть в строках 15, 29 и 34, и тогда в строке 39 не будут освобождены выделенные ресурсы. Совет. Смешение C++ (особенно STL) с API, написанными на C, неизменно открывает много дыр, через которые могут утекать ресурсы, особенно (но не только) при возникно) вении исключений. Всюду, где возможно и эффективно, старайтесь пользоваться паттер) ном Facade (он же Wrapper).
Если подходящих классов нет, напишите сами. Даже если единственным вы игрышем будет RAII и несколько параметров, имеющих значения по умолчанию, все равно надежность заметно повысится (не говоря уже о том, что приобретен ный опыт никогда не пропадет даром).
19.3. Класс unixstl::readdir_sequence Прежде чем приступать к разработке класса readdir_sequence, посмотрим, как особенности API opendir/readdir влияют на характеристики расширения STL. API opendir/readdir предоставляет косвенный доступ к элементам набора, поэтому нам потребуется итераторный класс, а не указатель. Мы можем переходить от одного элемента каталога к следующему, но воз вращаться разрешено только в начало. Такой механизм не позволяет под держать ни двунаправленный итератор, ни итератор с произвольным досту% пом (раздел 1.3), поэтому наш API будет в лучшем случае поддерживать однонаправленный итератор. Каждое обращение к readdir() продвигает вперед позицию текущего эле мента. Поэтому просмотр, начатый обращением к opendir(), однопроход ный, следовательно, мы будем иметь итератор ввода. API не предоставляет средств для изменения содержимого каталога, то есть поддерживает лишь неизменяющий доступ. readdir() возвращает указатель на структуру struct dirent*, в которой, согласно стандарту, обязано быть лишь поле d_name, содержащее заверша ющееся нулем имя элемента (все остальное не переносимо). Поэтому ти пом значения для набора будет char const*. Нет никаких гарантий, что последовательные вызовы readdir() возвра щают указатель на одну и ту же область памяти, предыдущее содержимое которой каждый раз затирается; это могут быть и указатели на разные об ласти. Следовательно, в экземпляре итератора должен храниться указа тель на struct dirent, а не на поле d_name. Каждое обращение к opendir() начинает новый просмотр. Следователь но, с вызовом opendir() должен быть ассоциирован вызов метода readdir_sequence::begin(). Пока не ясно (и, как выяснится, несуще ственно), должен ли набор вызывать opendir() и передавать полученный
198
Наборы
указатель DIR* классу итератора, или класс итератора сам будет вызывать opendir(), пользуясь информацией, предоставленной набором. Точно так же, не вполне ясно, кто выполняет первое обращение к readdir(): сам набор или итератор. Для просмотра следующего элемента необходимо вызывать readdir(), и это должно происходить в операторе инкремента итератора. Чтобы поддержать несколько обходов одного и того же экземпляра набора, класс итератора должен владеть описателем поиска DIR*, поскольку имен но инкремент итератора рано или поздно приводит к завершению просмот ра (либо по достижении позиции end(), либо в результате выхода итерато ра из области видимости). Проведенный анализ позволяет сделать следующие выводы: тип значения должен быть char const*; ссылки на элементы – недолговечные; итератор дол жен относиться к категории ввода, а сам набор неизменяемый. Получившийся ин терфейс класса readdir_sequence представлен в листинге 19.4. Листинг 19.4. Первоначальная версия класса readdir_sequence //  ïðîñòðàíñòâå èìåí unixstl class readdir_sequence { private: // Òèïû-÷ëåíû typedef char char_type; public: typedef char_type const* value_type; typedef std::basic_string string_type; typedef filesystem_traits traits_type; typedef readdir_sequence class_type; class const_iterator; public: // Êîíñòàíòû-÷ëåíû enum { includeDots = 0x0008 , directories = 0x0010 , files = 0x0020 , fullPath = 0x0100 , absolutePath = 0x0200 }; public: // Êîíñòðóèðîâàíèå template readdir_sequence(S const& dir, int flags = 0); public: // Èòåðàöèÿ const_iterator begin() const; const_iterator end() const; public: // Ðàçìåð bool empty() const; public: // Àòðèáóòû string_type const& get_directory() const; // Âñåãäà çàâåðøàåòñÿ // ñèìâîëîì '/' int get_flags() const;
Адаптация API opendir/readdir private: // Ðåàëèçàöèÿ static int validate_flags_(int flags); static string_type validate_directory_(char const* , int private: // Ïåðåìåííûå-÷ëåíû const int m_flags; const string_type m_directory; private: // Íå ïîäëåæèò ðåàëèçàöèè readdir_sequence(class_type const&); class_type& operator =(class_type const&); };
199
directory flags);
19.3.1. Типы и константы"члены В состав типовчленов входят string_type (он понадобится позже), тип зна чения и опережающее объявление вложенного класса const_iterator. Есть два основных способа написания итераторов для конкретных наборов. Можно, как мы поступили здесь, определять их в виде вложенных классов, имена которых со ответствуют той роли, которую они играют. А можно в виде отдельных классов, например readdir_sequence_const_iterator, которые затем включаются как типычлены с помощью typedef. На выбор влияют несколько факторов, как то: является ли набор шаблонным, терпимость (человека) к длинным именам типов, может ли данный тип итератора быть использован для нескольких наборов (раз дел 26.8) и так далее. Совет. Определяйте классы итераторов, применимых только к одному классу набора, в виде вложенных классов. Это снижает загрязнение пространства имен и проясняет связь между итератором и относящейся к нему последовательностью.
Константы описывают поведение набора, в частности смысл includeDots, directories и files такой же, как для glob_sequence. Обсуждение констант fullPath и absolutePath мы отложим до раздела 19.3.11. Тип traits_type определен на основе шаблона unixstl::filesystem_ traits (раздел 16.3), который абстрагирует различные средства, используемые в реализации. Тип char_type, определенный как char, применяется в определе нии класса, исходя из того, что в один прекрасный день этот класс может быть преобразован в шаблон для обобщения на вариант API opendir/readdir, работаю щий с широкими символами. (В нем определены типы wDIR и struct wdirent, для манипуляций которыми предназначены функции wopendir(), wreaddir() и т.д.) Совет. Определяйте типы)члены так, чтобы через них можно было определять остальные необходимые типы. Это поможет избежать нарушений принципа DRY SPOT.
Типу traits_type по справедливости следует быть закрытым, но он сделан открытым, потому что const_iterator должен его видеть.
200
Наборы
19.3.2. Конструирование В отличие от glob_sequence (раздел 17.3.5, глава 18), в классе readdir_ sequence имеется всего один открытый конструктор, гибкость которого обеспе чена использованием прокладки строкового доступа c_str_ptr (раздел 9.3.1): Листинг 19.5. Шаблонный конструктор класса readdir_sequence class readdir_sequence { . . . public: // Êîíñòðóèðîâàíèå template readdir_sequence(S const& dir, int flags = 0) : m_directory(stlsoft::c_str_ptr(dir)) , m_flags(validate_flags_(flags)) {} . . .
Метод validate_flags_() мы обсуждать не будем, так как он мало чем отли чается от одноименного метода в классе glob_sequence (раздел 17.3.4). Согласно закону большой двойки, деструктор не нужен, так как единственный ассоциированный с объектом ресурс имеет тип string_type, поэтому версии, сге нерированной компилятором, будет достаточно. Следовательно, этот тип без до полнительных усилий со стороны автора мог бы поддержать требования Assignable и CopyConstructible, предъявляемые к STL%контейнеру (C++ 03: 23.1;3). Однако конструктор копирования и копирующий оператор присваивания запре щены. Почему? Потому что класс readdir_sequence, как и glob_sequence (и практически все прочие API для обхода файловой системы), предоставляет лишь мгновенный снимок состояния системы. Запрет семантики копирования не позволит пользователю забыть об этом. Совет. Разрабатывая свои типы, подумайте, не стоит ли запретить некоторые операции для того, чтобы напомнить о правильном способе использования, а также для обеспече) ния надежности и корректности. Особенно это актуально в отношении типов, описываю) щих мгновенное состояние обертываемых наборов.
19.3.3. Методы, относящиеся к размеру и итерированию Методы begin() и end() возвращают экземпляры типа const_iterator. Ни изменяющих, ни обратных итераторов не предоставляется в силу однопроходной и не допускающей изменения природы API opendir/readdir. Что до размера, так для API opendir/readdir осмыслен только метод empty(), метод size() не предусмотрен. На первый взгляд, это представляется вопиющим упущением, и в некотором смысле так оно и есть. Но, поскольку API возвращает
Адаптация API opendir/readdir
201
по одному элементу за обращение, то единственный переносимый способ реали зовать метод size() мог бы выглядеть следующим образом: size_type readdir_sequence::size() const { return std::distance(begin(), end()); }
С точки зрения семантики, ничего плохого в такой реализации нет. Но время выполнения этой операции не постоянно, как принято ожидать (хотя такого тре бования нет!) от стандартных STLконтейнеров (C++03: 23.1), а, следовательно, и от расширений STL, а, скорее, имеет порядок O(n) (в зависимости от реализа ции opendir/readdir). Стало быть, хотя синтаксис и семантика одинаковы, слож ность существенно отличается от ожидаемой. Тут мы имеем типичный случай правила гуся (раздел 10.1.3). Поэтому мы не стали реализовывать этот метод, намекая тем самым, что по добная операция потенциально накладна. Если пользователь захочет, то сможет реализовать ее самостоятельно, но отсутствие готового метода заставит его заду маться о последствиях. Совет. Если это оправдано, опускайте в классах расширений STL методы, сложность ко) торых существенно отличается от принятой в аналогичных случаях в STL. Короче – не да) вайте невыполнимых обещаний.
Следовательно, пользователю, которому нужно выполнить несколько прохо дов или заранее знать предстоящий объем работы, вероятно, придется один раз обойти каталог и сохранить результаты в какомнибудь контейнере, например, std::vector<std::string>. Наверное, вам не нравится в этом подходе то, что приходится дублировать данные и многократно перераспределять память при до бавлении элементов (это будет делать класс std::back_inserter или эквивален тный ему). Но примите во внимание, что при каждом обходе каталога, содержаще го N элементов, требуется выполнить по меньшей мере 2 + N системных вызовов, тогда как выделение памяти для хранения тех же элементов может обходиться вообще без системных вызовов (в зависимости от оптимизаций, реализованных в конкретной стандартной библиотеке). Трудно представить себе операционную систему, в которой первое решение работало бы быстрее. (Тесты на моих машинах с системами Mac OS X и Windows XP, состоявшие в многократном обходе одного каталога с 512 файлами, показывают что копирование выполняется от двух до трех раз быстрее, чем повторный физический обход. Тестовая программа имеется на компактдиске.) Однако метод empty() реализован, потому что для него достаточно только «начать» просмотр (одно обращение к opendir() и одно к readdir()), а это в общемто операция с постоянным временем выполнения, хотя и сопряженная с нетривиальными накладными расходами. Методов доступа к элементам нет, так как обертываемый API однопроходный.
202
Наборы
19.3.4. Методы доступа к атрибутам Поскольку, как уже было сказано, мы ввели запрет на семантику копирова ния, было решено предоставить методы get_directory() и get_flags() на слу чай, если пользователь захочет повторить просмотр (но результаты при этом, ко нечно, могут отличаться). Метод get_directory() дает неизменяющий доступ к внутреннему члену m_directory, в котором хранится имя каталога, гарантированно завершающееся раз делителем компонентов пути ('/') (результат работы validate_directory_()). Метод get_flags() возвращает набор флагов после проверки. Например, если конструктору был передан флаг includeDots, то get_flags() вернет includeDots | directories | files. Хотя каждый из этих методов может возвращать не совсем то значение, кото рое было передано конструктору, использование их для конструирования нового объекта даст в точности такие же результаты при условии, что состояние файло вой системы не изменилось.
19.3.5. const_iterator, версия 1 Перейдем теперь к определению типа const_iterator; это класс, вложенный в readdir_sequence. Первая попытка показана в листинге 19.6. В этом определе нии коечто отсутствует, а коечто неправильно, но возьмем его за основу для по строения хорошей реализации. Листинг 19.6. Первоначальная версия readdir_sequence::const_iterator class readdir_sequence::const_iterator { public: // Òèïû-÷ëåíû typedef char const* value_type; typedef const_iterator class_type; private: // Êîíñòðóèðîâàíèå friend class readdir_sequence; // Äàäèì ïîñëåäîâàòåëüíîñòè äîñòóï // ê êîíñòðóêòîðó ïðåîáðàçîâàíèÿ const_iterator(DIR* dir, string_type const& directory, int flags); public: const_iterator(); ~const_iterator() throw(); public: // Ìåòîäû èòåðèðîâàíèÿ class_type& operator ++(); class_type operator ++(int); char const* operator *() const; bool equal(class_type const& rhs) const; private: // Ïåðåìåííûå-÷ëåíû DIR* m_dir; struct dirent* m_entry; int m_flags; }; bool operator ==( readdir_sequence::const_iterator const& lhs , readdir_sequence::const_iterator const& rhs);
Адаптация API opendir/readdir
203
bool operator !=( readdir_sequence::const_iterator const& lhs , readdir_sequence::const_iterator const& rhs);
С помощью закрытого конструктора преобразования итератор становится владельцем DIR*, поскольку мы не хотим, чтобы обертываемый API просачивался за пределы фасада. Поэтому класс readdir_sequence объявлен другом данного класса, это дает возможность вызывать его конструктор из метода begin(), как показано в листинге 19.7. Листинг 19.7. Методы итерирования readdir_sequence::const_iterator readdir_sequence::begin() const { DIR* dir = ::opendir(m_directory.c_str()); if(NULL == dir) { throw readdir_sequence_exception("Íå ìîãó îòêðûòü êàòàëîã äëÿ ïðîñìîòðà", errno); } return const_iterator(dir, m_directory, m_flags); } readdir_sequence::const_iterator readdir_sequence::end() const { return const_iterator(); }
Ниже показана реализация основных функций класса const_iterator. Кон структор инициализирует члены, а затем вызывает operator ++() для перехода к первому элементу (или end(), если результат пуст): readdir_sequence::const_iterator::const_iterator(DIR* dir , string_type const& directory, int flags) : m_directory(directory) , m_dir(dir) , m_entry(NULL) , m_flags(flags) { operator ++(); }
Деструктор освобождает память, занятую DIR*, если она уже не была осво бождена при вызове operator ++(): readdir_sequence::const_iterator::~const_iterator() throw() { if(NULL != m_dir) { ::closedir(m_dir); } }
operator *() просто возвращает указатель на имя элемента, проверив пред варительно предусловие: char const* readdir_sequence::const_iterator::operator *() const {
204
Наборы
UNIXSTL_MESSAGE_ASSERT("Ðàçûìåíîâàíèå íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_dir); return m_entry->d_name; }
Метод equal(), используемый для поддержки сравнения на равенство и нера венство, реализован с помощью m_entry. bool readdir_sequence::const_iterator::equal(const_iterator const& rhs) const { UNIXSTL_ASSERT(NULL == m_dir || NULL == rhs.m_dir || m_dir == rhs.m_dir); return m_entry == rhs.m_entry; } bool operator ==( readdir_sequence::const_iterator const& lhs , readdir_sequence::const_iterator const& rhs); bool operator !=( readdir_sequence::const_iterator const& lhs , readdir_sequence::const_iterator const& rhs);
К сожалению, тут имеется тонкая ошибка. Я уже говорил, что функция readdir() может при каждом вызове возвращать указатель на одну и ту же струк туру struct dirent, заполненную разными данными, или на разные структуры. В первом случае реализация equal() некорректна. Мы не станем сейчас исправ
лять эту ошибку, так как она будет автоматически исправлена вместе с устранени ем гораздо более серьезной ошибки, которую мы обсудим в следующем разделе. Остались только операторы пред и постинкремента. Оператор постинкре мента согласующийся с канонической формой, показан в листинге 19.8. Листинг 19.8. Каноническая форма оператора постинкремента const_iterator readdir_sequence::const_iterator::operator ++(int) { class_type r(*this); operator ++(); return r; }
Больше я не стану показывать полную реализацию оператора постинкремен та, а просто сошлюсь на каноническую форму, предполагая, что вы понимаете, о чем идет речь. (Конечно, то же самое относится и к операторам постдекремента для итераторов, которые их поддерживают.) Совет. Реализуйте операторы постинкремента и постдекремента в канонической форме через операторы прединкремента и предекремента соответственно.
Реализация оператора прединкремента довольно громоздкая: Листинг 19.9. Первоначальная версия оператора прединкремента const_iterator& readdir_sequence::const_iterator::operator ++() {
Адаптация API opendir/readdir
205
UNIXSTL_MESSAGE_ASSERT("Èíêðåìåíò íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_dir); for(;;) { errno = 0; m_entry = ::readdir(m_dir); if(NULL == m_entry) { if(0 != errno) { throw readdir_sequence_exception("Îøèáêà ïðè îáõîäå", errno); } } else { if(0 == (m_flags & includeDots)) { if(traits_type::is_dots(m_entry->d_name)) { continue; // '.' è '..' íå íóæíû; ïðîïóñêàåì } } if((m_flags & (directories | files)) != (directories | files)) { traits_type::stat_data_type st; string_type scratch(m_directory); scratch += m_entry->d_name; if(!traits_type::stat(scratch.c_str(), &st)) { continue; // Îøèáêà stat. Ïðåäïîëàãàåì, ÷òî ýëåìåíòà íåò, // è ïðîïóñêàåì } else { if(m_flags & directories) // Íàñ èíòåðåñóþò êàòàëîãè { if(traits_type::is_directory(&st)) { break; // Ýòî êàòàëîã, îñòàâëÿåì } } if(m_flags & files) // Íàñ èíòåðåñóþò ôàéëû { if(traits_type::is_file(&st)) { break; // Ýòî ôàéë, îñòàâëÿåì } } continue; // Íå ñîîòâåòñòâóåò, ïðîïóñêàåì } } } break; // Âûõîäèì èç öèêëà, ÷òîáû âåðíóòü íàéäåííûé ýëåìåíò } if(NULL == m_entry) // Ïðîâåðÿåì, çàêîí÷èëñÿ ëè îáõîä {
Наборы
206 ::closedir(m_dir); m_dir = NULL; } return *this; }
Хотя, как я уже говорил, в этой реализации есть фундаментальные проблемы, в некоторых отношениях она вполне разумна: имеется единственное обращение к readdir(); правильно обрабатывается возврат NULL из readdir() (путем обнуления и последующей проверки errno); проверка на равенство реализована в терминах m_dir; каталоги '.' и '..' отфильтровываются путем обращения к filesystem_ traits::is_dots(). При этом перед вызовом stat() проверяется имя элемента, что более эффективно; функции stat() передается полный путь, как и положено; отфильтровываются файлы или каталоги. При этом используются функции filesystem_traits::is_directory() и filesystem_traits::is_file() вместо менее прозрачных и чреватых ошибками проверок вида if(S_IFREG == (st.st_mode & S_IFREG)); конструктор обращается к operator ++(), чтобы первый раз вызвать readdir(), это согласуется с последующими обращениями к readdir(); если больше элементов не осталось, operator ++() закрывает описатель просмотра и присваивает ему значение NULL, давая знать equal(), что про смотр завершен. Деструктор сравнивает m_dir с NULL, чтобы закрыть ите раторы, еще не достигшие end(). Совет. В тех случаях, когда итератор нуждается в начальном позиционировании и для этого вызывается та же функция API, что и для последующих сдвигов, старайтесь органи) зовывать реализацию так, чтобы конструктор итератора вызывал operator ++() (или об) щую для того и другого функцию), и избегайте особых случаев (и лишних проверок).
Отметим, что эта версия поддерживает флаги includeDots, files и directories. Поддержку флагов fullPath и absolutePath мы реализуем, когда устраним все имеющиеся в ней проблемы.
19.3.6. Использование версии 1 Теперь проверим этот код в деле. Следующая программа нормально компили руется и исполняется: typedef unixstl::readdir_sequence seq_t; seq_t rds(".", seq_t::files); for(seq_t::const_iterator b = rds.begin(); b != rds.end(); ++b) { std::cout << *b << std::endl; }
Адаптация API opendir/readdir
207
Однако, если воспользоваться стандартным алгоритмом std::copy(), то после распечатки всех элементов в текущем каталоге программа аварийно за вершается: readdir_sequence rds(".", readdir_sequence::files); std::copy(rds.begin(), rds.end() , std::ostream_iterator(std::cout, "\n"));
Причина такого поведения лежит очень глубоко. Параметрыитераторы пере даются стандартным алгоритмам по значению, то есть копируются. Совет. Никогда не забывайте, что стандартные алгоритмы принимают параметры)итера) торы по значению (исключение составляет алгоритм std::advance()).
В определении const_iterator мы допустили серьезнейшую ошибку: класс напрямую управляет своими ресурсами, но не определяет семантику копирова ния. Конструктор копирования и копирующий оператор присваивания, опреде ленные за нас компилятором, просто копируют значения указателей m_dir и m_entry, поэтому, когда второй «владелец» DIR* вызовет closedir(), он «обло мается». Можно попытаться исправить ситуацию, просто запретив генерировать конструктор копирования и копирующий оператор присваивания: Листинг 19.10. Запрет операций копирования в классе const_iterator private: // Íå ïîäëåæèò ðåàëèçàöèè (ïîêà) const_iterator(class_type const& rhs); class_type& operator =(class_type const& rhs); };
Увы, от такого лекарства только умрешь скорее, поскольку потерян способ получить доступ к итератору, возвращенному методом begin(). И чуть ли не единственное, что мы еще можем сделать, – это вызвать конструктор readdir_sequence!
19.3.7. const_iterator, версия 2: семантика копирования Проблема первой реализации заключалась в том, что каждый экземпляр кон структора жадно цепляется за состояние итерирования и ни с кем не хочет им по делиться. Очевидное следствие такого поведения – эпизодические крахи про граммы, но есть и более тонкая ошибка, связанная с некорректным сравнением на равенство. Поэтому мы должны поступить, как мудрый родитель, и научить всех играть, не ссорясь. Решение показано в листинге 19.11. Во вложенном классе const_iterator объявлен еще один вложенный класс shared_handle. По поводу этого класса может возникнуть пара вопросов. Почему мы назвали методы AddRef() и Release(), а не add_ref() и release(), как обычно? Почему они воз
208
Наборы
вращают не void? Ответ на первый вопрос таков: мы следуем соглашению (заим ствованному из COM) о том, что такие типы доступны непосредственно, без даль нейшей адаптации – прокладок блокировки из STLSoft или шаблонного класса ref_ptr. А возврат значения позволяет мне наблюдать за счетчиком ссылок в от ладчике, исследуя, что находится в регистре EAX процессора Intel. Листинг 19.11. Определение вложенного класса shared_handle struct readdir_sequence::const_iterator::shared_handle { public: // Òèïû-÷ëåíû typedef shared_handle class_type; public: // Ïåðåìåííûå-÷ëåíû DIR* m_dir; sint32_t m_refCount; public: // Êîíñòðóèðîâàíèå è óïðàâëåíèå âðåìåíåì æèçíè explicit shared_handle(DIR* d) : m_dir(d) , m_refCount(1) {} sint32_t AddRef() { return ++m_refCount; } sint32_t Release() { sint32_t rc = —m_refCount; if(0 == rc) { delete this; } return rc; } private: ~shared_handle() throw() { UNIXSTL_MESSAGE_ASSERT("Óíè÷òîæåí êîíòåêñò, íà êîòîðûé åùå åñòü ññûëêè!" , 0 == m_refCount); if(NULL != m_dir) { ::closedir(m_dir); } } private: // Íå ïîäëåæèò ðåàëèçàöèè shared_handle(class_type const&); class_type& operator =(class_type const&); };
Этот класс играет роль описателя с подсчетом ссылок для ресурса DIR* и отве чает за организацию совместного владения и за освобождение ресурса (вызов closedir()), когда на него не останется ссылок.
Адаптация API opendir/readdir
209
Правило. При реализации итераторов ввода над поэлементным API, который не предос) тавляет общего доступа к состоянию перебора, вы должны сами создать разделяемый контекст для обеспечения правильной семантики копирования и сравнения экземпляров итераторов.
Естественно, такие классыописатели можно абстрагировать и сделать обоб щенными. Я не сделал этого сейчас по четырем причинам: уменьшение связаннос ти, переносимость, понятность и стабильность реализации. Написать шаблонный класс описателя – нетривиальное занятие с точки зрения как удобопонятности, так и переносимости между различными компиляторами; он получится сложнее, чем scoped_handle (раздел 16.5). Кроме того, он вводит связанность на этапе компиляции, хотя в данном случае это не так уж страшно. Наконец, нет доста точной мотивации, поскольку не похоже, что этот класс с подсчетом ссылок когда либо изменится (и он таки ни разу не изменялся за все время существования класса readdir_sequence). Определение const_iterator с использованием раз деляемого описателя приведено в листинге 19.12. Листинг 19.12. Определение класса readdir_sequence::const_iterator class readdir_sequence::const_iterator : public std::iterator<. . .> // Åùå ïðåäñòîèò ðåøèòü . . . { public: // Òèïû-÷ëåíû typedef char const* value_type; typedef const_iterator class_type; private: // Êîíñòðóèðîâàíèå friend class readdir_sequence; // Äàäèì ïîñëåäîâàòåëüíîñòè äîñòóï // ê êîíñòðóêòîðó ïðåîáðàçîâàíèÿ const_iterator(DIR* dir, string_type const& directory, int flags); public: const_iterator(); const_iterator(class_type const& rhs); ~const_iterator() throw(); class_type& operator =(class_type const& rhs); public: // Èòåðàöèÿ class_type& operator ++(); class_type operator ++(int); char const* operator *() const; bool equal(class_type const& rhs) const; private: // Ïåðåìåííûå-÷ëåíû struct shared_handle; shared_handle* m_handle; struct dirent* m_entry; int m_flags; string_type m_scratch; size_type m_dirLen; };
210
Наборы
19.3.8. operator ++() Изменения, необходимые для полного и правильного определения оператора прединкремента, показаны в листинге 19.13. Листинг 19.13. Реализация оператора прединкремента: использование разделяемого описателя const_iterator& readdir_sequence::const_iterator::operator ++() { UNIXSTL_MESSAGE_ASSERT("Èíêðåìåíò íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_handle); for(;;) { . . . } if(NULL == m_entry) // Ïðîâåðÿåì, çàâåðøèëñÿ ëè îáõîä { UNIXSTL_ASSERT(NULL != m_handle); m_handle->Release(); m_handle = NULL; } return *this; }
Вместо того чтобы вызывать closedir(), когда все элементы будут просмот рены, итератор освобождает свой разделяемый описатель. Необходимо внести еще одно изменение в этот оператор, но оно имеет отношение к обработке полных путей и будет рассмотрено в разделе 19.3.11.
19.3.9. Категория итератора и адаптируемые типы" члены Как я уже указывал в начале этой главы, readdir_sequence::const_iterator обладает однопроходной неизменяющей семантикой (то есть семантикой итера тора ввода) и поведением, характерным для недолговечных ссылок на элементы. Внешнему миру мы сообщаем об этом с помощью специализации std::iterator, которой этот класс наследует. Принимая во внимание категорию итератора и ссы лок на элементы, можно было бы ожидать такой специализации: Листинг 19.14. Возможное определение типов итератора class readdir_sequence::const_iterator : public std::iterator< std::input_iterator_tag , readdir_sequence::value_type , ptrdiff_t , readdir_sequence::value_type* , readdir_sequence::value_type& > . . .
Адаптация API opendir/readdir
211
Все вроде нормально, но в данном случае приводит к небольшой аномалии. Согласно этому определению, тип reference для итератора – char const*&. Но тогда компилятор выдаст ошибку в операторе разыменования. Давайте распи шем этот метод подробно, развернув typedef’ы: char const*& readdir_sequence::const_iterator::operator *() { return m_dir->d_name; // Îøèáêà: ññûëêó ìîæíî ñâÿçûâàòü òîëüêî ñ lvalue }
Компилятор говорит, что ссылку можно связывать только с lvalue, а m_dir->d_name – это rvalue. Чтобы поддержать такой тип возврата, в типе ите ратора следует хранить членуказатель типа char const* const*, скажем m_pref, и использовать его следующим образом: char const*& readdir_sequence::const_iterator::operator *() { m_pref = &m_dir->d_name[0]; return m_pref; }
Неудивительно, что я неохотно делаю такие вещи. С точки зрения эффектив ности char const* и char const*& эквивалентны, и возврат значения последнего типа лишь дает клиентскому коду возможность изменять то, на что указывает m_pref. В результате класс std::iterator специализирован для readdir_ sequence::const_iterator так, как будто он поддерживает только временную по значению ссылку на элемент (раздел 3.3.5): Листинг 19.15. Определение категории ссылок на элементы для итератора class readdir_sequence::const_iterator : public std::iterator< std::input_iterator_tag , readdir_sequence::value_type, ptrdiff_t , void, readdir_sequence::value_type > . . .
19.3.10. operator ">() Реализация метода operator ->() для класса readdir_sequence::const_ iterator не представляет проблемы, поскольку значение в данном случае имеет тип char const*, то есть, очевидно, не тип класса. Если бы по стандарту POSIX в структу ре struct dirent было несколько полей, а не только d_name, то имело бы смысл опре делить тип значения как struct dirent, и тогда operator ->() должен был бы воз вращать struct dirent const*. Но раз это не так, то и не будем мучиться.
19.3.11. Поддержка флагов fullPath и absolutePath Теперь мы готовы завершить рассмотрение класса readdir_sequence и предъявить окончательную реализацию const_iterator, в которой учтены фла ги fullPath и absolutePath.
212
Наборы
Еще одна проблема API opendir/readdir состоит в том, что он возвращает лишь имена элементов. Чтобы получить полный путь, необходимо конкатенировать имя с путем к просматриваемому каталогу, как показано в листинге 19.9. Если задан флаг fullPath, то возвращается результат конкатенации пути к каталогу – он уже содержит завершающую косую черту, не забывайте об этом – с именем. Чтобы реализовать это, следует изменить логику метода operator ++(), так чтобы он конкатенировал имя с путем к каталогу перед обращением к stat(), ис пользуя для этого члены m_scratch и m_dirLen: Листинг 19.16. Реализация оператор прединкремента: конкатенация с путем const_iterator& readdir_sequence::const_iterator::operator ++() { . . . if((m_flags & (fullPath | directories | files)) != (directories | files)) { // Îáðåçàòü áóôåð scratch ïî äëèíå ïóòè ê êàòàëîãó ... m_scratch.resize(m_dirLen); // ... è äîáàâèòü èìÿ ôàéëà m_scratch += m_entry->d_name; } if((m_flags & (directories | files)) != (directories | files)) { // Ïðîâåðèòü ñ ïîìîùüþ stat òèï ýëåìåíòà traits_type::stat_data_type st; if(!traits_type::stat(m_scratch.c_str(), &st)) { . . . }
Если нужно отфильтровать каталоги либо файлы или задан флаг fullPath, полный путь строится путем обрезания строки m_scratch (которая создается в конструкторе const_iterator копированием каталога, переданного конструк тору readdir_sequence) до длины, хранящейся в константном члене m_dirLen (который инициализируется длиной каталога, переданного конструктору readdir_sequence). Таким образом, строка m_scratch используется повторно, и количество конструирований типа string_type на один экземпляр const_ iterator сводится к 1 (вместо одного на каждый элемент просматриваемого ка талога). Кроме того, поскольку размер строки уменьшается только для каталогов, вполне вероятно, что количество перераспределений памяти в ходе просмотра бу дет невелико, а, может быть, они и вообще не понадобятся. Но есть и еще одна оптимизация. Если определен символ PATH_MAX (см. раз дел 16.4), то string_type – на самом деле специализация stlsoft::basic_ static_string, как следует из листинга 19.17. Это шаблонный класс, подобный basic_string, в котором имеется внутренний массив символов фиксированной длины, в данном случае PATH_MAX + 1, и, стало быть, память из кучи вообще не выделяется.
Адаптация API opendir/readdir
213
Листинг 19.17. Определения типовZчленов в случае, когда определен символ PATH_MAX private: // Òèïû-÷ëåíû typedef char char_type; public: typedef char_type const* value_type; #if defined(PATH_MAX) typedef stlsoft::basic_static_string< char_type , PATH_MAX > string_type; #else /* ? PATH_MAX */ typedef std::basic_string string_type; #endif /* !PATH_MAX */ typedef filesystem_traits traits_type; . . .
Поскольку гарантируется, что m_scratch содержит полный путь, если задан флаг fullPath, то operator *() может вернуть соответствующее значение: Листинг 19.18. Реализация оператора разыменования char const* readdir_sequence::const_iterator::operator *() const { UNIXSTL_MESSAGE_ASSERT( "Ðàçûìåíîâàíèå íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_entry); if(readdir_sequence::fullPath & m_flags) { return m_scratch.c_str(); } else { return m_entry->d_name; } }
Если просматриваемый каталог задан относительным путем, то пути, полу ченные при обходе с поднятым флагом fullPath, тоже будут относительными. Поэтому последним штрихом в реализации readdir_sequence станет поддержка флага absolutePath, который гарантирует – с помощью метода prepare_ directory_(), – что конструктору передается абсолютный путь (листинг 19.19). NULL или пустая строка интерпретируются как текущий рабочий каталог и, как мы видели в реализации glob_sequence::init_glob_() (раздел 17.3.8), для хра нения значения используется неизменяемая локальная статическая строка сим волов. Листинг 19.19. Реализация закрытого метода prepare_directory_() string_type readdir_sequence::prepare_directory_(char_type const* , int {
directory flags)
214
Наборы
if( NULL == directory || '\0' == *directory) { static const char_type s_thisDir[] = "."; directory = s_thisDir; } basic_file_path_buffer path; size_type n; if(absolutePath & flags) { n = traits_type::get_full_path_name(directory, path.size() , &path[0]); if(0 == n) { throw readdir_sequence_exception("Íå ìîãó ïîëó÷èòü ïóòü", errno); } } else { n = traits_type::str_len(traits_type::str_n_copy( &path[0] , directory, path.size())); } traits_type::ensure_dir_end(&path[n - 1]); directory = path.c_str(); return directory; }
Ну вот и все.
19.4. Альтернативные реализации В отличие от glob_sequence, не вполне ясно, почему расширение STL для API opendir/ readdir должно принимать форму набора, а не итератора. В общем то никакой неоспоримой причины и нет. Я выбрал этот путь, исходя из собствен ных привычек, стиля и желания быть последовательным. Поскольку я как прави ло стремлюсь все расширения представлять в виде наборов, а не итераторов, это вошло в привычку и стало моим стилем. И, чтобы быть последовательным, я пред почитаю именно такой способ, оставляя автономные итераторные классы для адаптеров итераторов. Но это мои личные предпочтения. Вы вольны пойти и другим путем. Однако имейте в виду, что у каждого подхода есть свои плюсы и минусы.
19.4.1. Хранение элементов в виде мгновенного снимка За. Если одно и то же результирующее множество нужно обработать несколь ко раз, то за перебор вы платите только единожды. Против. Мгновенные снимки устаревают, поэтому для получения актуально го результата нужно создавать новый экземпляр. Конечно, это небольшая пробле ма, но в случае, когда нужно получить актуальное содержимое каталога дважды,
Адаптация API opendir/readdir
215
а каждый результат обработать только один раз, вы платите за то, что не заказыва ли. В тех случаях, когда желательно многократно обрабатывать каждое результи рующее множество, можно просто скопировать его в контейнер, как было показа но в самом начале этой главы (листинг 19.2)
19.4.2. Хранение элементов в виде итератора В следующем фрагменте показано, как можно было бы реализовать функцию getWorkplaceSubdirectories() в терминах такого итераторного класса: std::vector<std::string> getWorkplaceSubdirectories() { using unixstl::readdir_iterator; return std::vector<std::string>( readdir_iterator(getWorkplaceDirectory() , readdir_iterator::directories | readdir_iterator::fullPath) , readdir_iterator()); }
За. В некоторых случаях такой подход более лаконичен. Показанная выше реализация getWorkplaceSubdirectories() состоит всего из одного предложе ния, хотя изза пространственных ограничений печатной страницы и моего навяз чивого стремления к выравниванию кода получилось две лишних строки. Впро чем, я не уверен, что это лучше версии на основе readdir_sequence; на мой взгляд, этот код не так прозрачен, хотя, возможно, я пристрастен. За. Такая форма «честнее» в том смысле, что каждый просмотр – это новый обход файловой системы, который, возможно, даст другие результаты. Против. Итератор моделируется как указатель, поэтому любая операция, вы зываемая для экземпляров итераторных классов, например readdir_iterator:: get_directory(), выглядит неестественно. А квалифицировать константычле ны классом итератора и вовсе вычурно. Кроме того, как мы увидим во втором томе, адаптация наборов – это простая и полезная техника, что делает аргумента цию, основанную на лаконичности автономных итераторов, по меньшей мере спорной. Против. Этот подход не применим к адаптерам наборов.
19.5. Резюме Хотя семантика API opendir/readdir изначально проста, его поэлементная природа означает, что можно поддержать лишь итераторы ввода и, следовательно, не обойтись без итераторного класса. Я продемонстрировал дизайн набора, бази рующегося на классе самого набора и классе неизменяющего итератора. Мы рас смотрели отношения между ними и отметили следующие важные особенности: для обеспечения однопроходной семантики (без краха!) итераторный класс должен поддерживать общее состояние; отсутствие метода size(), который не может быть реализован с постоян ным временем выполнения, говорит пользователю о производительности
216
Наборы
данного набора, а именно, что обход файловой системы не может удовлет ворить тем показателям вычислительной сложности, которых принято ожидать от STLнаборов; применение вспомогательного компонента filesystem_traits абстраги рует различные проверки, упрощает реализацию и в какойто мере гаран тирует совместимость с будущими версиями, если мы захотим преобразо вать класс в шаблон для поддержки двух разных API обхода файловой системы: для типов char и wchar_t; расширение функциональности, например добавление фильтрации ката логов или файлов и исключения каталогов '.' и '..', упрощает клиентс кий код и повышает общую надежность и, потенциально, производитель ность; использование закрытых статических методов для предварительной обра ботки аргументов конструктора позволяет сделать члены класса констант ными, что повышает надежность и несет информацию о решениях, приня тых на этапе проектирования, тем, кто будет сопровождать компонент в дальнейшем; использование для сравнения метода equal() (глава 15) позволяет реали зовать операторы сравнения, не являющиеся друзьями класса, что повы шает надежность и прозрачность. Возможно, вам показалось, что реализация чрезмерно усложнена некоторыми мерами, направленными на повышение эффективности. Но это не нарушает прин% ципа оптимизации, так как дизайн не пострадал. Согласен, в какойто мере это вступает в противоречие с принципом ясности, но библиотека общего назначения не должна забывать о производительности. Тесты, описанные в начале главы, по казывают, что в случае класса readdir_sequence этого и не произошло, поэтому я считаю, что овчинка стоила выделки.
Глава 20. Адаптация API FindFirstFile/FindNextFile При проектировании всегда следует учиты% вать ограничения, рассматривать различ% ные варианты и, следовательно, компромис% сы неизбежны. – Генри Петроски Написание открытых библиотек – лучший способ научиться писать хороший код. Ска% жу так: пиши, как должно, или публично признай свой позор. – Ади Шавит
20.1. Введение В этой главе мы будем основываться на опыте, приобретенном в ходе написа ния классов glob_sequence (глава 17) и readdir_sequence (глава 19). Нашей целью станет изучение средств обхода файловой системы в Windows, а точнее API FindFirstFile/FindNextFile. Хотя этот API аналогичен opendir/readdir в том смыс ле, что является однопроходным и возвращает по одному элементу, он в то же время обладает некоторыми чертами, роднящими его с API glob, а также рядом уникаль ных особенностей. Мы рассмотрим, какое влияние все это оказывает на проектиро вание STLнабора и шаблонного класса winstl::basic_findfile_sequence.
20.1.1. Мотивация Как уже повелось, сначала рассмотрим длинную версию (листинг 20.1). После первых двух глав у меня закончились интересные гипотетические сценарии, по этому я просмотрел все написанные мной исходные тексты и в очень старой ути лите обнаружил следующий код. Я подчеркиваю слова «очень старая», так как код не слишком красив и далек от моего нынешнего стиля. (Все ляпы – целиком вина тогдашнего меня, а я несу ответственность лишь за себя сегодняшнего, как имеют обыкновение говорить политики.) Единственное, что я добавил, – это три обращения к функциям протоколирования из библиотеки Pantheios; раньше вме сто них были Windowsсообщения, посылаемые объемлющему процессу. Ну и еще убрал несколько комментариев (которые вообще были неправильны!).
218
Наборы
Листинг 20.1. Реализация примера использования API FindFirstFile/ FindNextFile 1 void ClearDirectory(LPCTSTR lpszDir, LPCTSTR lpszFilePatterns) 2 { 3 TCHAR szPath[1 + _MAX_PATH]; 4 TCHAR szFind[1 + _MAX_PATH]; 5 WIN32_FIND_DATA find; 6 HANDLE hFind; 7 size_t cchDir; 8 LPTSTR tokenBuff; 9 LPTSTR tok; 10 11 pantheios::log_DEBUG(_T("ClearDirectory("), lpszDir, _T(", ") 12 , lpszFilePatterns, _T(")")); 13 ::lstrcpy(szFind, lpszDir); 14 if(szFind[::lstrlen(lpszDir) - 1] != _T('\\')) 15 { 16 ::lstrcat(szFind, _T("\\")); 17 } 18 cchDir = ::lstrlen(szFind); 19 tokenBuff = ::_tcsdup(lpszFilePatterns); // strdup() èëè wcsdup() 20 if(NULL == tokenBuff) 21 { 22 pantheios::log_ERROR(_T("Îøèáêà ïðè âûäåëåíèè ïàìÿòè")); 23 return; 24 } 25 else 26 { 27 for(tok = ::_tcstok(tokenBuff, ";"); NULL != tok; 28 tok = ::_tcstok(NULL, ";")) 29 { 30 ::lstrcpy(&szFind[cchDir], tok); 31 hFind = ::FindFirstFile(szFind, &find); 32 if(INVALID_HANDLE_VALUE != hFind) 33 { 34 do 35 { 36 if(find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 37 { 38 continue; 39 } 40 ::lstrcpy(szPath, lpszDir); 41 ::lstrcat(szPath, _T("\\")); 42 ::lstrcat(szPath, find.cFileName); 43 if(::DeleteFile(szPath)) 44 { 45 pantheios::log_NOTICE( _T("Óñïåøíî óäàëåí ") 46 , szPath); 47 ::SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, szPath, 0); 48 } 49 else 50 { 51 pantheios::log_ERROR(_T("Íå ìîãó óäàëèòü "), szPath 52 , _T(": "), winstl::error_desc(::GetLastError()));
Адаптация API FindFirstFile/FindNextFile 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 }
219
} } while(::FindNextFile(hFind, &find)); ::FindClose(hFind); } } ::free(tokenBuff); } ::lstrcpy(szFind, lpszDir); ::lstrcat(szFind, _T("\\*.*")); hFind = ::FindFirstFile(szFind, &find); if(INVALID_HANDLE_VALUE != hFind) { do { if( (find.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && ::lstrcmp(find.cFileName, _T(".")) && ::lstrcmp(find.cFileName, _T(".."))) { ::lstrcpy(szPath, lpszDir); ::lstrcat(szPath, _T("\\")); ::lstrcat(szPath, find.cFileName); ClearDirectory(szPath, lpszFilePatterns); // Ðåêóðñèÿ } } while(::FindNextFile(hFind, &find)); ::FindClose(hFind); }
Этой функции при вызове передаются имя каталога и образец, а она удаляет все соответствующие образцу файлы из указанного каталога. Код получился длинным и по большей части состоит из утомительного манипулирования строка ми и явного управления ресурсами. А теперь сравним с STLверсией, в которой используется шаблонный класс winstl::basic_findfile_sequence, показан ный в листинге 20.2. Листинг 20.2. Реализация того же примера с использованием класса winstl::basic_findfile_sequence 1 void ClearDirectory(LPCTSTR lpszDir, LPCTSTR lpszFilePatterns) 2 { 3 typedef winstl::basic_findfile_sequence ffs_t; 4 5 pantheios::log_DEBUG(_T("ClearDirectory("), lpszDir, _T(", ") 6 , lpszFilePatterns, _T(")")); 7 fs_t files(lpszDir, lpszFilePatterns, ';', ffs_t::files); 8 { for(ffs_t::const_iterator b = files.begin(); b != files.end(); 9 ++b) 10 { 11 if(::DeleteFile((*b).c_str())) 12 { 13 pantheios::log_NOTICE(_T("Óñïåøíî óäàëåí ôàéë ")
Наборы
220 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 }
, *b); ::SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, (*b).c_str(), 0); } else { pantheios::log_ERROR(_T("Íå ìîãó óäàëèòü ôàéë "), *b , _T(": "), winstl::error_desc(::GetLastError())); } }} ffs_t dirs(lpszDir, _T("*.*"), ffs_t::directories | ffs_t::skipReparseDirs); { for(ffs_t::const_iterator b = dirs.begin(); b != dirs.end(); ++b) { ClearDirectory((*b).c_str(), lpszFilePatterns); }}
Обратите внимание, что в предложениях протоколирования в строках 13–14 и 19–20 не нужно вызывать для аргументов метод c_str(), поскольку, как было сказано в разделе 9.3.1, библиотека Pantheios совместима со всеми типами, для которых определены прокладки строкового доступа c_str_data_a и c_str_len_a. Необходимые перегруженные варианты определены для типа findfile_ sequence::value_type (который в действительности является специализацией basic_findfile_sequence_value_type) и экспортированы в пространство имен stlsoft (как мы увидим ниже). (То же самое относится к шаблонному клас су winstl::basic_error_desc, так что временный экземпляр специализации winstl::error_desc в строке 20 тоже можно было бы передать функции прото колирования непосредственно.) Поскольку функции из Windows API не понимают прокладок строкового дос тупа, то в остальных частях ClearDirectory() приходится выполнять преобра зование явно. (Во втором томе мы увидим, как можно интегрировать с прокладка ми функции из стандартной библиотеки C, операционной системы и API сторонних производителей.)
20.1.2. API FindFirstFile/FindNextFile API FindFirstFile/FindNextFile состоит из двух структур и семи функций (листинг 20.3). Две функции – это оптимизации, имеющиеся только в операцион ных системах семейства NT; их мы рассмотрим ниже в этой главе. Основные пять функций: две пары FindFirstFileA/W() и FindNextFileA/W(), а также FindClose(). Входящая в состав API функция FindFirstFile() на самом деле просто директива #define, сводящаяся к FindFirstFileA() (char) или FindFirstFileW() (wchar_t) в зависимости от кодировки символов, то есть от наличия или отсутствия в программе символа препроцессора UNICODE). Анало гично функция FindNextFile() сводится к FindNextFileA() (char) или FindNextFileW() (wchar_t), а структура WIN32_FIND_DATA – к WIN32_FIND_DATAA
Адаптация API FindFirstFile/FindNextFile
221
(char) или WIN32_FIND_DATAW (wchar_t). В тех случаях, когда кодировка символов несущественна, я буду писать все имена без суффиксов A/W. Листинг 20.3. Типы и функции, составляющие API FindFirstFile/FindNextFile HANDLE HANDLE
FindFirstFileA(char const* searchSpec , WIN32_FIND_DATAA* findData); FindFirstFileW(wchar_t const* searchSpec , WIN32_FIND_DATAW* findData); FindNextFileA(HANDLE hSrch, WIN32_FIND_DATAA* findData); FindNextFileW(HANDLE hSrch, WIN32_FIND_DATAW* findData); FindClose(HANDLE hSrch); WIN32_FIND_DATAA
BOOL BOOL BOOL struct { DWORD dwFileAttributes; FILETIME ftCreationTime; FILETIME ftLastAccessTime; FILETIME ftLastWriteTime; DWORD nFileSizeHigh; DWORD nFileSizeLow; CHAR cFileName[MAX_PATH]; CHAR cAlternateFileName[14]; }; struct WIN32_FIND_DATAW { . . . // Òî æå, ÷òî WIN32_FIND_DATAA, çà èñêëþ÷åíèåì: WCHAR cFileName[MAX_PATH]; WCHAR cAlternateFileName[14]; };
Функция FindFirstFile() возвращает описатель поиска и заполняет предо ставленную вызывающей программой структуру WIN32_FIND_DATA, если образцу searchSpec соответствует хотя бы один файл или каталог. В противном случае она возвращает код INVALID_HANDLE_VALUE (-1). В случае удачного завершения вызывающая программа может, пользуясь полученным описателем, перебрать подходящие элементы, повторно вызывая функцию FindNextFile() до тех пор, пока она не вернет false. Когда поиск будет завершен или не останется представ ляющих интерес элементов, вызывающая программа должна вызвать функцию FindClose(), чтобы освободить ресурсы, связанные с описателем поиска. Отметим, что в Windows описатель поиска имеет непрозрачный тип HANDLE, экземпляры которого обычно закрываются функцией CloseHandle() (применяе мой для освобождения всех объектов ядра). API FindFirstFile/FindNextFile, как и glob, допускает наличие метасимволов в образце. Однако здесь имеются доволь но сильные ограничения. механизм сопоставления с образцом понимает только метасимволы ? и *; разрешается задавать не более одного образца, то есть такой образец недо пустим: "*.cpp;makefile.*"; метасимволы могут встречаться только в самом правом компоненте про сматриваемого пути searchSpec: образец "H:\publishing\books\ XSTLv1\pre*.doc" допустим, а "H:\publishing\books\*\preface.doc" – нет.
222
Наборы
Иными словами, этот API гораздо ближе к opendir/readdir в части порядка вызова функций. Отличие в том, что FindFirstFile() логически эквивалентна вызову opendir() с последующим первым вызовом readdir(). В обоих случаях для получения остальных элементов каталога вызывающая программа должна обращаться к функции чтения самостоятельно, а в конце явно прекратить про смотр. Таким образом, складывается впечатление, что STLнабор будет поддер живать итератор ввода и иметь много общего с readdir_sequence (раздел 19.3). Однако эти два API сильно различаются по способу возврата информации и по тому, какая именно информация возвращается. API FindFirstFile/FindNextFile по мещает всю имеющуюся информацию о найденном элементе в структуру, предос тавляемую пользователем, а не возвращает указатель на структуру, хранящуюся внутри самого API. Кроме того, структура WIN32_FIND_DATA содержит не только имя элемента (аналогом поля dirent::d_name служит cFileName), но также и значительную часть полей из структуры struct stat, заполняемой функцией stat(), которая была необходима в реализациях glob_sequence (раздел 17.3) и readdir_sequence. Это важный момент. Нам не нужно делать дополнительных вызовов, чтобы получить информацию об атрибутах, необходимую для фильтра ции; она уже и так присутствует. На самом деле, API FindFirstFile/FindNextFile – всего лишь часть опубликованного Windows API (доступного на всех платформах Windows) для получения полной информации о файле по его имени. Это различие и оказало основное влияние на проектирование шаблонного класса basic_findfile_sequence и вспомогательных классов. Отметим, что, в отличие от UNIX glob, в Windows нет API, который позволял бы задавать не сколько образцов поиска. Это тоже наложило отпечаток на дизайн basic_ findfile_ sequence.
20.2. Анализ примеров Прежде чем нырнуть, необходимо экипироваться. Поэтому исследуем обе представленные версии.
20.2.1. Длинная версия Изучая программу из листинга 20.1, мы обнаруживаем в ней следующие шаги. Строки 13–18. Вручную сформировать начало образца поиска, добавив в ко нец имени каталога разделитель компонентов, если это необходимо. Строки 19–28. Создать изменяемую копию параметра lpszFilePatterns и разбить эту строку на лексемы, пользуясь макросом _tcstok(), который сводится к strtok() или wcstok() для строк в многобайтовой или широкой кодировке со ответственно. (Описание этой и других функций разбиения строк и STLсовмес тимой последовательности, которая предлагает гораздо более удобный механизм разбиения, см. в главе 27.) Строка 30. Вручную сформировать полный образец поиска для каждого эле мента поданного на вход набора образцов. Для этого мы дописываем tok в конец пути к каталогу (после разделителя), который был сформирован в строках 13–18.
Адаптация API FindFirstFile/FindNextFile
223
Строки 31–32. Начать поиск и проверить код возврата. Отметим, что FindFirstFile() завершается с ошибкой, если нет подходящих элементов или
неверно задан образец либо имя каталога. Этим она отличается от функции opendir(), которая всегда возвращает ненулевой указатель DIR*, если каталог существует, даже в случае, когда он содержит только подкаталоги '.' и '..' или
(в некоторых системах) не содержит вообще ничего. Строки 36–39. Отфильтровать каталоги. Строки 40– 42. Вручную сформировать имя подлежащего удалению файла. Строки 43–53. Попытаться удалить файл, проверяя код возврата. Если файл успешно удален, вызвать функцию SHChangeNotify(), которая сообщит оболочке Windows об изменении, чтобы та могла модифицировать изображение на экране. Строка 55. Получить следующий элемент или выйти из цикла. Строка 56. Освободить описатель поиска. Строка 59. Освободить память, занятую временным буфером, который был нужен для работы _tcstok(). Строки 62–63. Вручную сформировать образец поиска, конкатенировав имя каталога, разделитель компонентов и специфичный для Windows образец «все»: "*.*". Строки 64–65. Начать поиск и проверить код возврата. Строки 69–71. Отфильтровать файлы и каталоги '.' и '..' (во избежание зацикливания). Строки 73–75. Вручную сформировать путь к подкаталогу, в который мы со бираемся рекурсивно спуститься. Строка 76. Рекурсивный спуск. Строка 79. Получить следующий элемент или выйти из цикла. Строка 80. Освободить описатель поиска. Уф! Сколько кода! Как и в случае readdir(), API возвращает только имена файлов и каталогов, поэтому мы тратим много сил на формирование корректных полных путей. Потом еще фильтрация – снова с учетом каталогов '.' и '..' – и освобождение ресурсов. Ну и, конечно, раз мы добавили какието классы, напи санные на C++, то возможны исключения, а об их обработке мы совершенно забы ли. В общем, нетривиальный код. А теперь перейдем к расширению STL.
20.2.2. Короткая версия В листинге 20.2 мы видим гораздо более лаконичный вариант, в котором ис пользуется компонент basic_findfile_sequence. Строка 3. Определить удобный (т.е. короткий) локальный typedef для специа лизации шаблона класса. Строка 7. Объявить объект files, передав конструктору каталог для про смотра, один или несколько образцов поиска, разделитель (';') и флаг фильтра ции ffs_t::files. Этот объект вернет все файлы из указанного каталога, отве чающие данному образцу.
224
Наборы
Строки 8–22. Эти строки функционально эквивалентны строкам 13–60 в лис тинге 20.1. Строки 24–25. Объявить объект dirs, передав конструктору каталог для про смотра, образец «все» и флаги фильтрации ffs_t::directories и ffs_t:: skipReparseDirs. Этот объект вернет только подкаталоги указанного каталога. (Чуть позже мы рассмотрим флаг skipReparseDirs.) Строки 26–30. Функционально эквивалентны строкам 62–81 из листин га 20.1. Помимо лаконичности, экономии усилий и большей понятности, этот вариант еще и безопасен относительно исключений, что особенно важно при манипуля циях объектами ядра. И я готов поспорить с любым приверженцем C, испыты вающим неприязнь к STL, который будет утверждать, что длинная версия про зрачнее. Обратите внимание, что в строке 3 я явно написал специализацию. На самом деле, в заголовочном файле <winstl/ filesystem/findfile_sequence.hpp> уже есть следующие три полные специализации этого шаблонного класса: typedef winstl::basic_findfile_sequence findfile_sequence_a; typedef winstl::basic_findfile_sequence<wchar_t> findfile_sequence_w; typedef winstl::basic_findfile_sequence findfile_sequence;
При написании программ для Windows, поддерживающих различные коди ровки, обычно полагаются на условную компиляцию, а не употребляют явно име на функций для кодировок ANSI и Unicode. В обеих версиях ClearDirectory() мы следуем рекомендованной практике, пользуясь макросом _T() при записи ли тералов и typedef’ами TCHAR, LPTSTR и LPCTSTR. Благодаря предложенным typedef’ам пользователь может просто писать findfile_sequence, забывая о том, что в действительности это специализация шаблона.
20.2.3. Точки монтирования и бесконечная рекурсия Надеюсь, у вас возник вопрос, зачем нужен флаг skipReparseDirs. (Если нет, срочно проснитесь!) Помимо всего прочего, в длинной версии (листинг 20.1) есть еще одна ошибка. В Windows 2000 и более поздних версиях ОС семейства NT под держивается идея точек монтирования (reparse point), позволяющих смонтиро вать диск на пустой каталог. Это бывает полезно, например, для увеличения места на существующем диске без реорганизации разделов или – мне это особенно нравится – для ограничения размера каталога, в который автоматически загружа ются файлы. Естественно, предполагалось, что на каталог будет монтироваться другой диск, например, диск T: монтируется на каталог H:\temp. Но можно смон тировать диск и на собственный подкаталог, например, H: на H:\infinite. При этом создается бесконечное дерево файловой системы. Сразу скажу, что подоб ный способ работы с файловой системой, конечно, неправилен, но уж раз такое возможно, следует проверять этот случай во время выполнения программы. По этому длинная версия некорректна. В короткой версии все смонтированные ката логи пропускаются, поэтому такой ошибки в ней нет.
Адаптация API FindFirstFile/FindNextFile
225
20.3. Проектирование последовательности Пришло время подумать о тех свойствах последовательности, которые дикту ет API FindFirstFile/FindNextFile. Прежде всего, мы хотим поддержать компиля цию как для многобайтовых, так и для широких строк, поэтому последователь ность будет представлять собой шаблон, winstl::basic_findfile_sequence. Зависящие от кодировки аспекты мы абстрагируем в характеристическом классе, как то делается в стандартной библиотеке для шаблонов basic_string, basic_ostream и т.д. В данном случае шаблон будет называться winstl:: filesystem_traits (раздел 16.3). Итак, шаблон последовательности выглядит следующим образом: template< typename C , typename T = filesystem_traits > class basic_findfile_sequence;
Я уже говорил, что API FindFirstFile/FindNextFile очень напоминает opendir/readdir в части формы и общей семантики: из файловой системы читает ся по одному элементу. Следовательно, можно ожидать, что последовательность будет поддерживать итератор ввода, реализованный в виде класса. Но изза оши бок в некоторых компиляторах, проявившихся на ранних этапах жизни этого ком понента, шаблон класса итератора нельзя реализовать в виде вложенного класса, поэтому он представлен отдельным классом, который я игриво назвал basic_ findfile_sequence_const_iterator. В нем используется класс разделяемого описателя, как и в случае readdir_sequence::const_iterator (разделы 19.3.5 и 19.3.7), который управляет описателем поиска, возвращаемым операционной сис темой. Так мы поддерживаем требования, предъявляемые итератором ввода. В более тонких деталях API различаются, и это нашло отражение в интерфей се обеих последовательностей. Наиболее очевидное отличие – тот факт, что структура WIN32_FIND_DATA содержит дополнительные атрибуты найденного элемента. Было бы безумием отбросить эту информацию. Учитывая еще, что в структуре содержится только имя файла, а не полный путь, мы приходим к вы воду о необходимости специального типа значения. Поэтому мы заведем еще один шаблонный класс, присвоив ему имя, выдающееся своей лаконичностью: basic_findfile_sequence_value_type. Поскольку метасимволы поддерживаются, пусть даже в ограниченной форме, было бы глупо отказываться от них. Следовательно, конструкторы, скорее всего, будут похожи на конструкторы basic_findfile_sequence_value_type, то есть пользователю разрешено будет задавать образец, просматриваемый каталог и флаги. Но изза того, что в Windows не разрешается задавать составной образец, нам придется добавить функциональность, компенсирующую этот недостаток. Для задания нескольких образцов мы будем следовать стандартному в Windows соглашению о разделении путей точкой с запятой: *.cpp;*h. Прочие мелкие отличия будут учтены на этапе реализации, а не проектирова ния. Например, тот факт, что Windows API допускает как прямую, так и обратную косую черту в любом сочетании, например: H:\Publishing/Books/.
Наборы
226
20.4. Класс winstl::basic_findfile_sequence Начнем с шаблонного класса набора basic_findfile_sequence.
20.4.1. Интерфейс класса Общий вид класса basic_findfile_sequence (определенного в простран стве имен winstl) аналогичен классу readdir_sequence. В листинге 20.4 показа ны типы и константычлены. Если не считать того, что в качестве value_type ис пользуется тип basic_findfile_sequence_value_type, все соответствует ожиданиям, основанным на предыдущем опыте. Листинг 20.4. Типы и константыZчлены //  ïðîñòðàíñòâå èìåí winstl template class basic_findfile_sequence_value_type; template class basic_findfile_sequence_const_iterator; template< typename C , typename T = filesystem_traits > class basic_findfile_sequence { public: // Òèïû-÷ëåíû typedef C typedef T typedef basic_findfile_sequence typedef basic_findfile_sequence_value_type typedef basic_findfile_sequence_const_iterator
char_type; traits_type; class_type; value_type; T, value_type> const_iterator; const reference; const const_reference; find_data_type; difference_type; size_type; flags_type;
В листинге 20.5 показаны все четыре конструктора, позволяющие поразному инициализировать объект класса. В этом отношении класс больше похож на glob_sequence, чем на readdir_sequence.
Адаптация API FindFirstFile/FindNextFile
227
Листинг 20.5. Конструкторы и деструктор public: // Êîíñòðóèðîâàíèå explicit basic_findfile_sequence( char_type const* pattern , flags_type flags = directories | basic_findfile_sequence( char_type const* patterns , char_type delim , flags_type flags = directories | basic_findfile_sequence( char_type const* directory , char_type const* pattern , flags_type flags = directories | basic_findfile_sequence( char_type const* directory , char_type const* patterns , char_type delim , flags_type flags = directories | ~basic_findfile_sequence() throw();
files);
files);
files);
files);
Конструкторы могут применяться следующими способами: findfile_sequence findfile_sequence_a findfile_sequence_w findfile_sequence
ffs1(_T("*.*")); ffs2("*.*", findfile_sequence_a::skipReparseDirs); ffs3(L"*.cpp|makefile.???", L'|'); ffs1( _T("h:/freelibs"), _T("*.h;*.hpp"), ';' , findfile_sequence::files);
Остальные открытые методы показаны в листинге 20.6. Разумеется, есть пара begin()/end(). Как и для readdir_sequence, предоставляется метод empty(), а метод size() не предоставляется, потому что он потребовал бы полного и по
тенциально дорогого обхода всего каталога и мог бы возвращать разные результа ты при повторных вызовах. Метод get_directory() предоставляет доступ к ка талогу, заданному в конструкторе (или к текущему каталогу, если объект был создан конструктором, который не принимает параметра directory) и прошед шему процедуру контроля, которую мы рассмотрим чуть ниже. Листинг 20.6. Методы итерации, доступа к атрибутам и состоянию в классе basic_findfile_sequence public: // Èòåðàöèÿ const_iterator begin() const; const_iterator end() const; public: // Àòðèáóòû char_type const* get_directory() const; public: // Ñîñòîÿíèå bool empty() const;
Инвариант класс (глава 7) проверяет закрытый служебный метод is_valid(). Метод validate_flags_() несет ту же функцию, что в классах glob_sequence и readdir_sequence. Метод validate_directory_() гаранти
рует, что для каталога задан полный путь, в конце которого стоит разделитель компонентов. В нем учтено, что некоторые компиляторы не поддерживают ис ключений; к этой теме мы вернемся в разделе 20.4.4.
228
Наборы
Листинг 20.7. Методы проверки инварианта и поддержки реализации private: // Èíâàðèàíò bool is_valid() const; private: // Ðåàëèçàöèÿ static flags_type validate_flags_(flags_type flags); static void validate_directory_(char_type const* directory , file_path_buffer_type_& dir);
В листинге 20.8 приведены переменныечлены. К ним относятся просматри ваемый каталог, образцы и их разделитель, а также флаги. m_directory представ ляет собой специализацию шаблона file_path_buffer (раздел 16.4), так как это путь. m_patterns – небольшой внутренний буфер, реализованный в виде специа лизации auto_buffer: его длина может быть произвольна, но в большинстве слу чаев невелика. Листинг 20.8. ПеременныеZчлены private: // Ïåðåìåííûå-÷ëåíû typedef basic_file_path_buffer file_path_buffer_type_; typedef stlsoft::auto_buffer patterns_buffer_type_; const char_type m_delim; const flags_type m_flags; file_path_buffer_type_ m_directory; // Êàòàëîã, óêàçàííûé â êîíñòðóêòîðå patterns_buffer_type_ m_patterns; // Îáðàçöû, óêàçàííûå â êîíñòðóêòîðå
И завершаем определение класса обычным запретом на реализацию методов копирования (не показаны, см. раздел 19.3).
20.4.2. Конструирование Все четыре конструктора делают примерно одно и то же. Член m_flags ини циализируется методом validate_flags_(). Параметр pattern (или patterns) копируется в член m_patterns. Метод validate_directory_() корректирует переданный каталог и записывает его в m_directory. В листинге 20.9 показан только вариант с четырьмя параметрами. В тех конструкторах, где параметр directory не передается, вместо него подставляется NULL, а вместо отсутствую щего параметра delim – char_type() (иными словами '\0'). Листинг 20.9. Конструктор последовательности с четырьмя параметрами template basic_findfile_sequence::basic_findfile_sequence( char_type const* directory , char_type const* patterns , char_type delim , flags_type flags) : m_delim(delim) , m_flags(validate_flags_(flags)) , m_patterns(1 + traits_type::str_len(patterns)) { validate_directory_(directory, m_directory); traits_type::str_n_copy(&m_patterns[0], patterns
Адаптация API FindFirstFile/FindNextFile
229
, m_patterns.size()); WINSTL_ASSERT(is_valid()); }
20.4.3. Итерация Методы итерации не нуждаются в комментариях: Листинг 20.10. Методы итерации template const_iterator basic_findfile_sequence::begin() const { WINSTL_ASSERT(is_valid()); return const_iterator(*this, m_patterns.data(), m_delim, m_flags); } template const_iterator basic_findfile_sequence::end() const { WINSTL_ASSERT(is_valid()); return const_iterator(*this); }
Метод begin() возвращает экземпляр итератора, конструктору которого пе редается ссылка на последовательность, образцы, разделитель и флаги. Метод end() мог бы вернуть просто экземпляр, созданный конструктором по умолча нию. Но для целей отладки конструктор концевого итератора принимает обрат ную ссылку на последовательность, чтобы можно было отловить попытки сравне ния с экземпляром итератора, предназначенным для другой последовательности. Совет. Храните в концевом итераторе обратную ссылку на последовательность, чтобы обнаруживать попытки сравнения с итераторами, полученными от других экземпляров последовательности. Но принимайте меры к тому, чтобы не нарушать семантику сравне) ния с экземплярами, сконструированными по умолчанию.
Кстати, это еще одна причина предпочесть последовательности автономным итераторам.
20.4.4. Обработка исключений Метод validate_directory_() должен преобразовать переданный каталог в абсолютный путь и добавить при необходимости завершающий разделитель. Последнее достигается обращением к filesystem_ traits::ensure_dir_end(), а для решения первой задачи вызывается метод get_full_path_name(), который в случае ошибки возбуждает исключение. Реализация показана в листинге 20.11. Листинг 20.11. Реализация метода validate_directory_() template void basic_findfile_sequence::validate_directory_(
Наборы
230
char_type const* directory , file_path_buffer_type_& dir) { if( NULL == directory || '\0' == *directory) { static const char_type s_cwd[] = { '.', '\0' }; directory = &s_cwd[0]; } if(0 == traits_type::get_full_path_name(directory, dir.size() , &dir[0])) { #ifdef STLSOFT_CF_EXCEPTION_SUPPORT throw filesystem_exception(::GetLastError()); #else /* ? STLSOFT_CF_EXCEPTION_SUPPORT */ dir[0] = '\0'; #endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ } else { traits_type::ensure_dir_end(&dir[0]); } }
И снова для хранения каталога используется локальная статическая строка. Однако в данном случае мы не можем инициализировать ее строковым литералом ".", потому что при этом возникла бы ошибка компиляции, когда тип char_type совпадает с wchar_t. Вместо этого строка инициализируется массивом из двух символов: '.' и '\0'. Поскольку и нулевому символу, и символу '.' в любой ко дировке соответствует одна и та же кодовая позиция, этот способ позволяет без труда инициализировать простые строки. Совет. Создавайте независящие от кодировки символов строковые литералы (содержа) щие только кодовые позиции из диапазона 0x00)0x7F) в виде массивов типа char_type и инициализируйте их с помощью синтаксиса инициализации массивов.
По историческим причинам компонент basic_findfile_sequence должен корректно работать и при отсутствии поддержки исключений. В данном случае, если get_full_path_name() завершается с ошибкой, то в dir[0] записывается '\0'. Метод begin() обнаружит этот факт и вернет концевой итератор, как пока зано в листинге 20.12. Таким образом, просмотр некорректно заданного каталога работает даже тогда, когда компилятор не поддерживает исключений. Листинг 20.12. Обработка случая отсутствия поддержки исключений в методе begin() template const_iterator basic_findfile_sequence::begin() const { WINSTL_ASSERT(is_valid());
Адаптация API FindFirstFile/FindNextFile
231
#ifndef STLSOFT_CF_EXCEPTION_SUPPORT if('\0' == m_directory[0]) { ::SetLastError(ERROR_INVALID_NAME); return const_iterator(*this); } #endif /* !STLSOFT_CF_EXCEPTION_SUPPORT */ return const_iterator(*this, m_patterns.data(), m_delim, m_flags); }
Такой же подход применен и при проверке инварианта: Листинг 20.13. Метод проверки инварианта template bool basic_findfile_sequence::is_valid() const { #ifdef STLSOFT_CF_EXCEPTION_SUPPORT if('\0' == m_directory[0]) { # ifdef STLSOFT_UNITTEST unittest::fprintf(err, "ïóñòîé êàòàëîã, ïîääåðæêà èñêëþ÷åíèé âêëþ÷åíà\n"); # endif /* STLSOFT_UNITTEST */ return false; } #endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ if( '\0' != m_directory[0] && !traits_type::has_dir_end(m_directory.c_str())) { #ifdef STLSOFT_UNITTEST unittest::fprintf(unittest::err, "m_directory íå ïóñò è íå çàâåðøàåòñÿ ðàçäåëèòåëåì êîìïîíåíòîâ ïóòè; m_directory=%s\n", m_directory.c_str()); #endif /* STLSOFT_UNITTEST */ return false; } return true; }
Совет. Стремитесь к тому, чтобы компоненты вели себя предсказуемо даже тогда, когда исключения не поддерживаются. Если это недостижимо, включайте директиву #error, чтобы предотвратить компиляцию, а не генерировать небезопасный код, ничего не сооб) щая об этом.
20.5. Класс winstl::basic_findfile_sequence_const_iterator В листинге 20.14 приведено определение класса basic_findfile_sequence_ const_iterator. В его открытом интерфейсе нет никаких сюрпризов (они под жидают нас в реализации). Итератор принадлежит категории итераторов ввода. Категория ссылок на элементы – временные по значению. Вложенный класс
232
Наборы
shared_handle практически не отличается от того, что мы видели в readdir_ sequence, разве что в качестве «нулевого» значения используется INVALID_ HANDLE_VALUE, а не NULL, а для освобождения описателя применяется функция FindClose(). Поэтому определение вложенного класса мы не приводим.
Листинг 20.14. Определение класса basic_findfile_sequence_const_iterator //  ïðîñòðàíñòâå èìåí winstl template< typename C // Òèï ñèìâîëà , typename T // Òèï õàðàêòåðèñòè÷åñêîãî êëàññà , typename V // Òèï çíà÷åíèÿ > class basic_findfile_sequence_const_iterator : public std::iterator< std::input_iterator_tag , V, ptrdiff_t , void, V // âðåìåííàÿ ïî çíà÷åíèþ > { private: // Òèïû-÷ëåíû typedef basic_findfile_sequence sequence_type; public: typedef C char_type; typedef T traits_type; typedef V value_type; typedef basic_findfile_sequence_const_iterator class_type; typedef typename traits_type::find_data_type find_data_type; typedef typename sequence_type::size_type size_type; private: typedef typename sequence_type::flags_type flags_type; private: // Êîíñòðóèðîâàíèå basic_findfile_sequence_const_iterator( sequence_type const& seq , char_type const* patterns , char_type delim , flags_type flags); basic_findfile_sequence_const_iterator(sequence_type const& seq); public: basic_findfile_sequence_const_iterator(); basic_findfile_sequence_const_iterator(class_type const& rhs); ~basic_findfile_sequence_const_iterator() throw(); class_type& operator =(class_type const& rhs); public: // Ìåòîäû èòåðàòîðà ââîäà class_type& operator ++(); class_type operator ++(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ const value_type operator *() const; bool equal(class_type const& rhs) const; private: // Ðåàëèçàöèÿ static HANDLE find_first_file_( char_type const* spec , flags_type flags , find_data_type* findData); private: // Âñïîìîãàòåëüíûå êëàññû struct shared_handle { . . . };
Адаптация API FindFirstFile/FindNextFile
233
private: // Ïåðåìåííûå-÷ëåíû friend class basic_findfile_sequence; typedef basic_file_path_buffer file_path_buffer_type_; sequence_type const* m_sequence; shared_handle* m_handle; typename traits_type::find_data_type m_data; file_path_buffer_type_ m_subPath; size_type m_subPathLen; char_type const* m_pattern0; char_type const* m_pattern1; char_type m_delim; flags_type m_flags; };
Член m_sequence – это обратный указатель на последовательность. (Указа тель, потому что ссылке нельзя присвоить новое значение.) m_handle – указатель на экземпляр разделяемого контекста shared_handle. m_data – структура типа WIN32_FIND_DATA, в которой хранится информация о текущем просматриваемом элементе. m_delim и m_flags – разделитель и флаги, хранящиеся в экземпляре последовательности. Остальные четыре члена – m_subPath, m_subPathLen, m_pattern0 и m_pattern1 будут нужны при обработке образцов и просматривае мых элементов в операторе прединкремента (раздел 20.5.3).
20.5.1. Конструирование Как видно из листинга 20.14, в классе итератора есть четыре конструктора, а также открытый оператор копирующего присваивания и деструктор. В листин ге 20.15 приведена реализация конструктора преобразования с четырьмя пара метрами. Он вызывает operator ++(), чтобы сдвинуть итератор на первый подхо дящий элемент (или в конец). Отметим, что оба члена m_pattern0 и m_pattern1 инициализированы параметром patterns, который есть не что иное, как член m_patterns класса последовательности; мы объясним, зачем это нужно в разде ле 20.5.3. Листинг 20.15. Конструктор итератора с четырьмя параметрами template basic_findfile_sequence_const_iterator:: basic_findfile_sequence_const_iterator( sequence_type const& seq , char_type const* patterns , char_type delim , flags_type flags) : m_sequence(&seq) , m_handle(NULL) , m_subPath() , m_subPathLen(0) , m_pattern0(patterns) , m_pattern1(patterns) , m_delim(delim) , m_flags(flags)
234
Наборы
{ m_subPath[0] = '\0'; operator ++(); }
Копирующий оператор присваивания показан в листинге 20.16. Обратите внимание на локальную переменную и на то, как она используется для отложен ного освобождения исходного разделяемого описателя. Тем самым мы решаем проблему, когда итератор присваивается сам себе, а счетчик ссылок в этот момент равен 1. Возникает естественное стремление сначала освободить ресурс, но в дан ном случае это привело бы к уничтожению объекта разделяемого контекста еще до того, как ссылка будет увеличена на 1. Листинг 20.16. Копирующий оператор присваивания template class_type& basic_findfile_sequence_const_iterator:: operator =(class_type const& rhs) { WINSTL_MESSAGE_ASSERT("Ïðèñâàèâàíèå èòåðàòîðà èç äðóãîé ïîñëåäîâàòåëüíîñòè" , m_sequence == NULL || rhs.m_sequence == NULL || rhs.m_sequence); shared_handle* prev_handle = m_handle; m_handle = rhs.m_handle; m_data = rhs.m_data; m_subPath = rhs.m_subPath; m_subPathLen = rhs.m_subPathLen; m_pattern0 = rhs.m_pattern0; m_pattern1 = rhs.m_pattern1; m_delim = rhs.m_delim; m_flags = rhs.m_flags; if(NULL != m_handle) { m_handle->AddRef(); } if(NULL != prev_handle) { prev_handle->Release(); } return *this; }
Рассмотрения таких случаев можно избежать, если пользоваться интеллекту альными указателями с подсчетом ссылок, например шаблонным классом ref_ptr из библиотеки STLSoft. Не сделал я этого потому, что многие классы последовательностей – readdir_sequence, basic_findfile_sequence, basic_ findvolume_ sequence и прочие – написаны до того, как класс ref_ptr был пере несен в STLSoft из закрытой библиотеки моей компании (в которой он фигуриро вал под более длинным именем ReleaseInterface). Если бы я писал класс набо ра сейчас, то, скорее всего, использовал бы ref_ptr. Зато в теперешнем виде он позволяет привлечь ваше внимание к проблемам, возникающим при подсчете ссылок.
Адаптация API FindFirstFile/FindNextFile
235
Реализации других конструкторов эквивалентны тем, что имеются в классе readdir_sequence, поэтому, чтобы сэкономить место, я их опущу.
20.5.2. Метод find_first_file_() Прежде чем заняться оператором прединкремента – самым длинным в этом классе, – я хотел бы обсудить одну из используемых в нем служебных функций (листинг 20.17). Листинг 20.17. Реализация служебного метода find_first_file_() template HANDLE basic_findfile_sequence_const_iterator