Мэтью Уилсон
РАСШИРЕНИЕ БИБЛИОТЕКИ STL ДЛЯ С++ Наборы и итераторы
Мэтью Уилсон
EXTENDED STL, VOLUME 1 Collections an...
35 downloads
2102 Views
2MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Мэтью Уилсон
РАСШИРЕНИЕ БИБЛИОТЕКИ 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 () Следствием определения категорий временных по значению и отсутствую щих ссылок является тот факт, что в итераторах, возвращающие такие ссылки, не может быть реализован оператор «стрелка». Связано это с тем, что нет ничего та кого, что могло бы послужить в качестве возвращаемого значения, к которому можно было бы применить указатель или ссылку. А спецификация языка требует выполнения этого условия для любой перегрузки оператора ->(). Правило. Итераторы, относящиеся к категории временных по значению и отсутствующих ссылок, не могут определять оператор «стрелка».
Это правило полезно соотнести с проблемами, описанными в разделе 1.3.6. По правде говоря, в большинстве случаев итератор, поддерживающий семантику временной по значению ссылки, можно преобразовать так, чтобы он поддерживал недолговечные ссылки – достаточно было бы сохранить экземпляр текущего зна чения, но при этом может пострадать производительность. К счастью, как мы уви дим в главе 41, существуют способы распознать наличие или отсутствие операто ра «стрелка».
82
Основы
3.6. Еще о категориях ссылок на элементы Если сейчас все это кажется вам малопонятным или бесцельным, ничего страшного. Важность этого материала станет ясной по мере чтения частей II и III, где вы увидите, как различные характеристики влияют на проектирование и реа лизацию наборов, их итераторов и адаптеров итераторов.
Глава 4. Забавная безвременная ссылка Будь собой, прочие роли уже заняты. – Оскар Уайльд В C++ есть одна малоизвестная особенность, имеющая отношение к семантике кода, в котором используются временные по значению ссылки на элементы (раз дел 3.3). Рассмотрим следующий фрагмент: std::string const& rs = std::string("Áåçîïàñíî? Ñîìíèòåëüíî"); std::cout 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
Отметим также, что метод 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 () Реализация метода 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 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::find_first_file_( char_type const* searchSpec , flags_type flags , find_data_type* findData) { HANDLE hSrch = INVALID_HANDLE_VALUE; enum { #ifdef FILE_ATTRIBUTE_REPARSE_POINT reparsePointConstant = FILE_ATTRIBUTE_REPARSE_POINT #else /* ? FILE_ATTRIBUTE_REPARSE_POINT */ reparsePointConstant = 0x00000400 #endif /* FILE_ATTRIBUTE_REPARSE_POINT */ }; #if defined(_WIN32_WINNT) && \ _WIN32_WINNT >= 0x0400 if( (directories == (flags & (directories | files))) && system_version::winnt() && system_version::major() >= 4) { hSrch = traits_type::find_first_file_ex(searchSpec , FindExSearchLimitToDirectories, findData); } else #endif /* _WIN32_WINNT >= 0x0400 */ if(INVALID_HANDLE_VALUE == hSrch) { hSrch = traits_type::find_first_file(searchSpec, findData); } for(; INVALID_HANDLE_VALUE != hSrch; ) { if(traits_type::is_file(findData)) { if(flags & sequence_type::files) { break; } } else {
Наборы
236
if(traits_type::is_dots(findData->cFileName)) { if(flags & sequence_type::includeDots) { break; } } else if(flags & sequence_type::directories) { if( 0 == (flags & sequence_type::skipReparseDirs) || 0 == (findData->dwFileAttributes & reparsePointConstant)) { break; // Òî÷êè ìîíòèðîâàíèÿ íå ïðîïóñêàþòñÿ èëè ýòî íå òî÷êà // ìîíòèðîâàíèÿ } } } if(!traits_type::find_next_file(hSrch, findData)) { ::FindClose(hSrch); hSrch = INVALID_HANDLE_VALUE; break; } } return hSrch; }
Этот метод вызывает нужный вариант функции FindFirstFile() и получает первый элемент, соответствующий заданным флагам. Чтобы применить фильтра цию, метод синтезирует локальную константу, которая будет использоваться для проверки на точку монтирования. Делается это потому, что константа FILE_ ATTRIBUTE_REPARSE_POINT определена в заголовочных файлах компилятора не для всех версий Windows. (Мы определили перечисление, а не целочисленную константу, чтобы избежать жалоб компилятора на неиспользуемые переменные, сообщений об ошибках компоновки и прочей ерунды.) Еще один достойный упоминания момент – это условная проверка флагов и вызов функции FindFirstFileEx() (с помощью traits_type::find_first_ file_ex()). Эта функция имеется во всех операционных системах семейства NT, начиная с NT 4, но отсутствует в системах семейства Windows 9x. На самом деле, это тоже пара A/Wфункций, а ее сигнатура со скрытой зависимостью от кодиров ки символов выглядит так: HANDLE FindFirstFileEx( LPCTSTR , FINDEX_INFO_LEVELS , void* , FINDEX_SEARCH_OPS , void* , DWORD
fileName infoLevelId */ findFileData */ searchOp searchFilter additionalFlags);
Параметр searchOp определяет вид поиска. Флаг FindExSearchLimitToDi rectories говорит, что нужно возвращать только каталоги, и в этом смысле экви валентен флагу GLOB_ONLYDIR в API glob. Однако, в отличие от GLOB_ONLYDIR,
Адаптация API FindFirstFile/FindNextFile
237
это лишь рекомендация; не для всех файловых систем она выполняется. Тем не менее, мы включили ее в надежде, что для тех файловых систем, которые ее все же поддерживают, удастся сократить количество системных вызовов, необходимых для поиска в заданном множестве каталогов. Вспомнив, что в предыдущих главах упоминалась функция dl_call(), кото рая позволяет вызывать функции из динамически загружаемой библиотеки, вы могли бы спросить, почему она не используется в данном случае. Просто потому, что и без нее класс будет работать нормально, а накладные расходы на повторную загрузку библиотеки и перехват исключений, возбуждаемых в случае неудачи, только снижают производительность. Можно было бы один раз загрузить функ цию и сохранить ее описатель в экземпляре последовательности, но это слишком усложнило бы код; я предпочитаю обойтись статической компоновкой в случае, когда пользователь определил соответствующее значение символа _WIN32_WINNT. Этот символ употребляется в заголовочных файлах Windows для указания того, что компилировать код нужно только для NT, поэтому мне кажется, что использо вание в собственных интересах подобных указаний пользователю – вполне ра зумная практика. Совет. Стремитесь использовать функции API, которые доступны на отдельных платфор) мах, если пользователь явно указал, что это можно делать.
20.5.3. operator ++() Не могу не сознаться – этот метод огромен. Причин тому несколько: необходимо разобрать и последовательно обработать составные образцы; Windows принимает в качестве разделителя компонентов пути как пря мую, так и обратную косую черту; каталоги '.' и '..' нуждаются в специальной обработке; элементы необходимо фильтровать; путь к каталогу поиску нужно объединить с каждым образцом, перед тем как передавать его функции find_first_file(); необходимо сформировать подпуть для итератора. Получается довольно длинный код, поэтому я буду рассказывать о нем по ча стям. Прежде чем переходить к описанию реализации, хочу познакомить вас со слу жебным шаблоном функции stlsoft::find_next_token(), который реализует семантику разбора с возвратом к началу. У него есть два перегруженных варианта со следующими сигнатурами: template C const* find_next_token( C const*& p0 , C const*& p1 , C const* const end
Наборы
238 , C
delim);
template bool find_next_token(C const*& p0, C const*& p1, C delim);
Первый разбивает строку по заданному разделителю до точки end. Второй выполняет разбор, пока не встретится завершающий нуль. Оба принимают ссыл ки на указатели на области памяти, в которых сохраняется состояние разбора. В самом начале оба указателя устанавливаются на одну и ту же точку строки (на чало), а затем в цикле вызывается функция find_next_token(), пока она не сооб щит о достижении конца, вернув false. Каждая выделенная лексема представле на отрезком строки {p1 – p0, p0}, как показано в следующем примере, который выводит [][*.zip][*.html][][*.exe][][*.pdf]: static const char patterns[] = "||*.zip|*.html||*.exe||*.pdf|"; char const* p0 = &patterns[0]; char const* p1 = &patterns[0]; while(stlsoft::find_next_token(p0, p1, '|')) { ::printf("[%.*s]", p1 - p0, p0); }
Поскольку функция не выполняет ни выделения памяти, ни копирования, ра ботает она очень быстро (в разделе 27.9 мы рассмотрим вопрос о разбиении стро ки на лексемы более подробно). Недостаток ее в том, что она понимает только од носимвольные разделители, а так как возвращенные лексемы не являются строками, завершающимися нулем, то с ними трудно работать. Отметим, что фун кция не отбрасывает пустые отрезки, но это легко сделать в клиентском коде: . . . while(stlsoft::find_next_token(p0, p1, '|')) { if(p1 != p0) { ::printf("[%.*s]", p1 - p0, p0); } }
Теперь печатается [*.zip][*.html][*.exe][*.pdf]. Разобравшись с этим шаблоном, вернемся к основной теме. Сначала, в листинге 20.18, я представлю общую структуру кода, а потому перейду к отдельным частям. Листинг 20.18. Метод прединкремента template class_type& basic_findfile_sequence_const_iterator::operator ++() { WINSTL_MESSAGE_ASSERT("Ïîïûòêà èíêðåìåíòèðîâàòü íåäåéñòâèòåëüíûé èòåðàòîð!" , '\0' != *m_pattern0); WINSTL_ASSERT(NULL != m_pattern0); WINSTL_ASSERT(NULL != m_pattern1);
Адаптация API FindFirstFile/FindNextFile
239
enum { #ifdef FILE_ATTRIBUTE_REPARSE_POINT reparsePointConstant = FILE_ATTRIBUTE_REPARSE_POINT #else /* ? FILE_ATTRIBUTE_REPARSE_POINT */ reparsePointConstant = 0x00000400 #endif /* FILE_ATTRIBUTE_REPARSE_POINT */ }; for(; '\0' != *m_pattern0 || '\0' != *m_pattern1; ) { if(NULL == m_handle) { while(stlsoft::find_next_token(m_pattern0,m_pattern1, m_delim)) { WINSTL_ASSERT(m_pattern0 hSrch; ) { . . . // 5. Ïðîôèëüòðîâàòü ýëåìåíòû â ñîîòâåòñòâèè ñ ôëàãàìè. } } } return *this; }
Хочется надеяться, что здесь все понятно. Во внешнем цикле for обработка продолжается, пока не кончатся образцы. Внутри этого цикла проверяется член m_handle. Если он равен NULL, то активного образца нет, то есть в данный момент итератор не обходит файловую систему по выделенному из строки образцу. Это может случиться при первом вызове operator ++() или потому, что уже найдены все элементы, соответствующие предыдущему образцу, и, стало быть, пора пере ходить к следующему. Как бы то ни было, в цикле while извлекается следующая лексема, причем в предложении if пустые лексемы отбрасываются. (Теперь по нятно, почему по умолчанию m_delim равно '\0', ведь именно нуль служит для find_next_token() признаком завершения обработки. Поскольку он игнориру ется, никакого разбора по существу не произойдет. Это означает, что если объект последовательности создавался конструктором, в котором устанавливается это значение m_delim, то поддержка составных образцов будет отключена. Что и тре бовалось доказать.) Получив непустую строку, мы формируем образец поиска из текущей лексе мы и просматриваемого каталога, как показано в листинге 20.19.
240
Наборы
Листинг 20.19. Формирование образца поиска . . . // 1. Ñôîðìèðîâàòü îáðàçåö ïîèñêà èç ïðîñìàòðèâàåìîãî êàòàëîãà è ëåêñåìû file_path_buffer_type search; // Áóôåð, â êîòîðîì ôîðìèðóåòñÿ îáðàçåö ïîèñêà size_type cch; // Óñêîðÿåò îïåðàöèè str_??() if(traits_type::is_path_rooted(m_pattern0)) { search[0] = '\0'; cch = 0; } else { traits_type::str_copy(&search[0], m_sequence->get_directory()); cch = traits_type::str_len(&search[0]); —cch; // Ïóòü ê êàòàëîãó óæå çàâåðøàåòñÿ ðàçäåëèòåëåì êîìïîíåíòîâ traits_type::ensure_dir_end(&search[(cch > 1) ? (cch - 2) : 0]); } traits_type::str_n_cat( &search[0] + cch, m_pattern0 , m_pattern1 - m_pattern0); . . .
Проверка с помощью функции is_path_rooted() делается на случай, если пользователь решил сконструировать последовательность так: findfile_sequence files("D:\\Dev", "*.cpp|h:/abs/olute.txt", '|');
Нам не хотелось бы, чтобы при этом второй образец для поиска имел вид "D:\Dev\h:/abs/olute.txt". В остальных случаях мы получаем путь к каталогу от класса последовательности с помощью обратного указателя m_sequence и ко пируем его в буфер search. В этом месте вычисляется и сохраняется длина пути,
чтобы не начинать каждую строковую операцию с начала строки; конечно, эконо мия получает грошовая, но все равно стоит сделать, чтобы не выполнять абсолют но бессмысленную работу. Значение длины уменьшается на 1, чтобы метод ensure_dir_end() правильно работал во всех случаях, и напоследок в конец строки дописывается лексема, представляющая текущий образец. Совет. Старайтесь запоминать место, с которого можно надежно продолжать обработку строки, чтобы избежать ненужной работы при манипулировании строками, не завершаю) щимися нулем, с помощью функций из стандартной библиотеки C.
В следующей части метода вычисляется подпуть поиска с учетом шизофрени ческой поддержки функциями Windows API обоих вариантов косой черты в пу тях. Код приведен в листинге 20.20. Листинг 20.20. Обработка прямой и обратной косой черты . . . // 2. Âû÷èñëèòü ïîäïóòü äëÿ òåêóùåãî îáõîäà char_type const* slash; // Ïðèõîäèòñÿ îáúÿâëÿòü, . . . char_type const* bslash; // . . . ÷òîáû íå âûëåçàòü çà ïðåäåëû ñòðàíèöû // êíèãè. ;-)
Адаптация API FindFirstFile/FindNextFile
241
slash = traits_type::str_rchr(&search[0] + cch, '/'); bslash = traits_type::str_rchr(&search[0] + cch, '\\'); WINSTL_ASSERT(!traits_type::is_path_rooted(m_pattern0) || ((NULL != slash) || (NULL != bslash))); if( NULL != slash && slash >= m_pattern1) { slash = NULL; } if( NULL != bslash && bslash >= m_pattern1) { bslash = NULL; } if( NULL == slash && NULL == bslash) { m_subPath[0] = '\0'; m_subPathLen = 0; } else { if(NULL == slash) { slash = bslash; } else if(NULL != bslash && slash < bslash) { slash = bslash; } const size_t n = static_cast<size_t>(slash - &search[0]); traits_type::str_n_copy(&m_subPath[0], &search[0], n); m_subPathLen = n; m_subPath[n] = '\0'; } . . .
Здесь ищется последний символ прямой или обратной косой черты, чтобы уз нать, какую часть образца поиска следует копировать в член m_subPath. Обратите внимание, что мы продолжаем использовать переменную cch в обращениях к traits_type::str_rchr(), чтобы не просматривать всю строку целиком. Вы числять подпуть, который в дальнейшем объединяется с членом WIN32_FIND_ DATA::cFileName для формирования полного пути, нужно для того, чтобы пра вильно обрабатывать такие критерии поиска: findfile_sequence files( "H:/freelibs/shwild/current" , "include\shwild\*.h*;src\*.h*", ';');
Здесь в состав образца поиска включены подкаталоги. Если бы не специаль ная обработка подпути, то возвращенный путь к элементу оказался бы неправиль ным. Представим себе, что каталог H:\freelibs\shwild\current\include\ shwild содержит файл shwild.hpp. Если бы мы не вычислили подпуть, то был бы
242
Наборы
возвращен такой путь к элементу: H:\freelibs\shwild\current\shwild.hpp. (Это урок из реального опыта, который я получил спустя много времени после того, как счел, что класс findfile_sequence хорошо протестирован и правильно работает!) На следующем шаге вызывается функция find_first_file_(), а затем идет обработка особого случая каталогов '.' и '..' и создание экземпляра разделяе мого описателя: Листинг 20.21. Получение элементов и создание разделяемого контекста . . . // 3. & 4. Âûçâàòü find_first_file_() è îáðàáîòàòü ñïåöèàëüíûå êàòàëîãè HANDLE hSrch = find_first_file_(search.c_str(), m_flags, &m_data); if(INVALID_HANDLE_VALUE != hSrch) { stlsoft::scoped_handle cleanup( hSrch, ::FindClose , INVALID_HANDLE_VALUE); if( '.' == m_pattern0[0] && ( m_pattern1 == m_pattern0 + 1 || ( '.' == m_pattern0[1] && m_pattern1 == m_pattern0 + 2))) { const size_t n = static_cast<size_t>(m_pattern1 - m_pattern0); traits_type::str_n_copy(&m_data.cFileName[0], m_pattern0, n); m_data.cFileName[n] = '\0'; } m_handle = new shared_handle(hSrch); if(NULL != m_handle) { cleanup.detach(); } return *this; } . . .
Описатель поиска передается конструктору класса scoped_handle (раздел 16.5), который автоматически вызывает для него функцию FindClose() при воз никновении исключения. Обработка особого случая призвана гарантировать, что в том случае, когда пользователь указал в качестве образца поиска строку "." или "..", в качестве элемента возвращается именно это имя, а не имя соответствующего каталога. Здесь нет однозначно правильного или неправильного решения, просто мне так удобно. Вы можете поступить иначе. Осталось создать экземпляр shared_handle и не забыть освободить ресурс, если это не получится. Проверка на NULL гарантирует корректное поведение вне зависимости от того, поддерживаются исключения или нет. Если все хорошо, вы зывается метод scoped_handle::detach(), который передает описатель поиска в распоряжение экземпляра shared_handle. Последняя часть метода – фильтрация, показанная в листинге 20.22. Логичес ки она не отличается от того, что было проделано в find_first_file_(), но до
Адаптация API FindFirstFile/FindNextFile
243
полнительно мы освобождаем и устанавливаем в NULL переменную shared_ handle по завершении поиска. Листинг 20.22. Фильтрация элементов . . . // 5. Ïðîôèëüòðîâàòü ýëåìåíòû â ñîîòâåòñòâèè ñ ôëàãàìè. if(NULL != m_handle) { for(; INVALID_HANDLE_VALUE != m_handle->hSrch; ) { if(!traits_type::find_next_file(m_handle->hSrch, &m_data)) { m_handle->Release(); m_handle = NULL; break; } else { if(traits_type::is_file(&m_data)) { if(m_flags & sequence_type::files) { return *this; } } else { if(traits_type::is_dots(m_data.cFileName)) { if(m_flags & sequence_type::includeDots) { return *this; } } else if(m_flags & sequence_type::directories) { if( 0 == (m_flags & sequence_type::skipReparseDirs) || 0 == (m_data.dwFileAttributes & reparsePointConstant)) { return *this; } } } } } }
Больше о классе итератора сказать практически нечего. Оператор разымено вания просто передает подпуть и член m_data конструктору типа значения, а ме тод equal() смотрит, одинаковы ли члены m_handle у двух экземпляров. По поводу реализации стоит отметить еще одну вещь: ошибка при вызове ме тода find_first_file_() интерпретируется как «элементов не найдено». Мы не проверяем дополнительно, так ли это в действительности, или произошла какая
244
Наборы
то ошибка операционной системы, не позволившая выполнить поиск. Я решил, что не стоит загружать пользователя рассмотрением различных условий, при ко торых такая ситуация может сложиться, и до сих пор никто не жаловался. Но это еще один вопрос, на который вы можете ответить подругому. На самом деле, в шаблонном классе inetstl::basic_findfile_ sequence, который имеет мно го общего с версией для WinSTL, такая проверка производится, но это мы обсу дим в следующей интерлюдии, главе 21.
20.6. Класс winstl::basic_findfile_sequence_value_type По сути, тип значения – это полный путь вместе со структурой WIN32_FIND_ DATA. Он обладает удобным интерфейсом в виде шаблонного класса basic_ findfile_sequence_value_type, определение которого, а заодно и реализация четырех методов, приведены в листинге 20.23. Листинг 20.23. Определение класса basic_findfile_sequence_value_type template class basic_findfile_sequence_value_type { private: // Òèïû-÷ëåíû typedef basic_findfile_sequence sequence_type; typedef typename sequence_type::flags_type flags_type; public: typedef C char_type; typedef T traits_type; typedef basic_findfile_sequence_value_type class_type; typedef typename traits_type::find_data_type find_data_type; typedef typename sequence_type::size_type size_type; private: // Êîíñòðóèðîâàíèå basic_findfile_sequence_value_type( find_data_type const& data , char_type const* directory , size_type cchDirectory) : m_data(data) { traits_type::str_n_copy(&m_path[0], directory, cchDirectory); traits_type::ensure_dir_end(&m_path[0]); traits_type::str_cat(&m_path[0] + cchDirectory, data.cFileName); } public: basic_findfile_sequence_value_type(); class_type& operator =(class_type const& rhs); public: // Àòðèáóòû find_data_type const& get_find_data() const; char_type const* get_filename() const; char_type const* get_short_filename() const { return '\0' != m_data.cAlternateFileName[0] ? m_data.cAlternateFileName : m_data.cFileName;
Адаптация API FindFirstFile/FindNextFile
245
} char_type const* get_path() const; char_type const* c_str() const; operator char_type const * () const; bool is_directory() const { return traits_type::is_directory(&m_data); } bool is_file() const; bool is_compressed() const; #ifdef FILE_ATTRIBUTE_REPARSE_POINT bool is_reparse_point() const; #endif /* FILE_ATTRIBUTE_REPARSE_POINT */ bool is_read_only() const; bool is_system() const; bool is_hidden() const; bool equal(char_type const* rhs) const { return 0 == traits_type::str_compare_no_case( this->get_path() , rhs); } bool equal(class_type const& rhs) const; private: // Ïåðåìåííûå-÷ëåíû friend class basic_findfile_sequence_const_iterator; typedef basic_file_path_buffer file_path_buffer_type_; find_data_type m_data; file_path_buffer_type_ m_path; };
Конструктор копирует информацию из WIN32_FIND_DATA в m_data, после чего строит член m_path из подпути и поля структуры cFileName. Все методы is_??() реализованы с помощью соответствующих методов характеристического класса filesystem_traits и пользуются полем dwFileAttributes структуры WIN32_FIND_DATA. Сравнение производится методами equal(). Тот из них, который принимает другой экземпляр класса, просто вызывает перегруженный вариант с типом char_type const*, в котором и выполняется вся работа. Регистр символов при сравнении не учитывается, так как файловые системы Windows нечувствительны к регистру. Наконец, метод get_short_filename() возвращает или настоящее короткое имя, если такое существует, или «обычное» имя в противном случае. Windows ста рается в какойто мере обеспечить обратную совместимость, присваивая допол нительные короткие имена файлам, истинное имя которых не соответствует со глашениям, принятым в DOS. Например, мой начальный каталог называется MATTY.SYNESIS, а его короткое имя MATTY~1.SYN. Если имя и так соответствует соглашениям DOS, то поле cAlternateFileName пусто. Для учета всех этих ос ложнений и введен метод get_short_filename(). Мы видели, что точки монтирования можно обработать независимо от того, какие заголовочные файлы использовались при компиляции файла, поскольку
246
Наборы
это поведение определяется на этапе выполнения. Но ради простоты в методе is_reparse_point() точки монтирования пропускаются, если не задан флаг FILE_ATTRIBUTE_REPARSE_POINT. Это разумное (и экономящее усилия) поведе ние по умолчанию, но одновременно оно служит пользователю напоминанием о том, что хорошо бы обновить заголовочные файлы. Существует оператор неявного преобразования. Это рудимент прошлых вре мен, когда я еще не опасался писать такие вещи. В данном случае он не объявлен нежелательным, поскольку безвреден – ему не соответствует никакой неявный конструктор преобразования. Однако это ребяческий подход, которого следует избегать в новых проектах, учитывая, что есть более удачные альтернативы. Одна из них – прокладки строкового доступа. Такие прокладки определены для типа значения, и мы опишем их в следующем разделе.
20.7. Прокладки В примере из раздела «Мотивация» (раздел 20.2) я опирался на прокладки строкового доступа для типа значения последовательности. В листинге 20.24 при ведены их определения. Листинг 20.24. Прокладки строкового доступа для типа basic_findfile_sequence_ value_type namespace stlsoft { template C const* c_str_data( winstl::basic_findfile_sequence_value_type const& v) { return v.get_path(); } template size_t c_str_len( winstl::basic_findfile_sequence_value_type const& v) { return ::stlsoft::c_str_len(v.get_path()); } template C const* c_str_ptr( winstl::basic_findfile_sequence_value_type const& v) { return v.get_path(); } } // namespace stlsoft
Почитателям прокладок строкового доступа полезно знать, что имеются так же варианты этих функций с суффиксами _a и _w. Все детали можно найти в реа лизации класса winstl::basic_findfile_sequence в дистрибутиве STLSoft (имеется на компактдиске).
Адаптация API FindFirstFile/FindNextFile
247
20.8. А где же шаблонные прокладки и конструкторы? Читая предыдущие главы (да и почти всю эту книгу), вы можете прийти к вы воду, что я помешался на прокладках. Прощаю вас, потому что так оно и есть. По этому тот факт, что в конструкторах класса basic_findfile_sequence не ис пользуются прокладки строкового доступа, выглядит очевидным упущением. Ведь пользователям этого класса были бы доступны те же блага, что и пользовате лям других классов, в которых прокладки применяются. А отсутствуют они лишь по причине плохого планирования с моей стороны. Мы уже видели, с какими пре пятствиями приходится бороться, чтобы справиться с неоднозначностями шабло нов энумераторов и конструкторов в классе glob_sequence из главы 18. В классе basic_findfile_sequence конструкторов больше, и я (пока) не придумал рабо тающую, не страдающую от неоднозначностей, обратно совместимую схему обновления интерфейса класса. Поэтому пользователям приходится вручную вы зывать метод c_str() и ему подобные, если они хотят применять последователь ности, передавая конструктору чтото отличное от Cстрок. Если читатель захо чет принять вызов и сумеет отыскать удачный механизм применения прокладок строкового доступа к этой последовательности, я буду рад узнать, как это можно сделать.
20.9. Резюме По существу, при адаптации API FindFirstFile/FindNextFile к STL мы стал киваемся с теми же проблемами и решениями, которые уже видели на примере API opendir/readdir: итератор должен обобществлять состояние, чтобы поддержать однопро ходную семантику; метод size() не предоставляется (это согласуется с принципами наимень% шего удивления и наибольшего удивления); отбрасывание каталогов '.' и '..', а также фильтрация файлов или ката логов производится самим компонентом (принцип экономии); для обеспечения надежного интерфейса и семантики применяются инкап суляция и безопасность относительно исключений. Однако между обоими API существуют различия, которые наложили отпеча ток на проектирование и реализацию класса basic_findfile_sequence: все строковые API в Windows существуют в двух вариантах: для кодировок ANSI и Unicode. Поэтому STLнабор (и все относящиеся к нему типы) должны поддерживать обе формы, отсюда и реализация в виде шаблона класса, параметризуемого характеристическим классом winstl:: filesystem_traits (принцип разнообразия); структура WIN32_FIND_DATA содержит ценную информацию о состоянии, которую не хочется игнорировать. Если мы намерены дать к ней доступ
248
Наборы
пользователю и одновременно разрешить полные/абсолютные пути, необ ходимо, чтобы тип значения был представлен классом; поскольку в Windows нет эквивалента API glob, а функция FindFirstFile() принимает только один образец поиска, то необходимо самостоятельно надстроить слой для обработки составных образцов. Это требует разбора строки с образцом и неизбежно усложняет код метода operator ++() (принцип экономии); API, предлагаемые Windows, допускают (в большинстве случаев) символы прямой и обратной косой черты в качестве разделителей компонентов пути; это усложняет работу с образцами (принцип экономии); следует принимать во внимание дополнительные возможности файловых систем, например, точки монтирования и поддержку поиска одних лишь каталогов (при обращении FindFirstFileEx(FindExSearchLimitToDi rectories)), но так, чтобы не ограничивать переносимость компонента (принцип экономии); В следующей интерлюдии мы обсудим еще один необльшой компонент, отно сящийся к файловым системам. А затем перейдем к математическим последова тельностям, управлению процессами, разбиению строк, окнам и вводу/выводу с разнесением и сбором. Это расширит диапазон задач, которые можно адаптиро вать к STLнаборам.
20.10. Еще об обходе файловой системы с помощью recls Просто из спортивного интереса я хочу показать вам альтернативную реали зацию функции ClearDirectory(), в которой используется библиотека recls для рекурсивного поиска в файловой системе, реализованная на основе unixstl:: glob_sequence, unixstl::readdir_sequence и winstl::basic_ findfile_ sequence. Если убрать протоколирование, то все сводится к коду, показанному в листинге 20.25. Листинг 20.25. Реализация примера с применением библиотеки recls void ClearDirectory(LPCTSTR lpszDir, LPCTSTR lpszFilePatterns) { typedef recls::stl::basic_search_sequence ffs_t; ffs_t files(lpszDir, lpszFilePatterns, recls::FILES); { for(ffs_t::const_iterator b = files.begin(); b != files.end(); ++b) { if(::DeleteFile((*b).c_str())) { ::SHChangeNotify(SHCNE_DELETE, SHCNF_PATH, (*b).c_str(), 0); } }} }
Глава 21. Интерлюдия: о компромиссе между эффективностью и удобством использования: обход каталогов на FTP-сервере Давать деньги и власть правительству – все равно, что давать виски и ключи от ма% шины подросткам. – П. Дж. О’Рурк API WinInet предлагает абстракцию протоколов Интернета и содержит ряд функций, упрощающих программирование. с использованием протоколов HTTP, FTP и Gopher. Сейчас для нас представляют особый интерес функции FtpFindFirstFile(), InternetFindNextFile() и InternetClose(), которые позволяют обойти ката лог на FTPсервере. Удобно, по крайней мере для пользователей Windows, что информация возвращается в структуре WIN32_FIND_DATA. Следовательно, можно написать код, представленный в листинге 21.1. Листинг 21.1. Перебор файлов на FTPZсервере с помощью функций из API WinInet HINTERNET WIN32_FIND_DATA HINTERNET
hConnection = . . . data; hFind = ::FtpFindFirstFile(hConnection, "*.*", &data , 0, 0);
if(NULL != hFind) { do { . . . // ×òî-òî ñäåëàòü ñ äàííûìè } while(::InternetFindNextFile(hFind, &data)); ::InternetCloseHandle(hFind); }
Это означает, что код, работающий с Windows API FindFirstFile/ FindNextFile для поиска в локальной файловой системе, можно повторно исполь зовать или адаптировать к поиску на удаленном FTPсервере. Основное различие состоит в том, что поиск инициируется относительно соединения, представленно го непрозрачным описателем типа HINTERNET. Ну а раз так, то вас не удивит тот
250
Наборы
факт, что в подпроекте InetSTL, который очень близок к подпроекту WinSTL, тоже есть компонент basic_findfile_sequence.
21.1. Класс inetstl::basic_findfile_sequence В данном случае мне не стыдно признаться, что определение класса basic_findfile_sequence получено в основном путем копирования и вставки, так как различия почти полностью инкапсулированы в классах filesystem_ traits из обоих проектов. Единственное расхождение в открытом интерфейсе,
который приведен в листинге 21.2, связано с тем, что конструкторам передается описатель открытого соединения. Листинг 21.2. Определение конструкторов класса basic_findfile_sequence // Â ïðîñòðàíñòâå èìåí inetstl template< typename C , typename X = throw_internet_exception_policy , typename T = filesystem_traits > class basic_findfile_sequence { . . . public: // Êîíñòðóèðîâàíèå basic_findfile_sequence(HINTERNET hconn , char_type const* pattern , flags_type flags = directories | files); basic_findfile_sequence(HINTERNET hconn , char_type const* directory , char_type const* pattern , flags_type flags = directories | files); basic_findfile_sequence(HINTERNET hconn , char_type const* directory , char_type const* patterns , char_type delim , flags_type flags = directories | files);
Несмотря на сходство обоих наборов функций поиска файлов (и шаблонных классов basic_findfile_sequence), в их семантике имеется существенное раз личие. В силу самой природы протокола FTP для данного соединения можно од новременно поддержать только один обход файловой системы. Попытка вызвать FtpFindFirstFile() второй раз вернет NULL, а GetLastError() при этом сооб щит об ошибке ERROR_FTP_TRANSFER_IN_PROGRESS. И так будет до тех пор, пока вы не закроете первый описатель поиска. Следовательно, код, приведенный в ли стинге 21.3, никогда не войдет во второй цикл. Листинг 21.3. Перебор файлов на FTPZсервере с помощью inetstl::findfile_sequence using inetstl::findfile_sequence; inetstl::session sess; inetstl::connection conn( sess.get(), "ftp.some-host-or-other.com"
Интерлюдия: о компромиссе
251
, INTERNET_INVALID_PORT_NUMBER, «anonymous» , NULL, INTERNET_SERVICE_FTP , INTERNET_FLAG_PASSIVE); findfile_sequence ffs(conn.get(), "/", "*.zip|*.bz", '|'); findfile_sequence::const_iterator b = ffs.begin(); findfile_sequence::const_iterator b2 = ffs.begin(); // Âñåãäà âîçâðàùàåò // îøèáêó for(; b != ffs.end(); ++b) {} for(; b2 != ffs.end(); ++b2) { . . . // Ñþäà íèêîãäà íå ïîïàäåì }
Для обработки этой ситуации мы возбуждаем исключение в закрытом стати ческом методе inetstl::basic_findfile_sequence::find_first_file_(), как показано в листинге 21.4. Листинг 21.4. Реализация inetstl::findfile_sequence ::find_first_ file() template HINTERNET basic_findfile_sequence:: find_first_file_( INTERNET hconn , char_type const* spec , flags_type /* flags */ , find_data_type* findData) { HINTERNET hSrch = traits_type::find_first_file( hconn, spec , findData); if(NULL == hSrch) { DWORD err = ::GetLastError(); if(ERROR_FTP_TRANSFER_IN_PROGRESS == err) { exception_policy_type()("Íà äàííîì ñîåäèíåíèè óæå èäåò îáõîä", err); } else { exception_policy_type()("Îøèáêà ïðè ïîèñêå", err); } } return hSrch; }
Если класс компилируется с отключенной обработкой исключений или пользователь параметризовал его нулевой политикой исключений, то метод begin() вернет корректно сформированный экземпляр итератора, эквивалентно го итератору end().
21.2. Класс inetstl::basic_ftpdir_sequence Хотя семантика класса inetstl::basic_findfile_sequence четко опреде лена и он успешно использовался много лет (в том числе и для поддержки FTP в библиотеке recls), имеется ограничение, которое в некоторых случаях оказыва
252
Наборы
ется обременительным. При работе с алгоритмами над целыми наборами (то есть такими, которые применяются к последовательности, а не к паре итераторов, см. том 2) ограничение единственного активного обхода пару раз приводило к пробле мам. Кроме того, в данном случае не следует забывать, что задержки при установ лении FTPсоединения и получении информации от FTPсервера намного пре вышают потенциальную неэффективность, связанную с копированием элементов в контейнер. Соблюдение баланса между эффективностью и удобством использования – решающий фактор при проектировании STLнаборов. Часто ради эффективности мы готовы несколько ограничить интерфейс, если приходится работать со слабы ми категориями итераторов и ссылок на элементы. Но в данном конкретном слу чае эффективность на стороне FTPклиента не имеет практического значения. А соображения удобства использования подсказывают, что неплохо было бы вос пользоваться кэшированием, дабы обойти ограничительную, а иногда и трудно предсказуемую семантику единственного активного обхода. Поэтому теперь в подпроект InetSTL включен также класс basic_ftpdir_sequence, который ре комендуется применять в прикладных программах вместо basic_findfile_ sequence. Этот класс хранит список элементов во внутреннем объекте vector, который заполняется в конструкторах с помощью локального объекта basic_ findfile_sequence. Полное определение приведено в листинге 21.5 (кроме реа лизаций второго и третьего конструкторов, которые очень похожи на реализацию первого). Листинг 21.5. Определение класса basic_ftpdir_sequence //  ïðîñòðàíñòâå èìåí inetstl template< typename C , typename X = throw_internet_exception_policy , typename T = filesystem_traits > class basic_ftpdir_sequence { private: // Òèïû-÷ëåíû typedef basic_findfile_sequence sequence_type_; public: typedef typename sequence_type_::char_type char_type; typedef typename sequence_type_::value_type value_type; typedef typename sequence_type_::size_type size_type; typedef int flags_type; private: typedef std::vector values_type_; public: typedef typename values_type_::const_iterator const_iterator; typedef typename values_type_::const_reverse_iterator const_reverse_iterator; public: // Êîíñòàíòû-÷ëåíû enum search_flags { includeDots = sequence_type_::includeDots
Интерлюдия: о компромиссе
253
, directories = sequence_type_::directories , files = sequence_type_::files }; public: // Êîíñòðóèðîâàíèå basic_ftpdir_sequence(HINTERNET hconn , char_type const* pattern , flags_type flags = directories | files) { sequence_type_ ffs(hconn, pattern, flags); std::copy(ffs.begin(), ffs.end(), std::back_inserter(m_values)); } basic_ftpdir_sequence(HINTERNET hconn , char_type const* directory , char_type const* pattern , flags_type flags = directories | files); basic_ftpdir_sequence(HINTERNET hconn , char_type const* directory , char_type const* patterns , char_type delim , flags_type flags = directories | files); public: // Èòåðàöèÿ const_iterator begin() const { return m_values.begin(); } const_iterator end() const { return m_values.end(); } const_reverse_iterator rbegin() const { return m_values.rbegin(); } const_reverse_iterator rend() const { return m_values.rend(); } public: // Ðàçìåð size_type size() const { return m_values.size(); } bool empty() const { return m_values.empty(); } private: // Ïåðåìåííûå-÷ëåíû values_type_ m_values; }; typedef basic_ftpdir_sequence ftpdir_sequence_a; typedef basic_ftpdir_sequence<wchar_t> ftpdir_sequence_w; typedef basic_ftpdir_sequence ftpdir_sequence;
В этом классе есть методы size(), empty(), а также прямой и обратный ите раторы. Поскольку значения хранятся в векторе, то итератор непрерывный, а так как набор неизменяемый, то ссылки на элементы фиксированные. Такой класс как танк – не очень быстрый, но неубиенный.
Глава 22. Перебор процессов и модулей Никогда не позволяйте своему этическому чувству мешать делать то, что вы считае% те правильным. – Айзек Азимов Это моя игрушка. Если попробуешь ее от% нять, мне придется тебя съесть. – Кошка, Красный карлик API состояния процессов (PSAPI) в Windows предлагает несколько функций для доступа к состоянию системы. Для процессов простейшим и наиболее упот ребительным является функция EnumProcesses(), определенная следующим образом: BOOL EnumProcesses( DWORD* , DWORD , DWORD*
pProcessIds cb pBytesReturned);
Здесь pProcessIds указывает на массив, в котором будут возвращены иден тификаторы процессов, работающих в системе; cb – размер этого массива в бай тах; pBytesReturned – указатель на переменную, в которой возвращается число помещенных в массив байтов. Количество возвращенных идентификаторов мо жет быть меньше числа реально работающих процессов, если размер массива не достаточен, поэтому рекомендуется вызывать функцию повторно с увеличенным размером массива, если оказалось, что *pBytesReturned == cb. Если не считать этого нюанса, то функция достаточно проста. Мы обернем ее в STLнабор pid_ sequence. PSAPI предоставляет также функцию EnumProcessModules() для перебора модулей в данном процессе: BOOL EnumProcessModules(HANDLE hProcess , HMODULE* lphModule , DWORD cb , DWORD *lpcbNeeded);
Семантика в точности аналогична EnumProcesses(). Эту функцию мы обер нем в STLнабор process_module_sequence.
Перебор процессов и модулей
255
22.1. Характеристики набора Рассмотрим, какие характеристики набора обусловлены семантикой выше упомянутых функций. Понятие изменяемой последовательности процессов лишено смысла, и API не предоставляет средств для модификации, поэтому отбрасываем сразу. Набор будет неизменяемым. В любой момент процесс может быть завершен и удален из списка актив ных; клиентский код, вызвавший функцию EnumProcesses(), повлиять на это не может. На самом деле, тот факт, что список активных процессов воз вращается целиком, подчеркивает, что клиент должен трактовать этот спи сок как мгновенный снимок состояния системы. Набор, обертывающий функцию EnumProcesses(), естественно, должен владеть массивом идентификаторов процессов. Так как набор неизменяемый, его итераторы являются неизменяющими. Обертываемый API заполняет предоставленный вызывающей программой массив, поэтому класс набора может просто завести внутренний буфер. Сле довательно, итераторы могут быть представлены константными указателя ми на этот массив, и, стало быть, являются непрерывными (раздел 2.3.6). Итератор неизменяющий и непрерывный; поэтому при условии, что набор реализует идиому неизменяющего RAII (раздел 11.1), ссылки могут быть фиксированными и неизменяющими (раздел 3.3.2).
22.2. Класс winstl::pid_sequence Наверное, это простейший из всех полностью функциональных классов рас ширений STL, рассматриваемых в этой книге. Полное определение приведено в листинге 22.1. Листинг 22.1. Объявление класса pid_sequence //  ïðîñòðàíñòâå èìåí winstl class pid_sequence { public: // Òèïû-÷ëåíû typedef DWORD value_type; typedef processheap_allocator allocator_type; typedef pid_sequence class_type; typedef value_type const* const_pointer; typedef value_type const* const_iterator; typedef value_type const& const_reference; typedef size_t size_type; typedef ptrdiff_t difference_type; typedef std::reverse_iterator const_reverse_iterator; public: // Êîíñòðóèðîâàíèå pid_sequence(); pid_sequence(class_type const& rhs);
256
Наборы
~pid_sequence() throw(); public: // Èòåðàöèÿ const_iterator begin() const; const_iterator end() const; const_reverse_iterator rbegin() const; const_reverse_iterator rend() const; public: // Äîñòóï ê ýëåìåíòàì const_reference operator [](size_type index) const; public: // Ðàçìåð bool empty() const; size_type size() const; private: // Ïåðåìåííûå-÷ëåíû typedef stlsoft::auto_buffer< value_type , 64, allocator_type > buffer_type_; buffer_type_ m_pids; private: // Íå ïîäëåæèò ðåàëèçàöèè class_type& operator =(class_type const&); };
Такой код пишется почти по трафарету. Отметить стоит следующие особен ности: в качестве распределителя памяти мы применяем класс processheap_ allocator, а не std::allocator. Это упрощает использо вание данного класса в небольших компонентах для Windows, которые не нуждаются в связывании со стандартной библиотекой; имеется конструктор копирования, который позволяет сохранять копии мгновенных снимков; выделение памяти для буфера и изменение его размера управляется клас сом stlsoft::auto_buffer (раздел 16.2). Выбран размер, достаточный для хранения 64 элементов (256 байтов), поэтому в большинстве случаев выделять память из кучи не потребуется.
22.2.1. Простые реализации на основе композиции Хотя класс auto_buffer и не является STLконтейнером (раздел 2.2), тем не менее он предоставляет многие методы контейнеров для удобства реализации на боров. Поэтому большую часть методов pid_sequence можно реализовать непос редственно в терминах одноименных методов auto_buffer, что положительно сказывается на простоте pid_sequence. Например, метод empty() возвращает m_pids.empty(): bool pid_sequence::empty() const { return m_pids.empty(); }
То же самое относится к методам size(), begin(), end(), rbegin() и rend(). Также можно было бы реализовать и оператор индексирования, но я решил явно проверить предусловие в методе pid_sequence::operator [](), а не полагаться
Перебор процессов и модулей
257
на такую же проверку в auto_buffer::operator [](). Лучше обнаружить нару шение предусловия как можно раньше. Поэтому реализация выглядит так: const_reference pid_sequence::operator [](size_type index) const { WINSTL_MESSAGE_ASSERT("Èíäåêñ âíå äèàïàçîíà", index < size()); return m_pids[index]; }
Совет. Помещайте проверку предусловий как можно ближе к открытому интерфейсу класса.
22.2.2. Получение идентификаторов процессов Осталось реализовать только конструктор по умолчанию и конструктор копи рования. Проще всего обстоит дело с последним: pid_sequence::pid_sequence(pid_sequence const& rhs) : m_pids(rhs.m_pids.size()) { std::copy(rhs.m_pids.begin(), rhs.m_pids.end(), m_pids.begin()); }
Отметим, что член m_pids конструируемого экземпляра инициализируется значением m_pids исходного экземпляра. Поскольку в классе auto_buffer не оп ределен (специально) конструктор копирования, элементы нужно скопировать из одного экземпляра в другой явно. (Тут один из наиболее проницательных рецен зентов высказал некоторое раздражение отсутствием такой функциональности, заметив, что «если бы в auto_buffer был конструктор копирования, то класс pid_sequence вообще не понадобился бы!». Я уважаю, но не разделяю его точку зрения. Для меня весьма важно, чтобы auto_buffer даже не намекал на семанти ку, которой не предоставляет.) Ну и напоследок самая суть этого класса – его конструктор по умолчанию: Листинг 22.2. Конструктор по умолчанию pid_sequence::pid_sequence() : m_pids(buffer_type_::internal_size()) { DWORD cbReturned; for(;;) { if(!::EnumProcesses(&m_pids[0], sizeof(value_type) * m_pids.size() , &cbReturned)) { throw system_exception( "Íå ìîãó ïåðåáðàòü ïðîöåññû" , ::GetLastError()); } else
Наборы
258 { const size_t n = cbReturned / sizeof(value_type); if(n < m_pids.size()) { m_pids.resize(n); break; } else { const size_type size = m_pids.size(); m_pids.resize(1); m_pids.resize(2 * size); } } } }
После инициализации фактический размер m_pids равен внутреннему разме ру, то есть максимальному, который можно получить без обращения к куче. Вы зов EnumProcesses() производится в цикле, с удвоением размера m_pids на каж дой итерации. Если EnumProcesses() возвращает ошибку, возбуждается исключение. В случае успешного завершения возвращенное EnumProcesses() число проверяется, чтобы понять, занят ли весь буфер m_pids. Если да, то вполне возможно, что получены не все идентификаторы процессов, поэтому цикл повто ряется с удвоенным размером буфера. В противном случае можно выходить из цикла в уверенности, что возвращены все идентификаторы. Отметим, что для подобных API набор заведомо предпочтительнее итератора, так как потенциально накладный системный вызов для получения идентифика торов процессов выполняется единожды, после чего обходить последователь ность можно сколько угодно раз, и это не будет стоить ничего, кроме инкременти рования указателя. Возможно, вам непонятно, зачем нужно предложение m_pids.resize(1). Оно было добавлено для того, чтобы устранить мелкую неэффективность, кото рую заметил проницательный Ади Шавит, просматривая ранний вариант рукопи си. Сможете ли вы понять, в чем проблема, и разобраться, как предложенное ре шение ее устраняет. Ответы, пожалуйста, присылайте на почтовой открытке.
22.2.3. Работа без поддержки исключений Чтобы можно было работать в контекстах, где не производится компоновка со стандартной библиотекой, например в облегченных COMсерверах, которые свя зываются только с системными библиотеками Windows, этот класс написан так, чтобы сохранять функциональность и без поддержки исключений. Настоящая реализация приведена в листинге 22.3. Листинг 22.3. Пересмотренная реализация конструктора по умолчанию pid_sequence::pid_sequence() : m_pids(buffer_type_::internal_size()) {
Перебор процессов и модулей
259
. . . if(!::EnumProcesses(&m_pids[0], sizeof(value_type) * m_pids.size() , &cbReturned)) { #ifdef STLSOFT_CF_EXCEPTION_SUPPORT throw system_exception( "Íå ìîãó ïåðåáðàòü ïðîöåññû" , ::GetLastError()); #else /* ? STLSOFT_CF_EXCEPTION_SUPPORT */ m_pids.resize(0); break; #endif /* STLSOFT_CF_EXCEPTION_SUPPORT */ } else { . . . m_pids.resize(1); if(!m_pids.resize(2 * size)) { m_pids.resize(0); break; } . . .
Если EnumProcesses() завершается с ошибкой или не удается выделить па мять, то размер m_pids сбрасывается в нуль. В этом случае метод pid_sequence:: size() вернет 0, и клиентский код сможет узнать, что случилось от функции Windows GetLastError(). Конечно, такое использование нельзя назвать нор мальным, но происходит это достаточно часто, чтобы оправдать скромные до полнительные усилия.
22.3. Класс winstl::process_module_sequence Реализация класса process_module_sequence практически повторяет pid_ sequence с тем отличием, что значение имеет тип HMODULE, а не DWORD, и отсут ствует конструктор по умолчанию. Вместо этого предоставляется конструктор, принимающий описатель процесса HANDLE, который передается функции EnumProcessModules(), как показано в листинге 22.4. Листинг 22.4. Тип значения и конструктор преобразования для класса process_module_sequence //  ïðîñòðàíñòâå èìåí winstl class process_module_sequence { public: // Òèïû-÷ëåíû . . . typedef HMODULE value_type; . . . public: // Êîíñòðóèðîâàíèå explicit process_module_sequence(HANDLE hProcess); . . .
Наборы
260
22.4. Перебор всех модулей в системе Имея оба эти класса, совсем нетрудно получить список всех модулей во всех активных процессах. Чтобы найти имя модуля в конкретном процессе, мы пользу емся функцией PSAPI GetModuleFileNameEx(), для чего служит небольшая вспомогательная функция get_module_path(), показанная в листинге 22.5. Так как длина путей к модулям в Windows ограничена константой _MAX_PATH, мы мо жем воспользоваться шаблонным классом stlsoft::basic_static_string, рас смотренным в разделе 19.3.11. Листинг 22.5. Вспомогательная функция get_module_path() stlsoft::basic_static_string get_module_path(HANDLE hProcess, HMODULE hModule) { stlsoft::basic_static_string s("", _MAX_PATH); DWORD cch = ::GetModuleFileNameEx(hProcess, hModule , &s[0], _MAX_PATH); if(0 == cch) { throw winstl::system_exception( "Íå ìîãó ïîëó÷èòü ïóòü" , ::GetLastError()); } s.resize(cch); return s; }
В предположении, что у пользователя, запускающего программу из листин га 22.6, достаточно привилегий, она выведет путь к каждому исполняемому про цессу и пути загруженных в него модулей. (Для краткости я опустил обработку ошибок и закрытие описателя процесса hProcess.) Листинг 22.6. Пример программы перебора процессов и модулей #include <winstl/system/pid_sequence.hpp> #include <winstl/system/process_module_sequence.hpp> int main() { using winstl::pid_sequence; using winstl::process_module_sequence; pid_sequence pids; pid_sequence::const_iterator b = pids.begin(); for(; b != pids.end(); ++b) { HANDLE hProcess = ::OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ , false, *b); std::cout = rhs.m_threshold; } else
Наборы
276 { return rhs.m_i0 >= m_threshold; } } }
Более гибким был бы класс, в котором учтены обе модели использования. Но попытка написать его наталкивается на проблему: как избегнуть неоднозначнос ти при конструировании последовательности для разных сценариев. Можно было бы включить конструктор с двумя параметрами: . . . public: // Êîíñòðóèðîâàíèå Fibonacci_sequence(size_t n, value_type limit); . . .
Если конец диапазона не задается, то второй параметр имеет некое предопре деленное значение, например: Fibonacci_sequence(0, 10000); Fibonacci_sequence(20, 0);
// Âåðõíÿÿ ãðàíèöà ðàâíà 10 000 // Ïîñëåäîâàòåëüíîñòü èç 20 ýëåìåíòîâ
Очевидно, что такой подход далек от элегантности и чреват ошибками. Чуть менее отталкивающий вариант – задать способ прекращения вычислений с помо щью перечисления и использовать как для верхней границы, так и для количества элементов параметр типа value_type: . . . public: // Êîíñòàíòû-÷ëåíû enum LimitType { thresholdLimit, countLimit }; public: // Êîíñòðóèðîâàíèå Fibonacci_sequence(value_type limit, LimitType type); . . .
23.5.3. Истинные typedef’ы Самое лучшее решение основано на использовании истинных typedef’ов (раз дел 12.3), которые обеспечивают однозначную перегрузку по схожим или даже идентичным типам. Так мы и поступили в окончательной реализации класса Fibonacci_sequence, которая показана в листинге 23.11. (Ему соответствует файл Fibonacci_sequence_7.hpp на компактдиске.) Обратите внимание на проверку предусловий в обоих конструкторах. Допустимо также возбуждать ис ключение std::out_of_range (поскольку заранее неизвестно, какое значение за даст пользователь). Листинг 23.11. Версия 7: объявление самого класса и характеристического класса template struct Fibonacci_traits; template
Числа Фибоначчи struct Fibonacci_traits { static const uint32_t maxThreshold static const size_t maxLimit }; template struct Fibonacci_traits { static const uint64_t maxThreshold static const size_t maxLimit };
277
= 2971215073; = 47;
= 12200160415121876738; = 93;
class Fibonacci_sequence { public: // Òèïû-÷ëåíû typedef ?? uint32_t or uint64_t ?? value_type; typedef Fibonacci_traits traits_type; typedef true_typedef<size_t, unsigned> limit; typedef true_typedef threshold; class const_iterator; public: // Êîíñòðóèðîâàíèå explicit Fibonacci_sequence(limit l = limit(traits_type::maxLimit)) : m_limit(l.base_type_value()) , m_threshold(0) { STLSOFT_MESSAGE_ASSERT( "Ñëèøêîì áîëüøàÿ âåðõíÿÿ ãðàíèöà" , l (arr.GetSize()) , sizeof(CString), compare_CStrings); // 3. Ñêîïèðîâàòü ñòðîêè â ñïèñîê lst.RemoveAll(); { for(int i = 0; i < arr.GetSize(); ++i) { lst.AddTail(arr[i]); }} } char const *fileName = . . . CStringList lines; readOrderedLines(fileName, lines); { for(POSITION pos = lines.GetHeadPosition(); NULL != pos; ) { std::cout 0), åñëè åãî åùå íåò!
и что этот оператор нельзя применять к константным экземплярам: int lookup(std::map const& m, int key) { return m[key]; // îøèáêà êîìïèëÿöèè, íàðóøåíà ãàðàíòèÿ const! }
Я попрежнему считаю, что оператор индексирования должен быть неизменя ющим (const) методом и возбуждать исключение, если запрошенного элемента нет в контейнере. В случае системного окружения разумно предположить, что чаще всего пользователям нужно искать значение, а не вставлять новое. Поэтому первое приближение к разрабатываемому классу могло бы выглядеть так:
326
Наборы
Листинг 25.3. Первая версия класса environment_map // Â ïðîñòðàíñòâå èìåí platformstl class environment_map { public: // Äîñòóï ê ýëåìåíòàì char const* operator [](char const* name) const { char const* value = ::getenv(name); if(NULL == value) { throw std::out_of_range("ïåðåìåííàÿ íå ñóùåñòâóåò"); } return value; } };
Применить этот метод можно следующим образом: environment_map env; std::cout { public: // Òèïû-÷ëåíû typedef zorder_iterator class_type; public: // Êîíñòðóèðîâàíèå zorder_iterator() : m_hwnd(NULL) {} explicit zorder_iterator(HWND hwnd) : m_hwnd(hwnd) {} public: // Ìåòîäû ïðÿìîé èòåðàöèè HWND operator *() const { return m_hwnd; } class_type& operator ++() { WINSTL_ASSERT(NULL != m_hwnd); m_hwnd = ::GetWindow(m_hwnd, GW_HWNDNEXT); } class_type& operator ++(int); // Êàíîíè÷åñêàÿ ôîðìà bool equal(class_type const& rhs) const { return m_hwnd == rhs.m_hwnd; } private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; };
Как видно из типов, заданных при специализации шаблона std::iterator, итератор zorder_iterator возвращает временные по значению ссылки на элемен ты. В данном случае можно было бы легко поддержать чувствительные ссылки (раздел 3.3), поскольку тип значения – это состояние итерации. Я остановился на временных по значению, потому что мне кажется, что возвращать HWND лучше, чем HWND&, так как второй вариант не слишком осмыслен. В разделе 26.5 при обсужде нии порчи итератора извне мы увидим, что от выбора категории ссылок на эле менты может многое зависеть.
26.3.2. Класс window_peer_sequence, версия 1 При таком определении интерфейса zorder_iterator класс window_peer_ sequence можно было бы реализовать, как показано в листинге 26.5. Листинг 26.5. Первоначальная реализация window_peer_sequence //  ïðîñòðàíñòâå èìåí winstl class window_peer_sequence { public: // Òèïû-÷ëåíû typedef zorder_iterator
iterator;
Наборы
358
typedef window_peer_sequence class_type; public: // Êîíñòðóèðîâàíèå window_peer_sequence(); explicit window_peer_sequence(HWND hwnd) : m_hwnd(hwnd) {} public: // Èòåðàöèÿ iterator begin() const { return iterator(::GetWindow(m_hwnd, GW_HWNDFIRST)); // Ïåðâîå îêíî } iterator end() const { return iterator(); } public: // Ðàçìåð bool empty() const; private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; };
Это определение удовлетворяет требованиям, предъявляемым в листинге 26.2, и обладает требуемым поведением. Однако оно не поддерживает обратную итерацию. А для этого нам понадобится обеспечить семантику двунаправленного итератора.
26.4. Версия 2: двунаправленная итерация Функция GetWindow() предлагает двунаправленный API, поэтому на первый взгляд никаких сложностей в добавлении к zorder_iterator семантики двунап равленности не ожидается. Но не все так просто, как кажется. Ко всему прочему двунаправленный итератор должен (1) уметь декрементировать концевой экзем пляр и (2) если диапазон не пуст, допускать разыменование. Другими словами, поведение следующего кода должно быть определено: some_container some_container::iterator some_container::iterator
cont; b = cont.begin(); e = cont.end();
if(b != e) { —e; // Ýòî îñìûñëåííàÿ îïåðàöèÿ . . . *b; // . . . è ýòà òîæå }
Тут возникает небольшое осложнение. Обратите внимание, что в листинге 26.5 есть единственная переменнаячлен – m_hwnd. В концевой точке она будет равна NULL. А, стало быть, оператор декремента не сможет вернуться на последний эле мент диапазона, поскольку для этого необходимо передать функции GetWindow() действительный описатель окна (вместе с флагом GW_HWNDLAST). Следовательно, в двунаправленном zorder_iterator должно быть две переменныечлена: одна для текущего контекста, а другая – ссылка, с помощью которой можно добраться до последнего элемента. Модифицированная версия показана в листинге 26.6.
Путешествие по Z8плоскости
359
Листинг 26.6. Модифицированная версия zorder_iterator class zorder_iterator : public std::iterator< std::bidirectional_iterator_tag , HWND, ptrdiff_t, void, HWND> { . . . public: // Êîíñòðóèðîâàíèå zorder_iterator(HWND hwnd, HWND hwndRoot) : m_hwnd(hwnd) , m_hwndRoot(hwndRoot) // Èñïîëüçóåòñÿ äëÿ äîñòóïà ê ïîñëåäíåìó îêíó {} public: // Ìåòîäû äâóíàïðàâëåííîé èòåðàöèè HWND operator *() const; class_type& operator ++(); class_type& operator ++(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ class_type& operator —() { if(NULL != m_hwnd) { m_hwnd = ::GetWindow(m_hwnd, GW_HWNDPREV); } else { m_hwnd = ::GetWindow(m_hwndRoot, GW_HWNDLAST); } } class_type& operator —(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ bool equal(class_type const& rhs) const; private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; HWND m_hwndRoot; };
В классе window_peer_sequence эти изменения учтены, как показано в лис тинге 26.7. Экземпляру итератора, который возвращает begin(), передается флаг поиска первого среди равноправных окон, а также описатель m_hwnd отправного окна на случай, если придется декрементировать концевой итератор. В принципе вместо m_hwnd можно передать описатель любого равноправного окна, так как для поиска с флагом GW_HWNDLAST это безразлично. Как и рекомендовано в стандарте, типчлен reverse_iterator определен в терминах std::reverse_iterator, а методы rbegin() и rend() передают в конструкторы обратного итератора соот ветственно итераторы end() и begin(). Другими словами, rbegin() возвращает адаптированную форму end(), а rend() – адаптированную форму begin(). Листинг 26.7. Модифицированная версия window_peer_sequence class window_peer_sequence { public: // Òèïû-÷ëåíû typedef zorder_iterator typedef std::reverse_iterator
iterator; reverse_iterator;
360
Наборы
typedef window_peer_sequence class_type; public: // Êîíñòðóèðîâàíèå explicit window_peer_sequence(HWND hwnd) : m_hwnd(hwnd) {} public: // Èòåðàöèÿ iterator begin() const { return iterator(::GetWindow(m_hwnd, GW_HWNDFIRST), m_hwnd); } iterator end() const; reverse_iterator rbegin() const { return reverse_iterator(end()); } reverse_iterator rend()const { return reverse_iterator(begin()); } private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; };
Примечание. Если у вас возникло подозрение относительно того, все ли здесь в порядке с методом end(), запишите на свой счет 50 баллов. Я сознательно не привел его реализа) цию, так как в ней есть скользкое место, которое я приберег на потом, когда мы усядемся вокруг костра, достанем губную гармошку и споем блюз о двунаправленных итераторах.
26.5. Учет внешних изменений Поклонники Windows должны запаниковать, увидев это определение, так как они знают, что Zпорядок может изменяться динамически. Если дочерние окна, например диалоги, обычно остаются там, куда их поместили, то окна верхнего уровня в любой момент могут быть перемещены либо программой, либо пользова телем, которому достаточно просто щелкнуть по окну или нажать комбинацию клавиш AltTab. Во всех рассмотренных выше наборах либо элементы (сами или их копии) на ходятся под контролем набора (glob_sequence, глава 17; pid_sequence и process_module_sequence, глава 22; Fibonacci_sequence, глава 23; CArray_ cadaptor и CArray_iadaptor, глава 24), либо абстракция API гарантирует, что изменения, произведенные другими потоками или процессами, не приведут ни к сбою в процессе обхода, ни к аномальному поведению (readdir_sequence, гла ва 19; findfile_sequence, глава 20). Исключение составляет класс environment_ map (глава 35), в котором мы избежали рассогласования путем кэширования со держимого в короткоживущих мгновенных снимках; при этом мы заранее догово рились, что этот набор можно использовать только в одном потоке. Однако в дан ном случае вполне может случиться, что перебор будет прерван в результате
Путешествие по Z8плоскости
361
действий других процессов, которые мы не можем ни контролировать, ни игнори ровать, а сам обертываемый API не дает никакой защиты. Связано это с тем, что окно, описатель которого хранится в данный момент в итераторе, может быть уничтожено; скажем, это окно проигрывателя DVDдисков, которое вы решили закрыть, чтобы уделить все внимание чтению книги «Расширение STL». Тогда вызов GetWindow() закончится с ошибкой и вернет NULL. Таким образом, дей ствие, произошедшее вне процесса, сделало итератор недействительным. Подоб ные проблемы в классической библиотеке STL даже не затрагиваются; нам необ ходимо искать другой подход.
26.5.1. Класс stlsoft::external_iterator_invalidation Может случиться, что описатель, переданный функции GetWindow(), отно сится к уже не существующему окну. Мы должны распознать эту ситуацию и предпринять соответствующие действия. GetWindow() может возвращать NULL по двум причинам: мы дошли до последнего окна в Zпорядке или окна с указан ным описателем больше нет. Выяснить, что произошло, можно обратившись к функции GetLastError(), которая возвращает ERROR_SUCCESS (0) в первом случае, или другой код, обычно ERROR_INVALID_WINDOW_HANDLE, – во втором. Та ким образом, порча итератора извне обнаруживается с помощью такого кода: zorder_iterator& zorder_iterator::operator ++() { . . . m_hwnd = ::GetWindow(m_hwnd, GW_HWNDNEXT); if( NULL == m_hwnd && ERROR_SUCCESS != ::GetLastError()) { . . . // Îáíàðóæåíà ïîð÷à èçâíå } }
Вопрос в том, что при этом делать. Поскольку итератор хранит только теку щую точку перебора, а она теперь недействительна и потому бесполезна, мы не можем вернуться на шаг назад и повторить попытку. Зато мы можем начать обход с начала, вызвав GetWindow(m_hwndRoot, GW_HWNDFIRST). Вполне может стать ся, что, хотя внешнее событие не сделало хранящийся в итераторе описатель не действительным, тем не менее порядок окон изменился, вследствие чего одно и то же окно может встретиться при обходе более одного раза. Клиентский код должен знать об этой ситуации – еще одна дырявая абстракция (глава 6) – и быть готов ее обработать. Поэтому можно утверждать, что возобновление обхода с начала – до статочно разумный подход. Причина, по которой я так не поступил, – это не крайне маловероятная воз можность зацикливания (которую можно вообще не принимать во внимание). Дело, скорее, в том, что так мы ничего не выигрываем, зато отнимаем у програм миста шанс самому решить, что делать в такой ситуации. Он мог бы начать обход заново, но мог бы и попросить пользователя перестать дергать окна, пока про грамма работает. Решать должен программист. Поэтому я решил возбуждать ис
362
Наборы
ключение типа stlsoft::external_iterator_invalidation, интерфейс кото рого показан ниже. Это исключение относится к категории нежелательных, но допустимых воздействий на содержимое STLнабора в результате работы внеш них компонентов, то есть других процессов или самой операционной системы. // Â ïðîñòðàíñòâå èìåí stlsoft class external_iterator_invalidation : public iteration_interruption { . . . public: // Êîíñòðóèðîâàíèå external_iterator_invalidation(); explicit external_iterator_invalidation(char const* message); external_iterator_invalidation(char const* message, long errorCode); . . . };
Итак, если оператор инкремента или декремента обнаружит, что описатель окна стал недействителен, то возбуждается исключение типа external_ iterator_invalidation: zorder_iterator& zorder_iterator::operator ++() { . . . m_hwnd = ::GetWindow(m_hwnd, GW_HWNDNEXT); if( NULL == m_hwnd && ERROR_SUCCESS != ::GetLastError()) { throw stlsoft::external_iterator_invalidation("îøèáêà ïðè îáõîäå â z-ïîðÿäêå: îêíî óíè÷òîæåíî", static_cast(dwErr)); } }
Преимущество этого механизма по сравнению с анализом вручную состоит в том, что исключение невозможно игнорировать. Мы могли бы забыть вызвать GetLastError() и сравнить возвращенный код с ERROR_INVALID_WINDOW_ HANDLE, но забыть об исключении, которое возбуждает вызванная функция, про сто так не получится. Вызывающая программа обязана предпринять какието действия. Это тот случай, когда преимущество исключений над кодами ошибок очевидно.
26.6. Класс winstl::child_window_sequence Теперь настало время познакомиться с классом child_window_sequence (ли стинг 26.8). Он совпадает с классом window_peer_sequence во всех отношениях, кроме одного. Листинг 26.8. Определение класса child_window_sequence // Â ïðîñòðàíñòâå èìåí winstl class child_window_sequence
Путешествие по Z8плоскости
363
{ public: // Òèïû-÷ëåíû typedef zorder_iterator iterator; typedef std::reverse_iterator reverse_iterator; typedef child_window_sequence class_type; public: // Êîíñòðóèðîâàíèå explicit child_window_sequence(HWND hwnd) : m_hwnd(::GetWindow(hwnd, GW_CHILD)) {} public: // Èòåðàöèÿ iterator begin() const; iterator end() const; reverse_iterator rbegin() const; reverse_iterator rend() const; public: // Ðàçìåð bool empty() const; private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; };
Очевидно, есть куча способов избавиться от дублирования кода, но их мы рас смотрим ниже. А сначала займемся парочкой ошибок, связанных с двунаправлен ной природой zorder_iterator, одна из которых критична. Для их устранения придется значительно переработать конструкторы класса zorder_iterator, и это будет нашей первоочередной задачей.
26.7. Блюз, посвященный двунаправленным итераторам 26.7.1. О стражах end() Чтобы поддержать семантику итераторов ввода и однонаправленных итерато% ров (раздел 1.3), концевой итератор не должен делать ничего особенного; у него даже состояния может не быть. (На практике удобнее, чтобы и этот экземпляр знал об исходном наборе, поскольку это помогает отлавливать ошибки, как мы видели в разделе 20.5.) Таким образом, следующие экземпляры логически эквивалентны: some_collection c; some_collection::iterator it; some_collection::iterator e = c.end(); assert(e == it);
Однако, если речь идет о двунаправленных итераторах, то такое поведение концевого итератора неприемлемо. Правило. Для любого набора типа C, обладающего двунаправленными итераторами типа I, соотношение C().end() == C::I() никогда не выполняется.
Другими словами: some_collection c; some_collection::iterator it;
364
Наборы
some_collection::iterator e = c.end(); assert(e != it);
Причина в том, что концевой двунаправленный итератор должно быть разре шено декрементировать, чтобы вернуться к перебираемому диапазону. Иначе го воря, следующий код должен быть корректен: some_collection c; some_collection::iterator b = c.begin(); some_collection::iterator e = c.end(); if(b != e) { *--e; // Äîïóñòèìîå è ïðåäñêàçóåìîå ïîâåäåíèå }
Это скользкое место поджидает программистов, отважившихся на расшире ние STL, а особенно тех, кто раньше поддерживал только наборы с итераторами ввода или однонаправленными итераторами. Напомним, что в исходной версии класса window_peer_sequence метод end() был реализован так: iterator window_peer_sequence::end() const { return iterator(); }
Поскольку метод zorder_iterator::equal() определяет равенство двух эк земпляров на основе сравнения членов m_hwnd, это означает, что код будет работать правильно, пока мы обходим набор в прямом направлении; все автономные тесты в этом случае проходят. Увы, я не стал писать тесты для обратного обхода, и ошибка так и осталась необнаруженной. Когда спустя некоторое время я воспользовался этим компонентом для обхода в обратном направлении, выяснилось, что он не рабо тает. Вообще ничего не происходило! В реализации zorder_iterator::operator — () не было проверки предусловия m_hwndRoot != NULL (теперь есть). Поэтому декремент концевого итератора возвращал тот же самый концевой итератор – если передать GetWindow() NULL, то она и вернет NULL, – и ошибка не вызывала никаких видимых эффектов. Обнаружив это, я тут же добавил проверку предусловия и пересмотрел опре деление метода end() в классах window_peer_sequence и child_window_ sequence. Оно стало выглядеть так: iterator window_peer_sequence::end() const { return iterator(NULL, m_hwnd); }
Теперь у экземпляра итератора есть ненулевой m_hwndRoot, с помощью кото рого можно получить последнее окно, передав флаг GW_HWNDLAST.
26.7.2. Убийственное двойное разыменование Вторая ошибка очень тонкая, но, увы, она означает, что нынешнее определе ние zorder_iterator безнадежно ошибочно. Традиционный способ реализации
Путешествие по Z8плоскости
365
обратных итераторов заключается в использовании шаблонного класса адаптера итератора std::reverse_iterator (C++03: 24.1.1.1). Каждому экземпляру об ратного итератора соответствует эквивалентный экземпляр прямого итератора, который называется базовым. Важно понимать, что экземпляр обратного итератора и экземпляр его базового итератора ссылаются на разные места в наборе. Стандарт (C++03: 24.4.1;1) определяет соотношение между обратным и соответствующим ему прямым итератором в виде следующего тождества &*(reverse_iterator (it)) == &*(it - 1). Среднему человеку это ни о чем не говорит, поэтому проил люстрирую его примером кода. Рассмотрим следующую специализацию вектора и четыре соответствующих ей итератора: std::vector v; v.push_back(1); v.push_back(2); v.push_back(3); std::vector::iterator std::vector::iterator std::vector::reverse_iterator std::vector::reverse_iterator
b = e = rb = re =
v.begin(); v.end(); v.rbegin(); v.rend();
Четыре экземпляра итератора обладают следующими характеристиками: b ссылается на v[0] и *b == 1; e ссылается на v[3], который, конечно же, не существует, и потому разыме новывать e нельзя; rb хранит экземпляр базового типа (std::vector::iterator), эк вивалентный экземпляру e, который ссылается на элемент v[3] (не суще ствующий). Но говорят, что rb ссылается на e - 1, который равен v[2], поэтому разыменование возможно и *rb == 3; re хранит экземпляр базового типа (std::vector::iterator), эк вивалентный экземпляру b, который ссылается на элемент v[0] (суще ствующий). Но говорят, что re ссылается на b - 1, который равен элементу v[-1], которого, разумеется, не существует. Следовательно, re нельзя ра зыменовывать. Как же работает std::reverse_iterator? Он хранит фактический экземп ляр итератора своего базового типа и реализует собственные методы через методы этого экземпляра. В листинге 26.9 приведен фрагмент реализации std::reverse_ iterator. Из кода operator *() ясно видно, как реализуется это смещение на единицу между обратным итератором и его базовым экземпляром. Листинг 26.9. Определение std::reverse_iterator //  ïðîñòðàíñòâå èìåí std template class reverse_iterator : public iterator { public: // Òèïû-÷ëåíû typedef I iterator_type; typedef reverse_iterator class_type; public: // Êîíñòðóèðîâàíèå reverse_iterator(iterator_type base) : m_base(base) {} public: // Ìåòîäû ïðÿìîé èòåðàöèè class_type& operator ++() { —m_base; return *this; } reference operator *() { iterator_type base = m_base; —base; return *base; } . . . private: // Ïåðåìåííûå-÷ëåíû iterator_type m_base; };
Теперь перейдем к ошибке в классе zorder_iterator. При каждом разымено вании специализации обратного итератора (std::reverse_iterator) вызывается функция GetWindow() с помощью zorder_iterator:: operator —(). Поскольку между этими двумя вызовами Zпорядок может изме ниться в результате действий другого процесса или потока, поведение любого кода, который вызывает оператор разыменования несколько раз, не определено. Напри мер, подразумеваемая по умолчанию (не специализированная) реализация алго ритма for_each_if(), который мы будем обсуждать во втором томе, выглядит так: Листинг 26.10. Реализация алгоритма for_each_if() template< typename I // Òèï èòåðàòîðà , typename UF // Âûçûâàåìàÿ óíàðíàÿ ôóíêöèÿ , typename UP // Óíàðíûé ïðåäèêàò, ïðîâåðÿþùèé óñëîâèå âûçîâà > UF for_each_if(I first, I last, UF func, UP pred) { for(; first != last; ++first) { if(pred(*first)) { func(*first); } } return func; }
Путешествие по Z8плоскости
367
В этом алгоритме итератор first разыменовывается дважды. Если восполь зоваться текущей реализацией класса zorder_iterator совместно с этим или по добным ему алгоритмом, то окажется, что мы выбрали для применения действия одно окно, а применили его к другому. Представьте себе неразбериху! В этом месте я готов простить вас, если вы просто пожмете плечами и скажете: «Слишком сложно!». Но у меня другое кредо. К счастью, кажущаяся безнадеж ность ситуации всего лишь обусловлена предположением, что обратный итератор всегда следует писать в терминах std::reverse_iterator. И хотя в абсолютном большинстве случаев это действительно так – мне до сих не встречался еще ка койнибудь компонент, для которого так поступать нельзя, – никто не заставляет. Важно понимать, что стандарт предлагает шаблон std::reverse_ iterator как удобное средство, но не говорит, что только его и надлежит использовать. В данном случае мы можем восстановить наш класс итератора в правах, отка завшись от использования reverse_iterator и написав собственную реализа цию. При условии, что она удовлетворяет всем требованиям, предъявляемым к обратному итератору, – предоставляет методы для всех необходимых категорий итераторов, поддерживает обязательное соотношение со своим базовым итерато ром и имеет метод base(), позволяющий получить экземпляр эквивалентного ба зового итератора, а также не приводит к чемунибудь, вроде проблемы двойного разыменования, – все будет хорошо. Таким образом, нам требуется другая реали зация zorder_iterator, которая и будет показана ниже.
26.7.3. Когда двунаправленный итератор не является однонаправленным, но оказывается обратимым и клонируемым Знатоки STL, прочитав предыдущий раздел, наверное, начали чесать в удив лении затылок. Если две копии двунаправленного итератора после декремента могут оказаться не равны, то налицо нарушение требований, предъявляемых к двунаправленному итератору (C++03: 24.1.4;1), среди которых фигурирует и это соотношение. Знатоки правы. Все еще хуже. Поскольку мы знаем, что это соотношение не выполняется, то мы знаем также, что не выполняется и соответствующее требование к однонап равленным итераторам (C++ 03: 24.1.3;1): функция GetWindow() гарантирует повторяемость результата с флагом GW_HWNDNEXT не в большей мере, чем с флагом GW_HWNDPREV, то есть не гарантирует вовсе. Похоже, мы влипли. У нас есть компоненты, обеспечивающие разумное поведение, которое соот ветствует официальному пониманию итераторов – однонаправленных и двунап равленных, – во всем, кроме одного существенного момента. Если итератор не удовлетворяет условиям однонаправленности, то, согласно стандарту, единствен ный выход – трактовать его как итератор ввода. Но, по определению, итераторы ввода не являются двунаправленными, так как двунаправленный итератор дол жен обладать всеми свойствами однонаправленного.
368
Наборы
Мы не можем обеспечить семантику однонаправленного итератора лишь от части. Однонаправленный итератор отличается от итератора ввода, главным об разом, в двух отношениях: можно получать независимые копии, и эти копии удов летворяют условию продвижения, то есть если i1 == i2, то ++i1 == ++i2. В данном случае независимые копии получать разрешается, но указанное соотношение мо жет нарушаться. Нам необходимо, чтобы zorder_iterator был обратимым кло% нируемым итератором. Можно было бы ограничиться интерфейсом итератора ввода и забыть о се мантике обратимости, но, на мой взгляд, это все равно, что выплеснуть прекрасно развитого ребенка вместе с водой. Во втором томе мы увидим, что чуть ли не един ственное обоснование для определения разных категорий итераторов – возмож ность специализации алгоритмов. Нет слов, это очень важная цель, но иногда ей можно поступиться. Если отказаться от обратной итерации, мы много потеряем. Пусть даже условие равенства при продвижении и нарушается, но такие предло жения, как приведенное ниже, тем не менее вполне корректны: std::for_each(wps.rbegin(), wps.rend() , predicate_function(::IsWindowEnabled, window_show(true)));
Решение предельно простое, но отнюдь не очевидное. Требуется изменить всего лишь один символ. Мы сохраним всю функциональность zorder_iterator, только в объявлении скажем, что это не двунаправленный итератор, а итератор ввода: class zorder_iterator : public std::iterator< std::input_iterator_tag , HWND, ptrdiff_t, void, HWND> { . . .
Теперь алгоритмы не вправе делать необоснованных предположений о воз можностях итераторов, но мы при желании можем воспользоваться наличествую щими свойствами двунаправленности напрямую. Стандарт определяет обра тимый контейнер (C++ 03: 23.1) как такой, в котором есть типычлены reverse_iterator и const_reverse_iterator, допускающие двунаправленный или произвольный доступ, а также методы rbegin() и rend(). Хотя странным кажется якобы обратимый STLнабор, в котором итераторы не являются двунап равленными, определения компонентов на самом деле не подразумевают обяза тельности такого поведения; единственная проверка, выполняемая в таком слу чае, касается категории итератора, а тут мы в безопасности, так как требуем минимума. Мы не притворяемся гусем (раздел 10.1.3).
26.8. winstl::zorder_iterator: итератор, обратный самому себе Не вдаваясь в дальнейшие философствования, я просто вытащу кролика из цилиндра: обратным к итератору zorder_iterator является… да сам же zorder_iterator! Конечно, коечто придется добавить, но по существу идея
Путешествие по Z8плоскости
369
очень проста. Вопервых, потребуются некоторые константы и характеристиче ские классы. Константы определены в базовом классе этого итератора zorder_ iterator_base, как показано в листинге 26.11. Листинг 26.11. Определение класса zorder_iterator_base // Â ïðîñòðàíñòâå èìåí winstl struct zorder_iterator_base { public: enum search { fromFirstPeer = 1 // , fromCurrent = 2 // , atLastPeer = 3 // , fromFirstChild = 4 // , atLastChild = 5 // }; };
Ïåðåéòè Ïåðåéòè Ïåðåéòè Ïåðåéòè Ïåðåéòè
â íà÷àëî ñïèñêà ðàâíîïðàâíûõ îêîí îò òåêóùåé òî÷êè ñïèñêà îêîí â êîíåö ñïèñêà îêîí â íà÷àëî ñïèñêà äî÷åðíèõ îêîí â êîíåö ñïèñêà äî÷åðíèõ îêîí
26.8.1. Характеристический класс для zorder_iterator В данном случае для определения характеристик мы воспользуемся не одним шаблонным классом и его специализациями, а двумя отдельными классами zorder_iterator_forward_traits и zorder_iterator_reverse_traits. В ли стинге 26.12 приведены опережающие объявления для обоих, а также определе ние класса zorder_iterator_forward_traits. Листинг 26.12. Определение класса zorder_iterator_forward_traits //  ïðîñòðàíñòâå èìåí winstl struct zorder_iterator_forward_traits; struct zorder_iterator_reverse_traits; struct zorder_iterator_forward_traits { public: // Òèïû-÷ëåíû typedef zorder_iterator_forward_traits this_type; typedef zorder_iterator_reverse_traits alternate_type; public: // Ôóíêöèè static HWND get_first_child(HWND hwnd) { return ::GetWindow(hwnd, GW_CHILD); } static HWND get_first_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDFIRST); } static HWND get_next_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDNEXT); }
370
Наборы
static HWND get_previous_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDPREV); } static HWND get_last_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDLAST); } };
Пять методов соответствуют пяти константам в определении zorder_ iterator_base, а их реализации очевидны. Обратите внимание на типчлен alternate_type, который определен как zorder_iterator_reverse_traits. Определение этого класса приведено в листинге 26.13 и функционально является обращением zorder_iterator_forward_traits. Каждая функция в нем выпол няет прямо противоположное действие, передавая соответствующее значение флага GW_HWND*. Исключение составляет функция get_first_child(), которая, чтобы добиться эффекта, противоположного GW_CHILD, дважды вызывает GetWindow(): сначала с флагом GW_CHILD, а потом с флагом GW_HWNDLAST. Листинг 26.13. Определение класса zorder_iterator_reverse_traits //  ïðîñòðàíñòâå èìåí winstl struct zorder_iterator_reverse_traits { public: // Òèïû-÷ëåíû typedef zorder_iterator_reverse_traits this_type; typedef zorder_iterator_forward_traits alternate_type; public: // Ôóíêöèè static HWND get_first_child(HWND hwnd) { return ::GetWindow(::GetWindow(hwnd, GW_CHILD), GW_HWNDLAST); } static HWND get_first_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDLAST); } static HWND get_next_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDPREV); } static HWND get_previous_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDNEXT); } static HWND get_last_peer(HWND hwnd) { return ::GetWindow(hwnd, GW_HWNDFIRST); } };
Путешествие по Z8плоскости
371
26.8.2. Шаблон zorder_iterator_tmpl Эти характеристические классы используются в шаблонной форме zorder_ iterator, которая носит кошмарное имя zorder_iterator_tmpl:
Листинг 26.14. Определение класс zorder_iterator_tmpl //  ïðîñòðàíñòâå èìåí winstl template class zorder_iterator_tmpl : public zorder_iterator_base , public std::iterator< std::input_iterator_tag , HWND, ptrdiff_t , void, HWND // BVT > { public: // Òèïû-÷ëåíû typedef T traits_type; typedef zorder_iterator_tmpl class_type; typedef zorder_iterator_tmpl base_iterator_type; typedef base_iterator_type iterator_type; private: // Êîíñòðóèðîâàíèå zorder_iterator_tmpl(HWND hwndRoot, HWND hwndCurrent); public: static class_type create(HWND hwndRoot, search from); zorder_iterator_tmpl(); public: // Èòåðàöèÿ class_type& operator ++(); class_type operator ++(int); value_type operator *() const; bool equal(class_type const& rhs) const; class_type& operator —-(); class_type operator —-(int); base_iterator_type base() const; private: // Ðåàëèçàöèÿ static HWND get_next_window_(HWND hwnd, HWND (*pfn)(HWND )); private: // Ïåðåìåííûå-÷ëåíû HWND m_hwndRoot; HWND m_hwndCurrent; }; typedef zorder_iterator_tmpl zorder_iterator;
Сразу нужно отметить несколько моментов. Вопервых, zorder_iterator_ tmpl наследует zorder_iterator_base, чтобы напрямую получить доступ к кон стантам. Следовательно, флаги поиска можно выразить в терминах итератора. Такая техника используется в библиотеке IOStreams, где флаги определены в классе std::ios_base, но доступны во всех производных классах и выражаются в терминах их имен. Вовторых, мы специализируем std::iterator так, чтобы получить итератор ввода и категорию временных по значению ссылок на элементы в соответствии с обсуждавшимися ранее требованиями.
372
Наборы
Конструктор преобразования объявлен закрытым во избежание инициализа ции пользователем некорректной пары окон. Чтобы воспользоваться этим конст руктором, следует вызвать статический метод create(), которому передается окно и один из членов перечисления search: child_window_sequence::iterator child_window_sequence::begin() { return iterator::create(m_hwnd, iterator::fromFirstChild); }
В задачу метода create() (см. листинг 26.15) входит определение подходя щих значений m_hwndRoot и m_hwndCurrent в соответствии с заданным видом по иска. Если затребовано дочернее окно, то hwndRoot корректируется путем вызова метода get_first_child(), определенного в характеристическом классе. hwndCurrent устанавливается равным hwndRoot для поиска, начиная с текущей позиции, или равным NULL – для поиска от последнего равноправного или дочер него окна. Если поиск ведется от первого равноправного или дочернего окна, то hwndCurrent присваивается результат обращения к методу get_first_peer() из характеристического класса. Листинг 26.15. Реализация метода zorder_iterator_tmpl::create() template zorder_iterator_tmpl zorder_iterator_tmpl::create(HWND hwndRoot, search from) { HWND hwndCurrent; switch(from) { case fromFirstChild: case atLastChild: hwndRoot = get_next_window_(hwndRoot , traits_type::get_first_child); default: break; } switch(from) { case fromCurrent: hwndCurrent = hwndRoot; break; case fromFirstPeer: case fromFirstChild: hwndCurrent = get_next_window_(hwndRoot , traits_type::get_first_peer); break; case atLastChild: case atLastPeer: hwndCurrent = NULL; break; } return zorder_iterator_tmpl(hwndRoot, hwndCurrent); }
Путешествие по Z8плоскости
373
Все методы характеристического класса вызываются через закрытый стати ческий метод get_next_window_() (листинг 26.16), который проверяет, не запор тился ли итератор в результате внешних действий. Листинг 26.16. Реализация метода zorder_iterator_tmpl::get_next_window_() template HWND zorder_iterator_tmpl::get_next_window_(HWND hwnd , HWND (*pfn)(HWND)) { hwnd = (*pfn)(hwnd); if(NULL == hwnd) { DWORD dwErr = ::GetLastError(); if(ERROR_SUCCESS != dwErr) { throw external_iterator_invalidation("îøèáêà ïðè ïîèñêå â z-ïîðÿäêå: îêíî óíè÷òîæåíî", static_cast(dwErr)); } } return hwnd; }
Конструктор по умолчанию устанавливает обе переменныечлены в NULL. Конструктор копирования, копирующий оператор присваивания и деструктор не объявлены, сгенерированные компилятором версии вполне годятся. В шаблонном классе есть все методы двунаправленного итератора, которые мы ранее видели в нешаблонной версии. Логика операторов инкремента и декре мента не отличается от описанной ранее, но теперь они пользуются методами get_next_peer(), get_previous_peer() и get_last_peer() из характеристи ческого класса, вызываемыми с помощью get_next_window_(). В листинге 26.17 приведены определения операторов прединкремента и предекремента. Пост ва рианты, как всегда, реализуются канонически (листинг 19.8). Листинг 26.17. Реализация операторов прединкремента и предекремента template typename zorder_iterator_tmpl::class_type& zorder_iterator_tmpl::operator ++() { WINSTL_MESSAGE_ASSERT("Ïîïûòêà èíêðåìåíòà íåäåéñòâèòåëüíîãî èëè âûøåäøåãî çà ïðåäåëû äèàïàçîíà èòåðàòîðà", NULL != m_hwndCurrent); m_hwndCurrent = get_next_window_(m_hwndCurrent , traits_type::get_next_peer); return *this; } template typename zorder_iterator_tmpl::class_type& zorder_iterator_tmpl::operator —() { WINSTL_MESSAGE_ASSERT("Ïîïûòêà èíêðåìåíòà íåäåéñòâèòåëüíîãî èëè âûøåäøåãî çà ïðåäåëû äèàïàçîíà èòåðàòîðà", NULL != m_hwndRoot);
374
Наборы
if(NULL != m_hwndCurrent) { m_hwndCurrent = get_next_window_(m_hwndCurrent , traits_type::get_previous_peer); } else { m_hwndCurrent = get_next_window_(m_hwndRoot , traits_type::get_last_peer); } return *this; }
Оператор разыменования всегда возвращает член m_hwndCurrent, который равен описателю текущего окна или NULL.
26.8.3. Семантика обратной итерации Чтобы поддержать стандартные требования к обратному итератору, в классе zorder_iterator_tmpl имеется метод base(), которые возвращает экземпляр типа base_iterator_type. Именно здесь в игру вступает типчлен характеристи ческих классов alternate_type. Им специализируется шаблон zorder_ iterator_tmpl, для того чтобы тип обратного итератора был производным от base_iterator_type. (Следуя соглашению, принятому в шаблоне std:: reverse_iterator, я включил синоним iterator_type, хотя считаю, что имя выбрано неудачно, оно слишком общее.) Метод base() реализован следующим
образом: template typename zorder_iterator_tmpl::base_iterator_type zorder_iterator_tmpl::base() const { base_iterator_type bi = base_iterator_type::create(m_hwndCurrent , fromCurrent); return ++bi; }
Теперь посмотрим, как нам удалось получить обратимый итератор и обойти проблему двойного разыменования. Метод create() используется для получе ния экземпляра типа base_iterator_type, который указывает на ту же внутрен нюю позицию, что и экземпляр, для которого вызван метод base(). Затем этот экземпляр инкрементируется, что приводит к продвижению назад, – не забывай те, это обратный итератор – и возвращается. Этот тип обладает тем же поведени ем, что и тип прямого итератора, только движется в обратном направлении. При выполнении операции инкремента вызывается метод zorder_iterator_ reverse_traits::get_next_peer(), который вызывает функцию GetWindow() с флагом GW_HWNDPREV. Если будет возвращено значение NULL, то итератор нахо дится в концевой точке, то есть в начале списка окон. Аналогично, при выполне нии декремента вызывается метод zorder_iterator_reverse_traits::get_ previous_peer(), который вызывает GetWindow() с флагами GW_HWNDNEXT и GW_HWNDFIRST соответственно.
Путешествие по Z8плоскости
375
Операция разыменования сводится просто к возврату m_hwndCurrent: ника кого двойного разыменования нет и в помине, а, значит, нет и опасности получить разные значения в алгоритмах, которые запрашивают значение итератора более одного раза. И, наконец, поскольку итератор помечен тегом input_iterator_ tag, он не будет использоваться в алгоритмах, предполагающих надежную много проходность, а не клонируемость. Но при этом нам попрежнему доступен любой алгоритм, в котором используются пары begin() и end() или rbegin() и rend(), так как и прямой, и обратный итераторы принадлежат категории итераторов вво да. Что и требовалось доказать. Совет. Если вы не в состоянии поддержать двунаправленный итератор для набора, кото) рый допускает обход в обратном порядке, то можете вместо этого предоставить незави) симые итераторы ввода, один из которых обходит набор в прямом направлении, а другой – в обратном.
26.9. Завершающие штрихи в реализации последовательностей равноправных окон С описанными выше усовершенствованиями класса zorder_iterator классы последовательностей window_peer_sequence и child_window_sequence стано вятся гораздо проще и почти не отличаются друг от друга. Все различия инкапсу лированы в методах begin(), end(), rbegin() и rend(). В листинге 26.18 приве ден код window_peer_sequence. Листинг 26.18. Окончательная версия класса window_peer_sequence class window_peer_sequence { public: // Òèïû-÷ëåíû typedef zorder_iterator iterator; typedef iterator::value_type value_type; typedef iterator::base_iterator_type reverse_iterator; typedef window_peer_sequence class_type; public: // Êîíñòðóèðîâàíèå explicit window_peer_sequence(HWND hwnd); public: // Èòåðàöèÿ iterator begin() const { return iterator::create(m_hwnd, iterator::fromFirstPeer); } iterator end() const { return iterator::create(m_hwnd, iterator::atLastPeer); } reverse_iterator rbegin() const; { return reverse_iterator::create(m_hwnd
376
Наборы
, reverse_iterator::fromFirstPeer); } reverse_iterator rend() const { return reverse_iterator::create(m_hwnd , reverse_iterator::atLastPeer); } public: // Ñîñòîÿíèå bool empty() const; private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; private: // Íå ïîäëåæèò ðåàëèçàöèè window_peer_sequence(class_type const&); class_type& operator =(class_type const&); };
Отметим, что реализации begin() и rbegin(), а также end() и rend() не от личаются ничем, кроме типа возвращаемого значения – разительный контраст с канонической формой (см. раздел 24.11.2). Реализация child_window_sequence в точности такая же, только в window_ peer_sequence используются константы fromFirstPeer и atLastPeer, а в child_window_sequence – fromFirstChild и atLastChild. Естественно, эта ситуация – кость в горле у программистов, которые не любят дублировать код, то есть у всех хороших программистов. Значит, нужно заняться рефакторингом. На компактдиске есть дополнительный материал, в котором объясняется, как это сделать с помощью общего шаблона последовательности, параметризованного константамифлагами.
26.10. Резюме Эта глава началась как упражнение на корректную реализацию семантики двунаправленного итератора, но оказалась гораздо более серьезным предприяти ем. Будет справедливо сказать, что мы подробно раскрыли тему двунаправленных итераторов, в том числе специфичные для них требования, касающиеся хранения состояния и эквивалентности. Освоив этот материал, вы сможете и сами писать такие итераторы. Попутно мы рассмотрели вопрос о порче итератора извне, то есть об изменении содержимого последовательности, которую обходит итератор, в результате действий, не контролируемых программой, и пояснили, почему в та кой ситуации лучше всего возбуждать исключение. (В главе 33 мы еще вернемся к этой теме.) Наконец, не забудьте про сногсшибательную идею шаблонного класса итератора, который одновременно описывает и тип обратного себе итератора.
26.11. Еще о Z"плоскости Небольшое лирическое отступление. Как только я осознал логический изъян в первой версии на основе шаблона std::reverse_iterator, то тут же придумал решение или нечто близкое к нему. Я понимал, что мне потребуется както преоб разовать различные вызовы функций (в одном классе GetWindow(GW_CHILD) вы
Путешествие по Z8плоскости
377
зывалась в конструкторе, а в другом – нет) в константы, но не проектировал реше ние от начала до конца. Оно стало результатом постепенной переработки. На мой взгляд, большинство хороших систем хорошо спроектированы, а большинство хороших библиотек стали таковыми в результате эволюции. Начинать всегда надо с основополагающих принципов, но в первом случае проектирование ведет ся в основном сверху вниз, а во втором – снизу вверх, и время от времени требует ся несколько взмахов косой редизайна или рефакторинга. То, что мы видели, и есть пример такого сенокоса.
Глава 27. Разбиение строки Как тщеславен тот, кто решился писать, не отважившись жить. – Генри Дэвид Торо Гонки не становятся легче, только быстрее. – Грег ЛеМонд
27.1. Введение Вы уже привыкли к моему стилю письма и, наверное, ждете пояснений по по воду эволюции очередного набора в элегантно оформленный класс, который на ура обрабатывает все возможные случаи. Если так, боюсь, что в этой главе вас ожидает разочарование. Ибо вы получите историю о том, как сложность, стремле ние к эффективности и гибкости и сотнядругая переработок породили действи тельно гибкий и очень быстрый класс, который при этом катастрофически нару шает гипотезу Хенни (глава 14). По мере того как я писал разные главы, вошедшие в часть II, большинство компонентов подверглись существенной переработке благодаря пристальному критическому изучению, необходимому для документирования их дизайна и реа лизации в книге. Однако с этой главой, которая посвящена шаблонному классу string_tokeniser из библиотеки STLSoft, дело обстоит иначе. Его реализация оттачивалась на протяжении нескольких лет, поэтому специально для книги не пришлось ничего улучшать. С другой стороны, интерфейс в некоторых случаях оставляет желать лучшего. Интерфейс самого класса лаконичен и очень прост, он состоит из трех очевидных конструкторов и методов begin(), end() и empty(). Напротив, шаблонный интерфейс невразумителен, раздут и может служить чуть ли не идеальным контрпримером для гипотезы Хенни. Хотя у этого класса всего два параметра шаблона без значений по умолчанию, гипотеза Хенни к нему все равно применима, поскольку в одном из наиболее полезных режимов использова ния приходится явно специфицировать от одного до четырех (!) параметров шаб лона, имеющих значения по умолчанию. Поэтому эта глава преследует две главных цели. Вопервых, класс string_ tokeniser – отличный пример временных по значению ссылок (раздел 3.3.5) в том смысле, что проливает свет на компромиссы между различными категориями ссылок и итераторов и соображениями производительности. Вовторых, само не совершенство шаблонного интерфейса дает пищу для иллюстрации трудностей,
Разбиение строки
379
с которыми приходится сталкиваться при написании гибких и понятных шаблон ных компонентов общего назначения.
27.2. Функция strtok() Разбиение на лексемы – это процедура расщепления строки на более мелкие части по заданному разделителю. Под разделителем можно понимать один сим вол, набор из нескольких символов или даже чтото более сложное, включающее позиционные или контекстнозависимые ограничения. В стандарте C для разбие ния строки на лексемы предусмотрена функция strtok() (C99: 7.21.5.8), в кото рой разделителем может быть любой одиночный символ из заданного набора. char* strtok(char* str, char const* delimiterList);
Это единственное средство разбиения строки, предлагаемое стандартом C или C++. (В стандарте C определен также вариант wcstok() для работы с широ кими символами, в котором аргументы и тип возвращаемого значения выражены в терминах wchar_t. Во всех остальных отношениях поведение обеих функций совпадает.) Функция разбивает строку str по разделителям, заданным в строке delimiterList, и возвращает указатель на начало очередной лексемы. Если лек сем больше не осталось, возвращается NULL. При первом обращении в параметре str передается разбиваемая строка, и при всех последующих – NULL. Текущая точка разбиения хранится во внутренней статической переменной и сбрасывает ся, когда str не равно NULL. Например, для разбиения строки по символам про пуска нужно написать такой код: char* char* const char
str = . . . tok; delims[] = " \r\n\t";
for(tok = ::strtok(str, delims); NULL != tok; tok = ::strtok(NULL, delims)) { ::puts(tok); }
Хотя этот код прост, эффективен и достаточно прозрачен, у него есть ряд не достатков. Самое главное, что для хранения состояния используется разделяемая внутренняя переменная, что делает код небезопасным относительно потоков и нереентерабельным. Если функция strtok() будет вызываться одновременно из двух потоков, пусть даже для разных строк, возникнет конкуренция за внутрен нюю переменную, в которой хранится предыдущая позиция в строке. Хотя в стан дарте вопросы многопоточности не рассматриваются, в нескольких стандартных библиотеках существуют другие реализации, в которых для обеспечения незави симости при вызове strtok() применяется память, отведенная для потока. Есть и более неприятная проблема, которую такими средствами не решить. Это вложен ные циклы разбиения строки в рамках одного потока, поведение которых не опре делено. Рассмотрим такой код:
Наборы
380
char str[] = "abc,def;ghi,jkl;;"; char* outer; char* inner; for(outer = strtok(str, ";"); NULL != outer; outer = strtok(NULL, ";")) { std::cout [D равно wchar_t]). Третий параметр B – тип полити ки, определяющей, нужно ли сохранять или пропускать пустые лексемы. По умолчанию он равен skip_blank_tokens<true> (листинг 27.3), то есть пустые лексемы пропускаются. Чтобы сохранить пустые лексемы в диапазоне [begin(), end()), следует присвоить этому параметру значение skip_blank_tokens. (Этот параметр является типом, а не просто булевским значением, по историчес ким причинам. Когдато я хотел сделать класс более изощренным, чем получи лось в итоге.) Листинг 27.3. Определение шаблонного класса политики skip_blank_tokens template struct skip_blank_tokens { enum { value = B }; };
Четвертый параметр V – тип значения «строкоразбивателя» (и вложенного в него класса итератора). По умолчанию он совпадает с S, но может быть любым подходящим строковым типом (что такое «подходящий», определяется характе ристическим типом, до которого мы скоро дойдем). Таким образом, поддержива ются различные сочетания копирования/некопирования разбиваемой строки и типов значений. Например, если S – представление строки (например, stlsoft:: basic_string_view), а V – обычный строковый класс (например, std::string), то разбиваемая строка не копируется (и не изменяется), а значе ния, возвращаемые при разыменовании итераторов, – отдельные экземпляры строк. (Строковое представление – это тип, в котором хранится длина и указатель
388
Наборы
на массив символов, но не предполагается, что часть массива, на который он ссы лается, завершается нулем. ) С объяснением оставшихся двух параметров шаблона – T и P – можно подож дать. Сначала посмотрим, как реализован итератор и как достигается впечатляю щая производительность, о которой я не устаю говорить (и докажу в разделе 27.9). Затем мы обсудим ряд специализаций, покрывающих распространенные сцена рии разбиения строк, посмотрим, как «разбиватель» проявляет себя в типичных ситуациях, и, наконец, поговорим о том, какие меры можно предпринять в случа ях, когда он оказывается далек от совершенства.
27.6.1. Класс string_tokensier::const_iterator В листинге 27.4 приведено определение вложенного класса const_ iterator. Листинг 27.4. Определение класса const_iterator public: // Èòåðàöèÿ class const_iterator : public std::iterator< std::forward_iterator_tag , value_type, ptrdiff_t , void, value_type // BVT > { public: // Òèïû-÷ëåíû typedef const_iterator class_type; . . . private: // Êîíñòðóèðîâàíèå friend class string_tokeniser<S, D, B, V, T, P>; const_iterator( underlying_iterator_type first , underlying_iterator_type last , delimiter_type const& delimiter) : m_find0(first) , m_find1(first) , m_next(first) , m_end(last) , m_delimiter(&delimiter) , m_cchDelimiter(comparator_type::length(delimiter)) { if(m_end != m_find0) { increment_(); } } public: const_iterator(); const_iterator(class_type const& rhs); class_type const& operator =(class_type const& rhs); public: // Ìåòîäû ïðÿìîé èòåðàöèè value_type operator *() const { return traits_type::create(m_find0, m_find1); }
Разбиение строки
389
class_type& operator ++() { increment_(); return *this; } const class_type operator ++(int); bool equal(class_type const& rhs) const { STLSOFT_MESSAGE_ASSERT( "Ñðàâíèâàþòñÿ èòåðàòîðû äëÿ ðàçíûõ ðàçáèâàòåëåé", m_end == rhs.m_end); return m_find0 == rhs.m_find0; } private: // Ðåàëèçàöèÿ void increment_() { STLSOFT_MESSAGE_ASSERT( "Ïîïûòêà èíêðåìåíòà íåäåéñòâèòåëüíîãî èòåðàòîðà", m_find0 != m_end); if(blanks_policy_type::value) // Ïðîïóñê èëè ñîõðàíåíèå ïóñòûõ ëåêñåì { for(m_find0 = m_next; m_find0 != m_end; ) { if(comparator_type::not_equal(*m_delimiter, m_find0)) { break; } else { m_find0 += static_cast(m_cchDelimiter); } } } else { m_find0 = m_next; } for(m_find1 = m_find0; ; ) // Ãëàâíûé öèêë ðàçáèåíèÿ { if(m_find1 == m_end) { m_next = m_find1; break; } else if(comparator_type::not_equal(*m_delimiter, m_find1)) { ++m_find1; } else { m_next = m_find1 + static_cast(m_cchDelimiter); break; } } private: // Ïåðåìåííûå-÷ëåíû underlying_iterator_type m_find0; // Íà÷àëî òåêóùåé ëåêñåìû underlying_iterator_type m_find1; // Êîíåö òåêóùåé ëåêñåìû underlying_iterator_type m_next; // Íà÷àëî ñëåäóþùåãî ýëåìåíòà
Наборы
390 underlying_iterator_type m_end; delimiter_type const* size_t
// Êîíöåâàÿ òî÷êà ïîñëåäîâàòåëüíîñòè m_delimiter; // Ðàçäåëèòåëü m_cchDelimiter; // ×èñëî ñèìâîëîâ â ðàçäåëèòåëå
}; . . .
В итераторе есть шесть переменныхчленов, из которых первые четыре имеют тип underlying_iterator_type, то есть типчлен const_iterator, определен ный в типе разбиваемой строки S. Члены m_find0 и m_find1 обозначают границы текущей точки итерации аналогично переменным p0 и p1 в функции stlsoft:: find_next_token() (раздел 27.5.3): диапазон [m_find0, m_find1) определяет по зицию и протяженность текущей лексемы. Таким образом, оператор разыменова ния просто вызывает функцию traits_type::create(), передавая ей эти два члена, и возвращает текущее значение. Член m_next содержит начало следующей лексемы, т.е. точку, с которой m_find0 возобновит работу. m_end – конец разбива емой строки; m_delimiter указывает на копию разделителя, которая хранится в экземпляре «разбивателя»; m_cchDelimiter – количество символов в разделите ле. Если разделитель – одиночный символ или набор символов, то m_cchDelimiter равен 1, а если строка, то ее длине. Конструктор преобразования и оператор прединкремента реализованы с по мощью вспомогательного закрытого метода increment_(). Он, в свою очередь, пользуется константой blanks_policy_type::value и методом comparator_ type::not_equal(). Последнему передается ссылка на разделитель и копия обертываемого экземпляра итератора, чтобы можно было понять, указывает ли данный экземпляр на начало разделителя. Алгоритм разбиения довольно прост и состоит из двух частей. Первая часть относится к пропуску пустых лексем. Если их нужно пропускать, то мы обходим m_find0, пока не дойдем до разделителя (или его начала). В противном случае m_find0 просто устанавливается на начало следующей точки (m_next). Основная часть алгоритма устанавливает m_find1 в текущую точку m_find0 и затем сдвига ет m_find1, пока не встретится следующий разделитель (или его первый символ). Когда алгоритм завершает работу, члены m_find0 и m_find1 определяют диапа зон текущей лексемы внутри последовательности.
27.6.2. Выбор категории Итератор однонаправленный. Так как «разбиватель» не изменяет разбивае мую строку, было бы странно, если бы итераторы не поддерживали многопроход ность. Поддержать более развитую категорию итераторов было бы невозможно или потребовало бы усложнения, которое вряд ли оправдано во всех сценариях использования. Поскольку «разбиватель» порождает лексемы, естественно ожидать, что кате гория ссылок на элементы – временные по значению. Но это не приговор – мы могли бы поддержать недолговечные ссылки (раздел 3.3.4), если бы хранили в каж дом экземпляре итератора (кроме концевого) экземпляр текущей лексемы, запо
Разбиение строки
391
миная его в методе increment_(). Однако это было бы неэффективно по двум причинам. Вопервых, при копировании экземпляров итератора пришлось бы ко пировать текущее кэшированное значение. Если строковый тип – обычная стро ка, то стоимость такой операции довольно высока. Поскольку итераторы (по мое му опыту) чаще копируются, чем разыменовываются, то такое решение оказалось бы антиоптимизацией. (К итераторам ввода это не относится, поскольку, разде ляя состояние итерации, они заодно часто хранят значение в разделяемом кэше.) Вовторых, подобное хранение экземпляра не позволило бы компилятору применить оптимизацию возвращаемого значения (RVO) в случае, когда каждый итератор разыменовывается не более одного раза. Совет. Предпочитайте временные по значению ссылки на элементы недолговечным ссылкам для наборов, в которых тип значения необходимо синтезировать внутри. Исклю) чение составляют итераторы ввода.
27.6.3. Класс stlsoft::string_tokeniser_type_traits В готовом шаблонном классе string_tokeniser_type_traits определены несколько типовчленов и три статических функции (листинг 27.5). Этого доста точно для большинства сценариев разбиения. Листинг 27.5. Определение класса string_tokeniser_type_traits template< typename S , typename V > struct string_tokeniser_type_traits { public: // Òèïû-÷ëåíû typedef typename S::value_type value_type; typedef typename S::const_iterator const_iterator_type; public: // Îïåðàöèè static const_iterator_type begin(S const& s) { return s.begin(); } static const_iterator_type end(S const& s) { return s.end(); } static V create(const_iterator_type from, const_iterator_type to) { return V(from, to); } };
Если вы хотите выполнять разбиение для строковых типов, в которых опреде лены оба типачлена, два неизменяющих метода доступа и один конструктор диа пазона (выделены полужирным шрифтом), необходимые основному шаблону, то
392
Наборы
у вас есть два варианта. Можно специализировать шаблон stlsoft::string_ tokeniser_type_traits своим типом или предоставить собственный характери стический тип для специализации «разбивателя». Пример первого подхода при веден в листинге 27.6, где с «разбивателем» используется тип CString. Листинг 27.6. Специализация шаблона string_tokeniser_type_traits классом CString из библиотеки MFC //  ïðîñòðàíñòâå èìåí stlsoft template struct string_tokeniser_type_traits { public: // Òèïû-÷ëåíû typedef TCHAR value_type; typedef LPCTSTR const_iterator_type; public: // Îïåðàöèè static const_iterator_type begin(CString const& s) { return s; } static const_iterator_type end(CString const& s) { return begin(s) + s.GetLength(); } static CString create(const_iterator_type from , const_iterator_type to) { return CString(from, to - from); } };
Если специализация определена, то использовать класс string_tokeniser совместно с CString так же просто, как и с любым другим строковым классом: string_tokeniser tokens("abc;def;ghi;;jkl;;;", ';'); std::copy(tokens.begin(), tokens.end() , std::ostream_iterator(std::cout, «\n»));
27.6.4. Класс stlsoft::string_tokeniser_comparator Классы политик сравнения концептуально не сложнее характеристических классов. Из листинга 27.4 видно, что от них требуются всего две функции: not_equal() и length(): template struct arbitrary_comparator { typedef ???? delimiter_type; template static bool not_equal(delimiter_type const& delim , const_iterator& it); static size_t length(delimiter_type const& delim); };
Разбиение строки
393
Однако не все так просто. Чтобы иметь возможность работать с различными строковыми типами и неодинаковыми строковым типом (S) и типом разделителя (D), готовый класс политики сравнения принимает три параметра шаблона – S, D и T, переданные шаблону «разбивателя», и определяет ряд перегруженных закры тых методов, ориентированных на типы разделителя char и wchar_t (листинг 27.7). Альтернатива – сопоставление компаратора с типом разделителя. Но для этого потребовалась бы частичная специализация шаблона, а «разбиватель» был написан задолго до того, как компиляторы стали поддерживать эту возможность, поэтому я выбрал более прагматичный подход – перегрузить методы в общем шаблонном классе. Листинг 27.7. Определение класса string_tokeniser_comparator template // Òî æå, ÷òî â tokeniser struct string_tokeniser_comparator { public: // Òèïû-÷ëåíû typedef D delimiter_type; typedef S string_type; typedef T traits_type; typedef typename traits_type::const_iterator_type const_iterator; private: typedef string_tokeniser_comparator class_type; private: // Ðåàëèçàöèÿ template static bool is_equal_(I1 p1, I2 p2, size_t n) { for(; n— > 0; ++p1, ++p2) { if(*p1 != *p2) { return false; } } return true; } template static bool is_equal_(D2 const& delim, I& p2) { return class_type::is_equal_(delim.begin(), p2, delim.length()); } static bool is_equal_(char const delim, const_iterator& it) { return delim == *it; } static bool is_equal_(wchar_t const delim, const_iterator& it) { return delim == *it; } template static size_t get_length_(D2 const& delim) {
394
Наборы
return delim.length(); } static size_t get_length_(char /* delim */) { return 1; } static size_t get_length_(wchar_t /* delim */) { return 1; } public: // Îïåðàöèè static bool not_equal(delimiter_type const& delim , const_iterator& it) { return !is_equal_(delim, it); } static size_t length(delimiter_type const& delim) { return get_length_(delim); } };
27.7. Тестирование Настало время посмотреть, как наше творение работает.
27.7.1. Одиночный символ"разделитель Для этого случая нужно выбрать строковый тип и соответствующий ему тип символа. В следующем примере мы разбиваем содержимое строки типа std::string по символу ';' и печатаем результаты. Должно получиться "abc,def,ghi,". std::string str = ";abc;;def;ghi"; stlsoft::string_tokeniser<std::string, char> tokens(str, ';'); std::copy(tokens.begin(), tokens.end() , std::ostream_iterator<std::string>(std::cout, ",")
Тот же результат можно получить и с помощью других компонентов. Сначала strtok(): std::string str = ";abc;;def;ghi"; char* writeableCopy = ::strdup(str.c_str()); char* tok; for(tok = ::strtok(string, ";"); NULL != tok; tok = ::strtok(NULL, ";")) { std::cout , string_t , stlsoft::string_tokeniser_type_traits<string_t , string_t > , stlsoft::charset_comparator<string_t> > tokeniser_t; char* str = . . . tokeniser_t tokens(str, " \r\n\t"); std::copy(tokens.begin(), tokens.end() , std::ostream_iterator<std::string>(std::cout));
Сравните с приведенным выше эквивалентным примером, где используется strtok(), и я не буду в обиде, если вы скажете, что STL – это безумие. На самом деле, проблема вполне прозаична – шаблонный класс string_tokeniser не очень
удачно спроектирован. К счастью, имеется решение.
27.8. Немного о политиках Я признаю ошибки проектирования не просто из свойственного англичанам самоуничижения. Абсолютно искренне. Заметьте, однако, что я не сказал, будто шаблонный класс плохо спроектирован. Правильнее сказать, что он не проекти ровался вообще. В процессе перехода от SynesisSTL::StringTokeniser (лис тинг 27.1) к варианту, который мог бы обрабатывать разделителистроки (раздел 27.4), всякое проектирование закончилось, и началась эволюция класса. К тому моменту, как я решил поддержать еще и набор символовразделите лей, класс пустил корни, и менять чтото было уже поздно. Такое проектирование по наитию не всегда стоит рекомендовать, поскольку можно оказаться в западне типа той, в которую я попал. По чести говоря, если вы много занимаетесь написа нием библиотек, то, наверное, нередко именно так и работаете, даже не осознавая этого, и часто это проходит безнаказанно благодаря приобретенному тяжким тру дом опыту, который указывает правильное направление. Но даже если эволюционная разработка приводит к неудовлетворительным результатам, как в данном случае, в хаосе случайности все же могут сохраниться крупицы мудрого предвидения. Видимо, в какойто момент меня осенило боже ственное провидение, и я включил политику сравнения. В то время – а это был конец 1990х годов – я, как и многие мои коллеги, был одержим концепцией поли тик. Могу только предположить, что воткнул компаратор лишь потому, что идея иметь политику сравнения показалась мне тогда здравой. К счастью, это совер шенно случайное проявление интуиции поможет вытащить компонент из черной дыры неудобоприменимости, если мы воспользуемся простой техникой, о кото рой часто забывают: наследованием. Как это делается, мы увидим в следующем разделе. Но желание в восторге хлопнуть себя по плечу сдерживается тем фактом,
400
Наборы
что даже политика сравнения не в состоянии решить проблему разбиения по пар ным символамразделителям.
27.8.1. Переработка параметров шаблона с помощью наследования У нас уже есть шаблон charset_comparator (раздел 27.6.4), но использова ние его в сочетании с классом string_tokeniser приводит к ужасающим резуль татам, главным образом изза упорядочения параметров шаблона. Простой и эф фективный способ изменить порядок параметров шаблона состоит в том, чтобы определить новый шаблон charset_tokeniser, реализовав его в терминах уже имеющегося. Вопрос в том, что лучше использовать: наследование или компози цию. В данном случае мы не добавляем в новый класс ни нового состояния, ни нового поведения во время выполнения, поэтому не видно препятствий к приме нению наследования, и это позволит компилятору применить оптимизацию пус% того производного класса (см. раздел 12.4 книги Imperfect C++). Применение наследования также освобождает нас от необходимости писать перенаправляющие функции для методов begin(), end() и empty(). Но не от не обходимости реализовать конструкторы. Впрочем, они потребовались бы в любом случае. В листинге 27.9 приведена полная реализация класса stlsoft::charset_ tokeniser. Листинг 27.9. Определение класса charset_tokeniser //  ïðîñòðàíñòâå èìåí stlsoft template< typename S , typename B = skip_blank_tokens<true> , typename V = S , typename T = string_tokeniser_type_traits<S, V> , typename D = S , typename P = charset_comparator<S> > class charset_tokeniser : public string_tokeniser<S, D, B, V, T, P> { private: // Òèïû-÷ëåíû typedef string_tokeniser<S, D, B, V, T, P> parent_class_type; public: typedef charset_tokeniser<S, B, V, T, D, P> class_type; typedef typename parent_class_type::string_type string_type; . . . // È äëÿ delimiter_type, value_type è ò.ä. typedef typename parent_class_type::const_iterator const_iterator; public: // Êîíñòðóèðîâàíèå template charset_tokeniser(S1 const& str, delimiter_type const& charSet) : parent_class_type(str, charSet) {} template
Разбиение строки
401
charset_tokeniser(S1 const& str, size_type n , delimiter_type const& charSet) : parent_class_type(str, n, charSet) {} template charset_tokeniser(I from, I to, delimiter_type const& charSet) : parent_class_type(from, to, charSet) {} };
27.8.2. Шаблоны"генераторы типов Хотя C++ не поддерживает (пока) typedef для шаблонов, но это средство мож но аппроксимировать с помощью шаблонов, генерирующих типы (раздел 12.2). И с их помощью получить альтернативное решение проблемы набора символов разделителей, показанное в листинге 27.10. Листинг 27.10. ШаблонZгенератор типов для «разбивателя» с набором символовZразделителей //  ïðîñòðàíñòâå èìåí stlsoft template< typename S , typename B = skip_blank_tokens<true> , typename V = S , typename T = string_tokeniser_type_traits<S, V> , typename D = S , typename P = charset_comparator<S> > struct charset_tokeniser_selector { public: // Òèïû-÷ëåíû typedef string_tokeniser<S, D, B, V, T, P> tokeniser_type; };
Мы уже встречались с шаблономгенератором: allocator_selector (раз дел 12.2.1). В данном случае выбор делается между двумя специализациями одно го и того же шаблона, а не между одинаковыми специализациями разных шабло нов. Но идея та же самая: шаблон генерирует тип от имени пользователя. stlsoft::charset_tokeniser_selector<std::string>::tokeniser_type tokens("\rabc def\nghi\tjkl ", " \r\n\t"); std::copy(tokens.begin(), tokens.end() , std::ostream_iterator<std::string>(std::cout, "\n"));
Достоинство такого подхода в том, что шаблонгенератор невелик и легко пи шется. Кроме того, он не порождает вообще никакого кода на этапе выполнения, поэтому теоретически и тестировать нечего. Недостаток же в том, что по сравне нию с шаблоном класса шаблонгенератор выглядит немного неуклюже и с тру дом воспринимается пользователями, не знакомыми с этой конструкцией. Поэто му они могут не понять, что нужное им средство решения возникшей проблемы существует. (Все классы, касающиеся разбиения строк, можно найти по образцу "\s*class\s+\w*token".)
402
Наборы
27.8.3. Как быть с гипотезой Хенни? Мы рассмотрели два относительно простых способа избежать нарушения ги% потезы Хенни, возникшего в результате эволюции компонента (и незавершенного проектирования на ранней стадии). Оба понятны и не требуют метапрограммиро вания. В библиотеке STLSoft Tokenising я выбрал решение, основанное на насле довании. Во втором томе мы познакомимся с более сложной техникой разреше ния таких ситуаций. А пока удовлетворимся тем, что есть, и сравним производительность различных компонентов.
27.9. Производительность Мы рассмотрим девять сценариев разбиения строки на лексемы: 1. Короткая строка (2 лексемы), одиночный символразделитель, сохранение пустых лексем. 2. Средняя строка (6 лексем), одиночный символразделитель, пропуск пус тых лексем. 3. Средняя строка, одиночный символразделитель, сохранение пустых лексем. 4. Длинная строка (60 лексем), одиночный символразделитель, пропуск пу стых лексем. 5. Средняя строка, разделительстрока, пропуск пустых лексем. 6. Средняя строка, разделительстрока, сохранение пустых лексем. 7. Длинная строка, разделительстрока, пропуск пустых лексем. 8. Средняя строка, набор символовразделителей, пропуск пустых лексем. 9. Длинная строка, набор символовразделителей, пропуск пустых лексем. Я тестировал следующие компоненты для разбиения строки при условии, что они поддерживают требуемую функциональность: A. stlsoft::string_tokeniser / charset_tokeniser (s=строка, v=строка) B. strtok() C. strtok_r() (специально написанная реализация) D. std::stringstream + std::getline() E. stlsoft::find_next_token() F. boost::tokenizer G. stlsoft::string_tokeniser / charset_tokeniser (s=строка, v=представление) H. stlsoft::string_tokeniser / charset_tokeniser (s=представление, v=строка) I. stlsoft::string_tokeniser / charset_tokeniser (s=представление, v=представление) Так как std::getline() и stlsoft::find_next_token() всегда сохраняют пустые лексемы, то я добавил простую проверку размера лексемы, чтобы имити ровать пропуск. Очевидно, обратное невозможно для компонентов, которые все
Разбиение строки
403
гда пропускают пустые лексемы. Процесс разбиения состоял из трех действий, повторенных 100 000 раз; измерялось суммарное время. Действия были функцио нально эквивалентны следующим операциям: std::copy(tokens.begin(), tokens.end(), . . . ); size_t n1 = std::distance(tokens.begin(), tokens.end()); size_t n2 = std::accumulate(tokens.begin(), tokens.end() , 0, invoke_length());
где invoke_length – бинарный объектфункция, который суммирует целочис ленный операнд с длиной строкового операнда. Поток, в котором производилось тестирование, выполнялся с высоким приоритетом, чтобы избежать помех со сто роны других процессов; время измерялось, начиная со второго повторения, чтобы избежать влияния процессорного кэша, подгрузки страниц памяти и т.д. Приве денные результаты относятся к компиляторам Visual C++ 7.1 и Intel C/C++ вер сии 8.0; тестировалось еще несколько компиляторов, но результаты во всех случа ях были похожи. Результаты (таблицы 27.1 и 27.2) представлены в виде процентной доли времени работы string_tokeniser (сценарии 16) или charset_tokeniser (сценарии 79) с S = S (строка), V = S (строка). В большинстве случаев результат не расходится с ожидаемым. На время отло жим в сторону варианты string_tokeniser, в которых используются строковые представления. Из остальных шести компонентов stlsoft::find_next_token() намного превосходит все остальные в тех сценариях, в которых применим. Неуди вительно, поскольку по существу это сочетание неизменяющих операций без ко пирования с делегированием ручной обработки (например, if(p1 != p0)) клиент скому коду. Понятно и то, что strtok() работает быстрее, чем strtok_r(), stlsoft:: string_tokeniser (когда и S, и V – строки), std::stringstream и boost:: tokenizer. Интересно, что strtok_r() работает быстрее string_tokeniser при разбиении по одиночному символуразделителю, но медленнее, когда задан набор символовразделителей. Полагаю, это связано с тем, что в реализации strtok_r() (имеется на компактдиске) используется библиотечная функция strpbrk(), по этому она не может быть оптимизирована, как встроенный код, целиком находя щийся в заголовочных файлах. Думаю, будет справедливо сказать, что std::stringstream не стоит исполь зовать для разбиения строк, каким бы ни был определяющий критерий: произво дительность или функциональная полнота. Этот компонент может составить кон куренцию другим в паре случаев, но эти случаи различны для разных компиляторов. Как и ожидалось, boost::tokenizer работает медленно для обо их компиляторов, что связано с его сложностью, и маловероятно, что он окажется главным кандидатом, когда разбиение производится по одиночному символу или по набору символов, а главный критерий – производительность. Но, как я уже отмечал, он обладает возможностями, которых другие компоненты вообще лише ны, а в этих случаях он, понятно, оказывается бесконечно быстрее.
1
47.8% 94.3% 22.7%
9.9%
5
28.8% 98.3% 17.3%
6
27.2% 99.8% 26.1%
7
РазделительZстрока
247.1% 56.4% 101.3% 33.0%
60.8% 132.7%
8
234.1% 45.8% 117.3% 38.6%
49.0% 140.3%
9
Набор символовZ разделителей
strtok() strtok_r() stringstream + getline() stlsoft::find_next_token() boost::tokenizer string_tokeniser (S, V) string_tokeniser (V, S) string_tokeniser (V, V)
Intel C/C++ 8.0
48.4% 55.8% 271.8% 202.6% 10.0% 15.3% 145.6% 23.9% 15.6% 81.8% 90.8% 7.5% 8.0% 17.5% 90.8% 11.0%
172.6% 12.9%
5
30.7% 39.8% 97.3% 15.4% 102.9% 9.2% 41.6% 93.9% 86.1% 8.5% 18.5%
4
41.1% 88.1% 18.6%
16.1 65.1% 15.0%
7
172.8% 48.5% 98.1% 27.0%
59.5% 137.0%
8
6
3
1
2
РазделительZстрока
Одиночный символZразделитель
118.8% 28.9% 89.6% 23.9%
36.2% 107.8%
9
Набор символовZ разделителей
Таблица 27.2. Сравнение производительности различных компонентов разбиения строки (Intel)
33.7% 42.6% 107.6% 152.3% 9.5% 4.6% 152.8% 10.7% 13.2% 97.6% 97.9% 8.1% 12.7%
51.1% 55.4% 227.7% 3.7% 198.2% 18.7% 91.3% 11.9%
4
3
2
Одиночный символZразделитель
strtok() strtok_r() stringstream + getline() 188.3% stlsoft::find_next_token() boost::tokenizer string_tokeniser (S, V) 18.9% string_tokeniser (V, S) 89.7% string_tokeniser (V, V) 8.2%
Visual C++ 7.1
Таблица 27.1. Сравнение производительности различных компонентов разбиения строки (Visual C++)
404
Наборы
Разбиение строки
405
На мой взгляд, наибольший интерес представляют случаи, где встречаются строковые представления. Использование обычного строкового типа (с семанти кой значения) в качестве строкового типа набора (S) вместо строкового представ ления снижает производительность на 10%, а в качестве строкового типа значения (V) – на 4090%. Если усреднить производительность по обоим компиляторам для тех случаев, где имеются данные, то окажется, что компонент string_tokeniser со строковыми представлениями по производительности сравним с find_next_ token(), в 3 раза быстрее strtok(), в 8.5 раз быстрее string_ tokeniser с обыч ными строками, в 10.5 раз быстрее boost::tokenizer и в целых 20 раз быстрее std::stringstream / std::getline(). А если представление используется только в качестве типа значения (V), то производительность снижается всего в два раза. Если нет необходимости преобразовывать значение в строку, завершающу юся нулем, или копировать его в обычную строку, то string_tokeniser с пред ставлениями строк демонстрирует впечатляющую комбинацию производитель ности и совместимости с STL.
27.10. Резюме Важно понимать, что достичь оптимального решения не всегда возможно. Причин может быть несколько: мы пытается «запихнуть» в один компонент слишком много функций; возможно, мы достигли локального максимума, и перспектива начать все с начала выглядит пугающей; необходимость поддерживать обратную совместимость; не всегда даже понятно, что лучшее решение существует, не говоря уже о том, как оно может выглядеть. В случае «строкоразбивателя» ясно, что мы уперлись в локальный максимум. Чтобы достичь для наборов символовразделителей такой же полноты и произво дительности, как для одиночного символа и разделителястроки, пришлось бы се рьезно переработать проект. Хотя существует проблема с понятностью списка па раметров шаблона, я долго противился искушению улучшить функциональность «разбивателя» просто потому, что он хорошо протестирован, прост в употреблении (в большинстве случаев) и очень быстро работает. Вопрос о полной переделке даже не поднимается, и не только потому, что пришлось бы заново убеждаться в коррек тности. Это как раз не самое страшное, особенно если строго применять зарекомен довавшие себя методики тестирования. Как раз в этом случае разработка на основе постоянного тестирования засверкала бы всеми гранями, так как по мере продвиже ния можно напрямую сравнивать старый и новый варианты. Гораздо сложнее спро ектировать заново нужную производительность после того, как компонент прошел такой длительный путь развития. Если честно, то о балансе между затратами и их возмещением тут даже говорить не приходится. (Разумеется, если найдется талант ливый читатель, который захочет проделать такую работу, я буду просто счастлив включить ее в STLSoft.) Наконец, и это, пожалуй, самое главное, приведенное опре деление шаблона charset_tokeniser, пусть и не блещет, но вполне эффективно. Иногда достаточно и прозаического решения.
Глава 28. Адаптация энумераторов COM Умная мысль, пусть даже ошибочная, мо% жет стать началом плодотворного исследо% вания, в результате которого будут добы% ты ценные знания. – Айзек Азимов Закопти мне селедочку, к завтраку буду! – Эйс Риммер, Красный карлик
28.1. Введение Одна из технических причин, по которым технология COM вышла из моды в этом тысячелетии, – исключительная многословность получающегося кода на C++. Две другие – сложность управления подсчетом ссылок и относительная лег кость, с которой пользователь может допустить ошибку в применении правил владения ресурсом. То и другое может приводить к утечкам и/или двойному уда лению. Особенно проблематичны в этом отношении энумераторы COM и наборы COM. Добавьте сюда еще тот факт, что, будучи языковонезависимыми, методы, определенные в COMинтерфейсе, не могут возбуждать исключений, тогда как обычным функциям и методам, написанным на C++, это отнюдь не запрещено. В этой главе мы остановимся на энумераторах COM, определенных протоко лом IEnumXXXX, и обсудим шаблонный класс enumerator_sequence из библио теки COMSTL, который элегантно инкапсулирует все негативные аспекты эну мераторов COM в STL%наборе (раздел 2.2). Мы увидим, что энумераторы COM естественным образом не поддерживают ни итераторы ввода, ни однонаправлен% ные итераторы (раздел 1.3), и познакомимся с техникой, позволяющей надежно и осмысленно поддержать ту или другую семантику в соответствии с указаниями пользователя. Мы узнаем, в чем опасность кэширования элементов в итераторах. И увидим, как можно использовать политики типа значения, чтобы приспособить последовательность для манипулирования типами с различной семантикой ини циализации, копирования и уничтожения.
28.2. Мотивация Как уже повелось, начнем со сравнения исходного API и STLнабора, отметив плюсы и минусы того и другого подхода. Для примера возьмем COMобертку библиотеки recls, которая позволяет осуществлять рекурсивный поиск в файло
Адаптация энумераторов СОМ
407
вой системе из любого COMсовместимого языка. Интерфейс энумератора recls/ COM, описываемый классом IEnumFileEntry, позволяет перебирать элементы файловой системы (представленные интерфейсом IFileEntry).
28.2.1. Длинная версия Для краткости предположим, что имеется функция open_search(), начинаю щая поиск, и объявлена она следующим образом: HRESULT open_search(char const* , char const* , long , IEnumFileEntry**
directory patterns flags ppenum) throw();
Эта функция принимает три параметра и возвращает экземпляр объекта эну мератора, от которого можно получить результаты поиска. Семантика парамет ров directory, patterns и flags такая же, как для всех языковых привязок recls: directory – каталог, от которого начинается поиск; patterns – один или не сколько образцов поиска, разделенных вертикальной чертой, например "*.cpp|*.hpp|?akefile"; flags – комбинация флагов, описывающая, нужно искать файлы или каталоги, требуется ли рекурсия и т.д. Следуя соглашениям COM, функция возвращает значение типа HRESULT, то есть 32битовое целое со знаком, в котором выделены группы битов, представляющих успех/ошибку, кате горию ошибки и код ошибки. В COM определены макросы SUCCEEDED() и FAILED(), позволяющие понять, соответствует ли данное значение типа HRESULT успеху или ошибке, без дальнейшей детализации. Предположим, что мы собираемся воспользоваться recls/COM для того, что бы найти все исходные файлы на C++ в текущем каталоге. (Обратите внимание на шаблонную функцию comstl::propget_cast, которая позже поможет нам со кратить трафаретный код. Она вызывает указанный метод и возвращает получен ное значение того типа, которым специализирован шаблон.) Листинг 28.1. Использование энумератора COM с помощью стандартных интерфейсов 1 2 3 4 5 6 7 8 9 10 11 12 13 14
IEnumFileEntry* HRESULT
pe; hr = , , ,
open_search("." "*.c|*.cpp|*.h|*.hpp|*.d|*.rb|*.java|*.cs|*.py" RECLS_F_FILES | RECLS_F_RECURSIVE &pe);
if(SUCCEEDED(hr)) { IFileEntry* entries[5]; ULONG numRetrieved = 0; for(pe->Reset(); SUCCEEDED(pe->Next(STLSOFT_NUM_ELEMENTS(entries) , &entries[0], &numRetrieved)); ) { if(0 == numRetrieved) {
Наборы
408 15 16 17 18 19 20 21 22 23 24 25 }
break; } for(ULONG i = 0; i < numRetrieved; ++i) { std::wcout Release();
Что здесь делается, мы детально рассмотрим чуть ниже. А пока замечу, что этот код не только непрозрачен, но к тому же хрупок и в двух местах не безопасен относительно исключений.
28.2.2. Короткая версия В листинге 28.2 приведена короткая версия, в которой используются шабло ны comstl::enumerator_sequence и comstl::interface_policy. Листинг 28.2. Использование энумератора COM с помощью enumerator_sequence 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
IEnumFileEntry* HRESULT
pe; hr = , , ,
open_search("." "*.c|*.cpp|*.h|*.hpp|*.d|*.rb|*.java|*.cs|*.py" RECLS_F_FILES | RECLS_F_RECURSIVE &pe);
if(SUCCEEDED(hr)) { typedef comstl::enumerator_sequence enum_t; enum_t entries(pe, false, 5); // Ãëîòàåì ññûëêó; ïàêåòû ïî 5 for(enum_t::iterator b = entries.begin(); entries.end() != b; ++b) { std::wcout Next(1, &punk, NULL); ) {
Адаптация энумераторов СОМ
411
punk->Release(); }
Экземпляр BSTR следует передать функции SysFreeString(): BSTR bstr; IEnumBSTR* pen = . . . for(pen->Reset(); S_OK == pen->Next(1, &bstr, NULL); ) { ::SysFreeString(bstr); }
Экземпляр LPOLESTR CoTaskMemFree():
необходимо
освободить
с
помощью
функции
LPOLESTR str; IEnumString* pen = . . . for(pen->Reset(); S_OK == pen->Next(1, &str, NULL); ) { ::CoTaskMemFree(str); }
А экземпляр VARIANT надлежит передать функции VariantClear(): VARIANT var; IEnumVARIANT* pen = . . . for(pen->Reset(); S_OK == pen->Next(1, &var, NULL); ) { ::VariantClear(&var); }
Любой класс, пытающийся абстрагировать интерфейсы энумератора, обязан обрабатывать различные типы ресурсов.
28.4. Анализ длинной версии Вооружившись знаниями о COM, мы теперь можем внимательно присмот реться к длинной версии, показанной в листинге 28.1. Строка 8. Объявляется массив из пяти указателей на IFileEntry, чтобы можно было получать от Next() целый блок элементов. Хотя я могу сообщить, что recls/COM создает объекты в том же потоке, где вызывается, в общем случае вы не вправе делать такие допущения. А раз так, то извлечение за один раз, ска жем от 5 до 10 элементов, будет разумным компромиссом между наблюдаемым временем реакции программы (тем выше, чем больше элементов запрошено) и общей производительностью (за счет снижения накладных расходов на обраще ния по сети). Строка 10. Сбрасываем энумератор. В данном случае это необязательно, но, вообще говоря, полезно, если мы хотим быть уверены, что начинаем с начала. Строки 10–11. Вызываем Next(), запрашивая столько элементов, сколько есть места в массиве entries. Их количество определяется макросом STLSOFT_NUM_ ELEMENTS() (раздел 5.1) в соответствии с принципом DRY SPOT (глава 5), чтобы защититься от случайных несогласованных изменений в ходе сопровождения.
412
Наборы
Совет. Задавайте размеры массивов с помощью конструкций, автоматизирующих вы) числение размера, например STLSOFT_NUM_ELEMENTS(), чтобы избежать опасностей, сопутствующих магическим числам.
Строки 13–16. Даже если вызов Next() завершился успешно, количество по лученных элементов может быть равно 0, и тогда нужно выйти из цикла. Строки 17–22. Поочередно обрабатываем каждый из возвращенных элемен тов (от одного до пяти). Строки 19–20. Получаем путь с помощью метода IFileEntry::get_Path() и передаем его оператору вставки в поток. Если этот оператор возбудит исключе ние, полученные указатели на элементы не освобождаются. Так, если возвращено четыре элемента, а исключение возникло на второй итерации внутреннего цикла, то три объекта останутся не освобожденными. Строка 21. Освобождаем элемент, обращаясь к его методу IUnknown:: Release(). Строка 24. Освобождаем энумератор, обращаясь к его методу IUnknown:: Release(). Если между возвратом из open_search() и этим вызовом возникнет исключение, объект, представляющий энумератор, не будет освобожден. Много слов, но мало действий. И существует несколько мест, где есть шанс оступиться. В таком коде можно допустить по меньшей мере четыре типичных ошибки: 1. Сравнивать код возврата с S_OK, вместо того чтобы проверять, возвращает ли макрос SUCCEEDED() значение true; если всего было 13 файлов, то тре тий вызов метода Next() вернет S_FALSE (1), и, значит, мы обработаем только первые 10 элементов, а три ссылки останутся не освобожденными – утечка памяти. 2. Передать NULL в качестве параметра numRetrieved в случае, когда numRequested не равно 1. 3. Забыть проверить, равно ли нулю количество возвращенных элементов; это приведет к зацикливанию. 4. Забыть об освобождении возвращенного ресурса, в данном случае – о вы зове IUnknown::Release() для каждого указателя на элемент. Для всех этих ошибок существенно то, что они не проявляются до момента выполнения, а некоторые могут оставаться незамеченными в течение длительно го времени. Даже если все сделано правильно, как в примере выше, код все равно небезопасен относительно исключений. И, конечно, при использовании энумера торов циклы требуется писать вручную, а это исключает применение алгоритмов.
28.5. Класс comstl::enumerator_sequence Шаблонный класс enumerator_sequence из библиотеки COMSTL решает все эти проблемы за нас. Он конструируется из интерфейса энумератора, от кото рого получает элементы, а нам предоставляет интерфейс STLнабора для доступа
Адаптация энумераторов СОМ
413
к этим элементам. Он управляет ресурсами, в том числе указателем на интерфейс энумератора, гарантирует правильное освобождение в деструкторе и безопасен относительно исключений.
28.5.1. Открытый интерфейс Открытый интерфейс класса enumerator_sequence показан в листинге 28.3. Листинг 28.3. Определение класса enumerator_sequence //  ïðîñòðàíñòâå èìåí comstl template< typename I // Èíòåðôåéñ ýíóìåðàòîðà , typename V // Òèï çíà÷åíèÿ , typename VP // Òèï ïîëèòèêè çíà÷åíèÿ , typename R = V const& // Òèï ññûëêè , typename CP = input_cloning_policy // Òèï ïîëèòèêè êëîíèðîâàíèÿ , size_t Q = 10 // Êîëè÷åñòâî > class enumerator_sequence { public: // Òèïû-÷ëåíû typedef I interface_type; typedef V value_type; typedef value_policy_adaptor value_policy_type; typedef R reference; typedef ???? pointer; typedef CP cloning_policy_type; typedef typename CP::iterator_tag_type iterator_tag_type; enum { retrievalQuanta = Q }; typedef enumerator_sequence class_type; typedef class_type sequence_type; typedef size_t size_type; class iterator; public: // Êîíñòðóèðîâàíèå enumerator_sequence(interface_type* i , bool bAddRef , size_type quanta = 0 , bool bReset = true); ~enumerator_sequence() throw(); public: // Èòåðàöèÿ iterator begin() const; iterator end() const; };
Первым делом нужно отметить список параметров шаблона, в котором аж шесть параметров. В главе 14 мы говорили, что всегда нужно помнить о гипотезе Хенни, и сейчас мы подвергаем ее серьезному испытанию. К счастью, для после дних трех параметров по умолчанию выбраны такие типы или значения, которые подходят для большинства случаев применения этой последовательности; мы рассмотрим этот вопрос ниже. Три параметра, не имеющие значения по умолча нию, – это интерфейс энумератора (I), тип значения (V) и тип политики значения (VP).
414
Наборы
28.5.2. Типы и константы"члены Как обычно, я использую параметры шаблона для определения типовчленов: interface_type, value_type, reference и cloning_policy_type. Параметр VP применяется для специализации шаблона value_policy_adaptor, о котором речь ниже, и таким образом определяет типчлен value_policy_type. У типа pointer своеобразное определение, которое мы обсудим в интерлюдии, следую
щей за этой главой. Все остальные типычлены согласуются со стандартом, за ис ключением типа iterator_tag_type, который наследует одноименному типу члену политики клонирования и ниже используется в объявлении класса итератора. Константачлен retrievalQuanta – это перечисление, которому при сваивается значение параметра шаблона Q. Тем самым клиентский код получает доступ к значению, заданному в виде параметра шаблона.
28.5.3. Политики значений Поскольку различные типы значений предъявляют разные требования к уп равлению ресурсами, мы используем политику, чтобы определить способ обра ботки значений конкретного типа. В каждой политике имеются три статические функции init(), copy() и clear(). В листинге 28.4 приведены определения трех готовых политик. Все готовые политики помещены в пространство имен comstl. Отметим, что методы init() и clear() не возбуждают исключений, а copy() должен иметь такую возможность. Почему, мы увидим ниже. Листинг 28.4. Готовые политики значений, предоставляемые библиотекой COMSTL //  ïðîñòðàíñòâå èìåí comstl struct GUID_policy { public: // Òèïû-÷ëåíû typedef GUID value_type; public: // Îïåðàöèè static void init(value_type*) throw() {} static void copy(value_type* dest, value_type const* src) { *dest = *src; } static void clear(value_type*) throw() {} }; struct VARIANT_policy { public: // Òèïû-÷ëåíû typedef VARIANT value_type; public: // Îïåðàöèè static void init(value_type* p) throw() {
Адаптация энумераторов СОМ
415
::VariantInit(p); } static void copy(value_type* dest, value_type const* src) { HRESULT hr = ::VariantCopy(dest, const_cast(src)); if(FAILED(hr)) { throw com_exception("îøèáêà ïðè êîïèðîâàíèè VARIANT", hr); } } static void clear(value_type* p) throw() { ::VariantClear(p); } }; template struct interface_policy { public: // Òèïû-÷ëåíû typedef I interface_type; typedef interface_type* value_type; public: // Îïåðàöèè static void init(value_type* p) throw() { *p = NULL; } static void copy(value_type* dest, value_type const* src) { *dest = *src; if(NULL != *dest) { (*dest)->AddRef(); } } static void clear(value_type* p) throw() { if(NULL != *p) { (*p)->Release(); *p = NULL; } } };
Для простой структуры GUID, которая не управляет ресурсами, методы init() и clear() ничего не делают, а копирование сводится к присваиванию, в результате которого старое значение заменяется. Сравните с политикой VARIANT_policy, где занимаемую память необходимо инициализировать функ цией VariantInit() и очищать функцией VariantClear(), а копирование вы полнять с помощью функции VariantCopy(). Третья политика interface_ policy – это шаблонный класс, параметризованный заданным интерфейсом; эк земпляры копируются путем вызова AddRef() и уничтожаются обращением к Release().
416
Наборы
Наверное, вам интересно, почему эти политики адаптируются с помощью value_policy_adaptor. Причина станет ясна, если взглянуть на реализацию вложенного класса enumerator_sequence::iterator::enumeration_context. Как показано в листинге 28.5, в двух методах init_elements_() и clear_ elements_() для обработки целой группы элементов применяется стандартный алгоритм for_each().
Листинг 28.5. Методы класса enumeration_context . . . struct enumeration_context { . . . void init_elements_(size_type n) throw() { COMSTL_ASSERT(n AddRef(); } COMSTL_ASSERT(is_valid()); } ~collection_sequence() throw() { COMSTL_ASSERT(is_valid()); m_root->Release(); } . . .
i bAddRef quanta = 0)
30.3.4. Итерация: чистое применение грязного трюка Полная реализация первоначальной версии collection_sequence была по чти идентична реализации enumerator_sequence; демон копирования и вставки в тот день торжествовал. Естественно, сопровождать такой код врагу не пожела ешь, поэтому в конце концов я решился вынести общие части в отдельный компо нент. Но мне очень не хотелось делать общими типы итераторов в случае, когда итератор управляет ресурсом, поскольку для этого либо пришлось бы объявить открытым то, что должно быть закрытым конструктором (преобразования), либо объявить отношение дружественности между заголовочными файлами. Ни то, ни другое не вяжется с моими представлениями о правильном C++коде. (Конечно, оба варианта все же лучше, чем независимые копии такого сложного кода, как enumerator_sequence::iterator). К счастью, иногда стоит отложить проблему, и решение приходит само собой. В данном случае меня осенило, что ни одна из этих неприятных мер не нужна. (Можно сказать и так: «Я уснул уставшим, а проснулся ленивым».) В данном слу чае я могу воспользоваться обычным грязным трюком – получить экземпляры итераторов от временного экземпляра набора, как показано в листинге 30.6 (обра ботка ошибок для краткости опущена). Листинг 30.6. Методы итерации collection_sequence::iterator begin() const { COMSTL_ASSERT(is_valid()); LPUNKNOWN punkEnum; HRESULT hr = enumerator_acquisition_policy_type::acquire(m_root , &punkEnum); if(SUCCEEDED(hr)) {
Адаптация наборов СОМ
455
enumerator_interface_type* ei; hr = punkEnum->QueryInterface( IID_traits<enumerator_interface_type>::iid() , reinterpret_cast(&ei)); punkEnum->Release(); if(SUCCEEDED(hr)) { COMSTL_ASSERT(is_valid()); return enum_seq_type(ei, false, m_quanta).begin(); // Âðåìåííûé! } else . . . } collection_sequence::iterator end() const { COMSTL_ASSERT(is_valid()); return iterator(); }
Почему так можно? Просто потому, что я сам писал оба STLнабора и знаю, что это пройдет. Однако такое объяснение мало что проясняет. Официальная при чина изложена в документации по классу enumerator_sequence: /// \note Èòåðàòîðû, âîçâðàùåííûå ìåòîäàìè begin()/end(), äåéñòâèòåëüíû /// è ïîñëå óíè÷òîæåíèÿ ýêçåìïëÿðà íàáîðà, îò êîòîðîãî îíè ïîëó÷åíû
Еще не существует механизма кодифицировать эту концепцию – действи тельность итераторов за пределами жизни экземпляра набора, – поэтому пока должно хватить и такой документации. Но существует способ проверить, поддер живают ли эту концепцию итераторы типа класса. В классе набора можно хранить список активных итераторов и, если этот список не пуст в момент уничтожения, то сообщать об этом в утверждении assert или оставлять записи в протоколе. Есте ственно, такой механизм следует делать необязательным и включать его имеет смысл только в отладочных или тестовых версиях. В некотором смысле это просто формализация дырявой абстракции (глава 6); наш грязный трюк на самом деле чист, так как мы пользуемся документирован ной особенностью набора enumerator_sequence. Но с тем же успехом можно ска зать, что мы знаем, что итератор класса std::vector непрерывный, а класса std::deque – «только» с произвольным доступом. Возникает вопрос, не может ли строка, в которой создается временный экзем пляр enumerator_sequence привести к утечке счетчика ссылок, ассоциирован ного с ei. К счастью, нет. В разделе 28.5.5 мы говорили, что конструктор enumerator_sequence гарантированно не возбуждает исключений. Если исклю чение возникнет в методе begin(), то экземпляр enumerator_sequence будет уничтожен, так как C++ гарантирует уничтожение всех не полностью сконструи рованных объектов в процессе обработки исключения. Поэтому все абсолютно нормально, хотя и выглядит странновато. (Если вам интересно, то однострочную форму я использовал для того, чтобы недвусмысленно указать: итератор получен от временного экземпляра.)
456
Наборы
30.3.5. Замечание по поводу метода size() Класс enumerator_sequence, как и многие другие изученные нами классы последовательностей, не мог предоставить метод size() изза того, что время его работы было бы заведомо не постоянным, и потому что от энумераторов COM не требуется, чтобы они перебирали те же самые или хотя бы какието элементы при повторном обходе. Я уже упоминал в начале главы, что все члены наборов COM, кроме _NewEnum, необязательны. Вспомните еще – если это не отпечаталось в вашем мозгу навечно, – что компилятор C++ инстанцирует только то, что используется. Следовательно, в принципе можно предоставить реализации необязательных методов COMна боров таким образом, что если какойто конкретный набор их не поддерживает, то никаких проблем не возникнет (если, конечно, мы не попытаемся воспользовать ся несуществующими методами). Проблема в том, что в разных наборах определе ния этих методов слишком сильно различаются. Так, даже при беглом просмотре заголовочных файлов из Microsoft Platform SDK обнаруживаются такие сигнату ры (и это лишь первые несколько интерфейсов). Методы Add() принимают long и возвращают IBodyPart*, или не прини мают никаких параметров и возвращают IDispatch*, или принимают VARIANT* + BSTR и не возвращают ничего, или принимают BSTR + VARIANT + VARIANT и возвращают SnapIn**. Методы Item() принимают long и возвращают IMessage*, или принима ют BSTR и возвращают VARIANT, или принимают VARIANT и возвращают IDispatch*. Методы Remove() принимают long, или BSTR, или VARIANT. Метод Add() определенно отправляется в корзину «Слишком сложно». А поддержка методов Item() и Remove() потребовала бы много усилий, но ре зультат вряд ли оправдал бы их; оставляю в качестве упражнения для читателей. Единственные члены, которые, если уж присутствуют, то имеют одинаковые сиг натуры, – это метод Clear() и свойство Count: struct ISomeCollection : public IDispatch { . . . virtual HRESULT Clear() = 0; virtual HRESULT get_Count(long* pCount) = 0; . . .
Вряд ли разумно предоставлять для набора метод clear(), если в нем нет ни каких методов добавления. А вот свойством Count воспользоваться не грех, по этому в классе collection_sequence определен метод size(), показанный в ли стинге 30.7 (вместе с комментарием для генерирования документации). Листинг 30.7. Метод size() . . . public: // Ðàçìåð
Адаптация наборов СОМ
457
/// Âîçâðàùàåò êîëè÷åñòâî ýëåìåíòîâ â íàáîðå /// /// \note Ýòîò ìåòîä íå êîìïèëèðóåòñÿ, åñëè â èíòåðôåéñå íàáîðà /// îòñóòñòâóåò ìåòîä get_Count() size_type size() const { COMSTL_ASSERT(is_valid()); ULONG count; HRESULT hr = m_root->get_Count(&count); COMSTL_ASSERT(is_valid()); return SUCCEEDED(hr) ? count : 0; } . . .
Если в интерфейсе набора определено свойство Count, то можно вызвать ме тод size(), чтобы заранее узнать, сколько элементов находится в диапазоне меж ду begin() и end(). В противном случае попытка обратиться к этому методу при ведет к ошибке компиляции.
30.4. Политики получения энумератора Я уже говорил, что набор обязан предоставлять только член _NewEnum, и это может быть либо метод, либо свойство. Иными словами, в C++ сигнатура может быть такой: interface ISomeCollectionOrOther : public IDispatch { virtual HRESULT get__NewEnum(LPUNKNOWN* ppunk) = 0; }
или такой: interface ISomeCollectionOrOther : public IDispatch { virtual HRESULT _NewEnum(LPUNKNOWN* ppunk) = 0; }
Причина несогласованности в том, что поначалу наборы поддерживали толь ко интерфейс IDispatch, а не дуальные интерфейсы. Дуальным называется ин терфейс, который наследует IDispatch и, следовательно, поддерживает позднее связывание (на этапе выполнения), но также явно определяет методы и свойства COM в виде C++–методов производного класса для удобства работы из языка, который (как C++) способен поддержать ранее связывание (на этапе компиля ции). Оба приведенных выше варианта для набора IsomeCollectionOrOther, – это дуальные интерфейсы. Их можно вызывать как напрямую, так и с помощью метода Invoke() родительского интерфейса IDispatch при условии, что извес тен диспетчерский интерфейс (DispId) этого члена. К счастью, DispId для _NewEnum имеет стандартное значение DISPID_NEWENUM (-4). «Ну и что?», спросите вы. Да просто именно для этого и нужен последний па раметр шаблона: политика получения энумератора. В библиотеке COMSTL опре
458
Наборы
делены три такие политики. Политика new_enum_property_policy предполага ет, что член _NewEnum определен на языке IDL как свойство, и потому вызывает метод get__NewEnum() (листинг 30.8). В большинстве встречавшихся мне интер фейсов COMнаборов, включая имеющиеся в Microsoft Platform SDK, _NewEnum – свойство, поэтому эта политики принимается по умолчанию. Я оставил коммен тарий, поскольку именно сюда отправит пользователя среда разработки, если он определит интерфейс, в котором нет свойства _NewEnum. Листинг 30.8. Определение класса new_enum_property_policy template struct new_enum_property_policy { static HRESULT acquire(CI* pcoll, LPUNKNOWN* ppunkEnum) { COMSTL_ASSERT(NULL != pcoll); COMSTL_ASSERT(NULL != ppunkEnum); // Åñëè êîìïèëÿòîð ñîîáùàåò, ÷òî â âàøåì èíòåðôåéñå íåò // ìåòîäà get__NewEnum, òî: // - âû ïåðåäàåòå ÷èñòûé èíòåðôåéñ IDispatch, ïîýòîìó ñëåäóåò // ïîëüçîâàòüñÿ ïîëèòèêîé new_enum_by_dispid_policy, èëè // - âû ïåðåäàåòå èíòåðôåéñ íàáîðà, â êîòîðîì _NewEnum îïðåäåëåí êàê ìå// òîä, ïîýòîìó ñëåäóåò ïîëüçîâàòüñÿ ïîëèòèêîé new_enum_method_policy, èëè // - âû ïåðåäàåòå íå òîò èíòåðôåéñ. Ïðîâåðüòå, íå óêàçàëè ëè âû // â ñïåöèôèêàöèè comstl::collection_sequence íåäîïóñòèìûé èíòåðôåéñ. return pcoll->get__NewEnum(ppunkEnum); } };
Альтернативный случай, когда _NewEnum – метод, обрабатывается политикой new_enum_method_policy, как показано в листинге 30.9. Листинг 30.9. Определение класса new_enum_method_policy template struct new_enum_method_policy { static HRESULT acquire(CI* pcoll, LPUNKNOWN* ppunkEnum) { COMSTL_ASSERT(NULL != pcoll); COMSTL_ASSERT(NULL != ppunkEnum); return pcoll->_NewEnum(ppunkEnum); } };
Если вы не знаете, с каким типом работаете, или точно знаете, что набор под держивает только интерфейс IDispatch, то можете воспользоваться политикой new_enum_by_dispid_policy, показанной в листинге 30.10. Она несколько слож нее остальных, так как должна запросить интерфейс IDispatch, вызвать очень громоздкий метод Invoke() и получить интерфейс энумератора из возвращенно го значения типа VARIANT.
Адаптация наборов СОМ
459
Листинг 30.10. Определение класса new_enum_by_dispid_policy template struct new_enum_by_dispid_policy { static HRESULT acquire(CI* pcoll, LPUNKNOWN* ppunkEnum) { COMSTL_ASSERT(NULL != pcoll); COMSTL_ASSERT(NULL != ppunkEnum); LPDISPATCH pdisp; HRESULT hr = pcoll->QueryInterface(IID_IDispatch , reinterpret_cast(&pdisp)); if(SUCCEEDED(hr)) { DISPPARAMS params; UINT argErrIndex; VARIANT result; ::memset(¶ms, 0, sizeof(params)); ::VariantInit(&result); hr = pdisp->Invoke( DISPID_NEWENUM, IID_NULL , LOCALE_USER_DEFAULT , DISPATCH_METHOD | DISPATCH_PROPERTYGET , ¶ms, &result , NULL, &argErrIndex); pdisp->Release(); if(SUCCEEDED(hr)) { hr = ::VariantChangeType(&result, &result, 0, VT_UNKNOWN); if(SUCCEEDED(hr)) { if(NULL == result.punkVal) { hr = E_UNEXPECTED; } else { *ppunkEnum = result.punkVal; (*ppunkEnum)->AddRef(); } } ::VariantClear(&result); } } return hr; } };
В этой версии неважно, определен ли член _NewEnum как метод или как свой ство. Он просто вызывается по DispId. Но (как же без «но») не думайте, что это панацея. Если в библиотеке типов нет описания интерфейса набора, а в компонен те применяется маршалинг библиотеки типов (так обычно и поступают ком поненты автоматизации), то вызов Invoke() завершится с ошибкой TYPE_E_ ELEMENTNOTFOUND, что на человеческом языке означает «Элемент не найден». (Чтобы библиотека типов раскрывала этот интерфейс, нужно поместить предло жение interface IMyInterface; внутрь блока library в IDLописании.)
460
Наборы
30.5. Резюме Если вы, как и я, не являетесь страстным поклонником COM (а надо при знать, что только родная мать может любить такое болтливое и хрупкое дитя), то вам будет приятно узнать, что больше мы к обсуждению COM в этой книге не вернемся. Но даже если вы не оценили всех прелестей COM, надеюсь, что вы из влекли уроки из рассмотренных приемов и ухищрений и сумеете применить их в более общей ситуации. Мы познакомились с несколькими идеями: шаблонный класс на основе политик позволяет превратить архаичную, многословную и небезопасную относительно исключений модель про граммирования в безопасный и лаконичный клиентский код. Шаблонный интерфейс не так понятен, как хотелось бы, но к интерфейсу класса в этом смысле претензий нет. И готов поспорить, что разобраться в политиках, которыми специализируется шаблон, куда проще, чем написать коррект ный и безопасный относительно исключений код на «голом» COM; если абстракция протекает слишком сильно, то можно получить итераторы одного STLнабора от временного экземпляра другого STLнабора. Прав да, эта методика годится не во всех случаях; можно инкапсулировать синтаксические несогласованности интерфейсов в обобщенном компоненте, применяя средства, работающие на этапе ком пиляции и/или выполнения, хотя и не без помощи динамической типиза ции на базе IDispatch::Invoke(). (Если я не отбил у вас навсегда охоту обращаться к COM из C++, можете по знакомиться с библиотекой VOLE, которая есть на компактдиске. Она позволяет надежно и лаконично обращаться к серверам COMавтоматизации. Пользоваться ей очень легко, и почти все хитрости автоматизации остаются от вас скрыты.)
Глава 31. Ввод/вывод с разнесением и сбором Скажи австралийцу, что здесь нельзя бить чечетку, и он тут же побежит покупать туфли с набойками. – Саймон Бриггс
31.1. Введение Не слушайте тех, кто говорит, что STL – штука красивая и абстрактная, вот только с производительностью у нее плоховато. Я уже не раз разоблачал этот миф (разделы 17.1.1, 19.1.1, 27.9, 36.2.2 и 38.2.1), а в этой главе не оставлю от него кам ня на камне. Предмет данной главы – ввод/вывод с разнесением и сбором, то есть обмен данными между прикладной программой и подсистемой ввода/вывода (обычно в ядре) в виде нескольких блоков, передаваемых за одну операцию. Основное на значение – возможность манипулировать заголовками пакетов отдельно от их полезной нагрузки, но этой методике, дающей заметный выигрыш в производи тельности, можно найти и другие применения. Правда, приходится платить ус ложнением процедуры обработки изза того, что логически смежные данные на ходятся в физически разнесенных областях памяти. Чтобы справиться с этой ситуацией, мы можем воспользоваться классами, которые предоставляют абст ракцию, позволяющую считать, что данные размещены линейно. Поскольку эта книга посвящена расширениям STL, то и абстракция будет иметь вид STL%набора (раздел 2.2) и ассоциированных с ним диапазонов итераторов. Но, как вы убеди тесь, за такие абстракции приходится платить, поэтому мы пойдем дальше и по смотрим, как можно оптимизировать передачу информации, не жертвуя мощью абстракции. Попутно мы познакомимся с правилами замещения функций, опре деленных в пространстве имен std, и с тем, как увязать одно с другим.
31.2. Ввод/вывод с разнесением и сбором Ввод/вывод с разнесением и сбором – это способ обмена информацией между API ввода/вывода и клиентской программой, при котором данные находятся в физически несмежных областях памяти. Например, функции readv() и writev()
462
Наборы
работают так же, как read() и write(), но вместо указателя на одну область па мяти принимают массив структур типа iovec: struct iovec { void* iov_base; size_t iov_len; }; ssize_t readv(int fd, const struct iovec* vector, int count); ssize_t writev(int fd, const struct iovec* vector, int count);
В API сокетов в Windows есть аналогичная структура и соответствующие функции: struct WSABUF { u_long len; char* buf; }; int WSARecv(SOCKET s , WSABUF* lpBuffers , DWORD dwBufferCount , . . . // È åùå 4 ïàðàìåòðà); int WSASend(SOCKET s , WSABUF* lpBuffers , DWORD dwBufferCount , . . . // È åùå 4 ïàðàìåòðà); int È åùå 6 ïàðàìåòðîâ (SOCKET s , WSABUF* lpBuffers , DWORD dwBufferCount , . . . // È åùå 6 ïàðàìåòðîâ); int WSASendTo( SOCKET s , WSABUF* lpBuffers , DWORD dwBufferCount , . . . // È åùå 6 ïàðàìåòðîâ);
Может возникнуть вопрос, зачем нужно выполнять ввод/вывод таким слож ным способом. Ответ прост – если файл или сетевые данные представлены в фик сированном формате, то можно читать или выводить одну или несколько записей (пакетов) без всякого перемещения, переформатирования и слияния. Это удобно. Аналогично, если формат записей (пакетов) переменный, но размер заголовка фиксирован, то можно считывать или записывать заголовок непосредственно в соответствующую ему структуру, а все остальное рассматривать как непрозрач ные двоичные данные переменного размера. И третья причина – производитель ность. Както я разработал архитектуру сетевого сервера, в которой использовал ся ввод/вывод с разнесением и сбором и схема многопоточного выделения памяти без блокировок. (Достаточно сказать, что работал он весьма шустро.) Но какова бы ни была причина использования ввода/вывода с разнесением и сбором, получающийся клиентский код сложен, непрозрачен и чреват ошибками. Нужна эффективная абстракция.
Ввод/вывод с разнесением и сбором
463
31.3. API ввода/вывода с разнесением и сбором 31.3.1. Линеаризация с помощью COM"потоков Сложность ввода/вывода с разнесением и сбором в том, что работа с данными, разбросанными по нескольким блокам памяти, – нетривиальная задача. В разных проектах (на платформе Windows) на протяжении 1990х годов я в основном пользовался реализациями COMпотоков из закрытых библиотек своей компа нии, которые разрабатывались несколькими годами раньше совсем для другой за дачи. Скажу несколько слов об архитектуре COMпотоков. (Я помню про свое обещание больше не возвращаться к COM, но эта тема будет интересна даже «упертым юниксоидам». Доверьтесь мне, я врач!) COMпоток – это абстракция, надстроенная над некоторой средой хранения, имеющая много общего с привычной абстракцией файла. По существу, поток пре доставляет доступ к хранилищу и определяет текущую точку в логическом эк стенте. Потоковый объект раскрывает интерфейс IStream (его сокращенная фор ма приведена в листинге 31.1), в котором определен ряд методов, в том числе Seek(), SetSize(), Stat() и Clone(). Имеются также методы для получения ис ключительного доступа к областям хранилища. Интерфейс IStream наследует ISequentialStream (тоже показан в листинге 31.1), в котором есть два метода: Read() и Write(). Вы можете реализовать поток над конкретным хранилищем, унаследовав IStream и предоставив определения его методов. Листинг 31.1. Определения интерфейсов ISequentialStream и IStream interface ISequentialStream : public IUnknown { virtual HRESULT Read(void* p, ULONG n, ULONG* numRead) = 0; virtual HRESULT Write(void const* p, ULONG n, ULONG* numWritten) = 0; }; interface IStream : public ISequentialStream { virtual HRESULT Seek(. . .) = 0; virtual HRESULT SetSize(. . .) = 0; virtual HRESULT CopyTo(. . .) = 0; virtual HRESULT Commit(. . .) = 0; virtual HRESULT Revert(. . .) = 0; virtual HRESULT LockRegion(. . .) = 0; virtual HRESULT UnlockRegion(. . .) = 0; virtual HRESULT Stat(. . .) = 0; virtual HRESULT Clone(. . .) = 0; };
В COM определена еще одна относящаяся к потокам абстракция в виде интер фейса ILockBytes (в сокращенном виде показан в листинге 31.2). Он представля ет произвольное хранилище как непрерывный массив байтов. Никакого состоя
464
Наборы
ния, в котором могла бы храниться информации о текущей позиции, в нем не пре дусмотрено. Отсюда и методы ReadAt() и WriteAt() вместо Read() и Write(). Листинг 31.2. Определение интерфейса ILockBytes interface ILockBytes : public IUnknown { virtual HRESULT ReadAt( ULARGE_INTEGER pos, void* p , ULONG n, ULONG* numRead) = 0; virtual HRESULT WriteAt(ULARGE_INTEGER pos, void const* p , ULONG n, ULONG* numWritten) = 0; virtual HRESULT Flush() = 0; virtual HRESULT SetSize(. . .) = 0; virtual HRESULT LockRegion(. . .) = 0; virtual HRESULT UnlockRegion(. . .) = 0; virtual HRESULT Stat(. . .) = 0; };
Довольно просто реализовать COMпоток в терминах интерфейса ILockBytes (точнее, объекта, который предоставляет соответствующие методы). Все, что нужно, – это указатель ILockBytes* и текущая позиция. Моя компания так и по ступила, написав функцию CreateStreamOnLockBytes(): HRESULT CreateStreamOnLockBytes(ILockBytes* plb, unsigned flags , IStream** ppstm);
Возникает очевидный вопрос: «Откуда берется объект ILockBytes?» И для этого есть функция – CreateLockBytesOnMemory(): HRESULT CreateLockBytesOnMemory(void* pv , size_t si , unsigned flags , void* arena , ILockBytes** pplb);
Эти две функции позволяют поддержать широкий спектр хранилищ, органи зованных в памяти: фиксированный буфер, «глобальную» память Windows, COMраспределитель памяти (IAllocator) и т.д. Один из многочисленных фла гов называется SYCLBOMF_FIXED_ARRAY и говорит о том, что pv указывает на мас сив структур типа MemLocBytesBlock: struct MemLockBytesBlock { size_t cb; void* pv; };
Не собираюсь и дальше распространяться на эту тему, поскольку теперь очень негативно отношусь к нетипизированным указателям, семантика которых опре деляется флагами. А рассказал я все это потому, что с помощью описанных средств я мог взять набор блоков памяти, содержащих разнесенные данные паке та, и получить указатель IStream*, позволяющий извлекать эти данные так, будто
Ввод/вывод с разнесением и сбором
465
они находятся в одном непрерывном блоке. Ниже показан такой код, и выглядит он простым и достаточно прозрачным. (Обработка ошибок для краткости опуще на.) Объекты ref_ptr служат для того, чтобы ссылки подсчитывались корректно даже при раннем возврате или исключении. std::vector<WSABUF> size_t ILockBytes* IStream*
blocks = . . . payloadSize = . . . plb; pstm;
SynesisCom::CreateLockBytesOnMemory(&blocks[1], payloadSize , SYCLBOMF_FIXED_ARRAY | . . ., NULL, &plb); stlsoft::ref_ptr lb(pbl, false); // false "ñúåäàåò" ññûëêó SynesisCom::CreateStreamOnLockBytes(plb, 0, &pstm); stlsoft::ref_ptr stm(pstm, false); // false "ñúåäàåò" ññûëêó . . . // Ïåðåäàòü stm âåðõíèì óðîâíÿì äëÿ îáðàáîòêè
Для учета порядка байтов поток можно далее обернуть в класс адаптера эк земпляра, работающий совместно с фабрикой объектовсообщений, и механизм эффективной трансляции TCPсегментов в написанные на C++ объекты протоко ла верхнего уровня будет готов. Высокая эффективность такой схемы обусловле на тем, что нет ни операций выделения памяти, ни копирования в отдельные поля готового объектасообщения. Это хорошая основа для модели сетевого сервера, и я пользовался ей несколь ко раз, хотя и в разных обличьях. Но у описанного подхода есть несколько особен ностей, изза которых может возникнуть желание поискать другое, менее завися щее от конкретной технологии решение. Вопервых, основной его недостаток в том, что, будучи основан на COM, сервер по сути привязан к платформе Windows. Вовторых, многие разработчики считают (неправильно), что технология COM, как и C++ и STL (снова неправильно) принципиально неэффективна, и разубе дить их в этом трудно, даже предъявляя неоспоримые факты. Добавьте еще нети пизированные указатели и тот факт, что реализация интерфейсов IStream и ILockBytes находится в закрытых библиотеках, – и захочется чегото получше.
31.3.2. Класс platformstl::scatter_slice_sequence – рекламный трейлер Альтернативный подход можно найти в новом и все еще развивающемся ком поненте scatter_slice_sequence, который входит в подпроект PlatformSTL. Это шаблонный классфасад, в котором хранится массив структур, описывающих буферы ввода/вывода. Он предоставляет методы, позволяющие вызвать плат форменные функции ввода/вывода для этих буферов, а также получить доступ к STLнабору (методы begin() и end()). Этот класс работает со структурами iovec и WSABUF, абстрагируя их свойства с помощью атрибутных прокладок (раз дел 9.2.1) get_scatter_slice_size, get_scatter_slice_ptr и get_scatter_slice_size_ member_ptr, которые показаны в листинге 31.3.
466
Наборы
Листинг 31.3. Атрибутные прокладки для структур iovec и WSABUF #if defined(PLATFORMSTL_OS_IS_UNIX) inline void* const get_scatter_slice_ptr(struct iovec const& ss) { return ss.iov_base; } inline void*& get_scatter_slice_ptr(struct iovec& ss); inline size_t get_scatter_slice_size(struct iovec const& ss) { return static_cast<size_t>(ss.iov_len); } inline size_t& get_scatter_slice_size(struct iovec& ss); inline size_t iovec::* get_scatter_slice_size_member_ptr(struct iovec const*) { return &iovec::iov_len; } #elif defined(PLATFORMSTL_OS_IS_WIN32) inline void const* get_scatter_slice_ptr(WSABUF const& ss) { return ss.buf; } inline void*& get_scatter_slice_ptr(WSABUF& ss); inline size_t get_scatter_slice_size(WSABUF const& ss) { return static_cast<size_t>(ss.len); } inline size_t& get_scatter_slice_size(WSABUF& ss); inline u_long WSABUF::* get_scatter_slice_size_member_ptr(WSABUF const*) { return &WSABUF::len; } #endif /* îïåðàöèîííàÿ ñèñòåìà */
Класс scatter_slice_sequence в настоящий момент поддерживает меха низм readv()/writev() в UNIX и WSARecv()/WSASend() и WSARecvFrom()/ WSASendTo() в Windows. В листинге 31.4 приведен пример использования специ ализации шаблонного класса типом iovec. В нем производится чтение содержи мого из одного файлового дескриптора в несколько буферов, обработка содер жимого в духе STL и запись результата в другой файловый дескриптор. Листинг 31.4. Пример использования класса scatter_slice_sequence с функциями readv() и writev() int fs = . . . // Îòêðûò äëÿ ÷òåíèÿ int fd = . . . // Îòêðûò äëÿ çàïèñè for(;;) {
Ввод/вывод с разнесением и сбором const size_t const size_t char const size_t
467
BUFF_SIZE = 100; MAX_BUFFS = 10; buffers[MAX_BUFFS][BUFF_SIZE]; numBuffers = rand() % MAX_BUFFS;
// Îáúÿâèòü ýêçåìïëÿð, ðàññ÷èòàííûé íà numBuffers áóôåðîâ platformstl::scatter_slice_sequence sss(numBuffers); // Ïîäãîòîâèòü êàæäûé áóôåð, íà ïðàêòèêå èõ ðàçìåðû ìîãóò // ðàçëè÷àòüñÿ { for(size_t i = 0; i < numBuffers; ++i) { sss.set_slice(i, &buffers[i][0], sizeof(buffers[i])); }} if(0 != numBuffers) //  ïîëåâûõ óñëîâèÿõ ìîæíî ïîëó÷èòü è 0 áóôåðîâ { size_t n = sss.read(::readv, fs); // Read from fs using ::readv() if(0 == n) { break; } // "Îáðàáîòàòü" ñîäåðæèìîå std::transform( sss.payload().begin(), sss.payload().begin() + n , sss.payload().begin(), ::toupper); sss.write(::writev, fd, n); // Âûâîä n áóôåðîâ â fd ñ ïîìîùüþ ::writev() } }
Очевидно, этот пример сильно урезан, но я верю в вашу способность домыс лить, что fs и fd могли бы быть сокетами, буферы взяты из арены в разделяемой памяти (в данный момент полностью исчерпанной), а «обработка» – нечто более сложное, чем преобразование в верхний регистр перед повторной передачей. Полезная нагрузка последовательности (которую возвращает метод payload()) предоставляет итераторы с произвольным доступом к содержимому блоков памяти. Как и в случае std::deque, важно понимать, что эти итераторы не являются непрерывными (раздел 2.3.6)! Арифметические операции над итерато рами выполняются за постоянное время, но обход диапазона – нелинейная опера ция. Класс scatter_slice_sequence все еще находится в процессе разработки, и его интерфейс может измениться до официального включения в подпроект PlatformSTL (на компактдиске он есть). Но что он безусловно дает, так это воз можность представить заданный набор блоков данных в виде STLпоследователь ности (раздел 2.2) вкупе с адаптерными методами read() и write(), которые принимают описатель файла или сокета, а также функцию ввода/вывода с разне сением и сбором и применяют их к блокам. Это логический эквивалент объекта COMпотока, создаваемого функциями CreateLockBytesOnMemory() (с флагом SYCLBOMF_FIXED_ARRAY) и CreateStreamOn LockBytes(). Один из недостатков состоит в том, что содержимое можно обходить только по одному элементу за раз, что может негативно сказаться на производительности. (Подсказка: это ключ к коечему интересному, что ждет нас в дальнейшем…)
468
Наборы
31.4. Адаптация класса ACE_Message_Queue Основная тема данной главы – мои попытки адаптировать очереди в памяти из библиотеки Adaptive Communications Environment (ACE) к идее STLнабора. Это отвечало бы требованиям к одному из моих недавних коммерческих сетевых проектов – службе маршрутизации промежуточного уровня. Чтобы воспользо ваться каркасом ACE Reactor, вы должны создать классы обработчиков, произ водные от ACE_Event_Handler (переопределив необходимые методы обработки событий ввода/вывода) и зарегистрировать их экземпляры в объектесинглете Reactor. Когда реактор обнаруживает событие ввода/вывода, для которого заре гистрирован обработчик, он вызывает нужный метод этого обработчика. При ра боте с потоковым протоколом TCP обычно применяется такая идиома: поместить полученные данные в объекты класса ACE_Message_Block и поставить их в оче редь, представленную экземпляром специализации шаблонного класса ACE_Message_Queue, как показано в листинге 31.5 (обработка ошибка для кратко сти опущена). Листинг 31.5. Простой обработчик события для каркаса ACE Reactor class SimpleTCPReceiver : public ACE_Event_Handler { . . . virtual int handle_input(ACE_HANDLE h) { const size_t BLOCK_SIZE = 1024; ACE_Message_Block* mb = new ACE_Message_Block(BLOCK_SIZE); ssize_t n = m_peer.recv(mb->base(), mb->size()); mb->wr_ptr(n); m_mq.enqueue_tail(mb); return 0; } . . . private: // Ïåðåìåííûå-÷ëåíû ACE_SOCK_Stream m_peer; // Ñîêåò, ïðåäñòàâëÿþùèé // ñîåäèíåíèå ACE_Message_Queue m_mq; // Î÷åðåäü ñîîáùåíèé };
Класс ACE_Message_Queue играет роль упорядоченного хранилища для всех блоков и тем самым точно отражает идею потока данных. Но ACE_Message_Queue – это всего лишь контейнер для блоков, он не пытается абстрагировать доступ к их содержимому. Чтобы получить содержимое очереди сообщений, потребуется ас социированный класс ACE_Message_Queue_Iterator, который умеет обходить блоки (листинг 31.6). Если существует следующий блок, то метод ACE_Message_ Queue_Iterator::next() устанавливает на него переданную ссылку на указа тель и возвращает ненулевой код. В противном случае возвращается 0. Метод advance() перемещает текущую точку обхода на следующий блок (если он есть).
Ввод/вывод с разнесением и сбором
469
Листинг 31.6. Пример использования класса ACE_Message_Queue_Iterator void SimpleTCPReceiver::ProcessQueue() { ACE_Message_Queue_Iterator mqi(m_mq); ACE_Message_Block* mb; for(; mqi.next(mb); mqi.advance()) { { for(size_t i = 0; i < mb->length(); ++i) { printf("%c", i[mb->rd_ptr()]; }} mb->rd_ptr(mb->length()); // Ñäâèíóòü óêàçàòåëü ÷òåíèÿ çà îáðàáîòàííûé // áëîê } }
Очевидно, если нужно обрабатывать набор блоков, как один логически непре рывный блок, то такой подход не очень удобен. Нам нужна последовательность, которая представила бы поток в плоском виде, удобном для обработки средствами STL.
31.4.1. Класс acestl::message_queue_sequence, версия 1 В подпроекте ACESTL содержится ряд компонентов, которые адаптируют ACE к STL (и упрощают работу с компонентами ACE). Шаблонный класс ACESTL::message_queue_sequence играет роль адаптера экземпляра для ACE_Message_Queue. Так как это довольно заковыристый класс, то я, как уже де лал не раз, представлю серию приближений к окончательной реализации. Но в отличие от материала, изложенного в других главах, здесь изменения будут на копительными, так что я уложусь в 40 страниц. В листинге 31.7 приведено опреде ление первой версии. Листинг 31.7. Определение класса message_queue_sequence //  ïðîñòðàíñòâ èìåí acestl template class message_queue_sequence { public: // Òèïû-÷ëåíû typedef char typedef ACE_Message_Queue typedef message_queue_sequence typedef size_t class public: // Êîíñòðóèðîâàíèå explicit message_queue_sequence(sequence_type& public: // Èòåðàöèÿ iterator begin(); iterator end();
value_type; sequence_type; class_type; size_type; iterator; mq);
Наборы
470 public: // Àòðèáóòû size_type size() const; bool empty() const; private: // Ïåðåìåííûå-÷ëåíû sequence_type& m_mq; private: // Íå ïîäëåæèò ðåàëèçàöèè message_queue_sequence(class_type const&); class_type& operator =(class_type const&); };
Учитывая ранее рассмотренные последовательности, тут почти нечего ком ментировать; интересное начнется в классе iterator. Отметим лишь, что тип зна чения – char, то есть метод size() возвращает число байтов в очереди, а [begin(), end()) определяет диапазон байтов. Никакие методы не оперируют понятием блока сообщения.
31.4.2. Класс acestl::message_queue_sequence::iterator В листинге 31.8 приведено определение класса acestl::message_queue_ sequence:: iterator. Здесь тоже многое вам уже знакомо. (Надеюсь, что вы уже привыкли к описываемым техническим приемам, распознаете похожие черты и видите различия. Естественно, я рассчитываю, что все это окажется полезным, когда вы будете писать свои собственные расширения STL.) Итератор относится к категории итераторов ввода (раздел 1.3.1). Категория ссылок на элементы – недолговечные или выше. На самом деле, ссылки фиксированные при условии, что никакой код – в этом или другом потоке – не изменяет содержимое обертываемой очереди сообщений или хранящихся в ней блоков (в таком случае ссылки были бы чувствительными). Итератор реализован с помощью класса shared_handle, который мы обсудим чуть ниже. Я не привел канонические манипуляции с shared_handle в методах конструирования, поскольку мы уже встречались с ними в других последовательностях (разделы 19.3 и 20.5). Листинг 31.8. Определение класса message_queue_sequence::iterator class message_queue_sequence::iterator : public std::iterator<std::input_iterator_tag , char, ptrdiff_t , char*, char& > { private: // Òèïû-÷ëåíû friend class message_queue_sequence; typedef ACE_Message_Queue_Iterator struct public: typedef iterator typedef char private: // Êîíñòðóèðîâàíèå
mq_iterator_type; shared_handle; class_type; value_type;
Ввод/вывод с разнесением и сбором
471
iterator(sequence_type& mq) : m_handle(new shared_handle(mq)) {} public: iterator() : m_handle(NULL) {} iterator(class_type const& rhs); // Ðàçäåëÿåò îïèñàòåëü ñ ïîìîùüþ AddRef() (+) ~iterator() throw(); // Âûçûâàåò Release() (-) åñëè íå NULL class_type& operator =(class_type const& rhs); // (+) new; (-) old public: // Èòåðàòîð ââîäà class_type& operator ++() { ACESTL_ASSERT(NULL != m_handle); if(!m_handle->advance()) { m_handle->Release(); m_handle = NULL; } return *this; } class_type operator ++(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ value_type& operator *() { ACESTL_ASSERT(NULL != m_handle); return m_handle->current(); } value_type operator *() const { ACESTL_ASSERT(NULL != m_handle); return m_handle->current(); } bool equal(class_type const& rhs) const { return lhs.is_end_point() == rhs.is_end_point(); } private: // Ðåàëèçàöèÿ bool is_end_point() const { return NULL == m_handle || m_handle->is_end_point(); } private: // Ïåðåìåííûå-÷ëåíû shared_handle* m_handle; };
Методы итерации реализованы в терминах методов класса shared_handle. Концевая точка характеризуется тем, что описатель равен NULL или сам сообщает о себе, что находится в концевой точке. Оператор прединкремента продвигает итератор вперед, вызывая метод shared_handle::advance() и освобождает опи сатель, если advance() возвращает false. Перегруженные операторы разымено вания реализованы с помощью перегруженных вариантов метода current() класса shared_handle. Отметим, что изменяющий (неconst) вариант возвраща ет изменяемую ссылку, а неизменяющий (const) вариант возвращает char по значению.
472
Наборы
Основные действия происходят в классе shared_handle. Его реализация по казана в листинге 31.9. Я собираюсь сменить тактику и не объяснять алгоритм до мельчайших деталей. Оставляю это вам в качестве упражнения. Но всетаки отме чу, что пустые экземпляры ACE_Message_Block пропускаются, поэтому проверка на концевую точку оказывается такой простой. Листинг 31.9. Определение класса shared_handle struct message_queue_sequence::iterator::shared_handle { public: // Òèïû-÷ëåíû typedef shared_handle class_type; public: // Ïåðåìåííûå-÷ëåíû mq_iterator_type m_mqi; ACE_Message_Block* m_entry; size_t m_entryLength; size_t m_entryIndex; private: sint32_t m_refCount; public: // Êîíñòðóèðîâàíèå explicit shared_handle(sequence_type& mq) : m_mqi(mq) , m_entry(NULL) , m_entryLength(0) , m_entryIndex(0) , m_refCount(1) { if(m_mqi.next(m_entry)) { for(;;) { if(0 != (m_entryLength = m_entry->length())) { break; } else if(NULL == (m_entry = nextEntry())) { break; } } } } private: ~shared_handle() throw() { ACESTL_MESSAGE_ASSERT("Îáùèé îïèñàòåëü óíè÷òîæåí, êîãäà íà íåãî îñòàâàëèñü ññûëêè!", 0 == m_refCount); } public: sint32_t AddRef(); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ sint32_t Release(); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ public: // Ìåòîäû èòåðàöèè bool is_end_point() const {
Ввод/вывод с разнесением и сбором
473
return m_entryIndex == m_entryLength; } char& current() { ACESTL_ASSERT(NULL != m_entry); ACESTL_ASSERT(m_entryIndex != m_entryLength); return m_entryIndex[m_entry->rd_ptr()]; } char current() const { ACESTL_ASSERT(NULL != m_entry); ACESTL_ASSERT(m_entryIndex != m_entryLength); return m_entryIndex[m_entry->rd_ptr()]; } bool advance() { ACESTL_MESSAGE_ASSERT("Íåäîïóñòèìûé èíäåêñ", m_entryIndex < m_entryLength); if(++m_entryIndex == m_entryLength) { m_entryIndex = 0; for(;;) { if(NULL == (m_entry = nextEntry())) { return false; } else if(0 != (m_entryLength = m_entry->length())) { break; } } } return true; } private: // Ðåàëèçàöèÿ ACE_Message_Block* nextEntry() { ACE_Message_Block* entry = NULL; return m_mqi.advance() ? (m_mqi.next(entry), entry) : NULL; } private: // Íå ïîäëåæèò ðåàëèçàöèè shared_handle(class_type const&); class_type& operator =(class_type const&); };
31.5. О том, как садиться на ежа Надеюсь, вы согласитесь, что возможность трактовать множество полу ченных в ходе сетевого обмена фрагментов, представленных в виде объектов ACE_Message_Block, как логически непрерывный поток существенно упрощает работу с таким потоком. В проекте программного обеспечения промежуточного уровня это позволило нам представить сообщения протокола верхнего уровня в виде объектов, применив комбинацию еще одного адаптера экземпляра и
474
Наборы
фабрики сообщений. Позвольте мне небольшое отступление по поводу самого протокола. Комитет по стандартам Австралии определил протокол обмена ин формацией об электронных платежах, она называется AS2805. Это очень гибкий протокол, но он обладает одним существенным недостатком. В заголовке фикси рованного формата нет информации о размере сообщения, а каждое сообщение состоит из переменного числа полей, причем размеры некоторых полей тоже пе ременные. Следовательно, понять, получено ли все сообщение, невозможно до тех пор, пока оно не будет полностью разобрано. А, значит, критически важно иметь механизм для простого и эффективного разбора сообщения. Такой механизм был создан путем применения еще одного адаптера экземп ляра к экземпляру acestl::message_queue_sequence с тем, чтобы его можно было трактовать как потоковый объект, подобно тому, как логически непрерыв ный экземпляр ILockBytes был превращен в потоковый объект функцией CreateStreamFromLockBytes(). Этот потоковый объект используется фабрикой сообщений, которая извлекает тип сообщения из заголовка пакета, а затем вызы вает соответствующую типу функцию десериализации, которая читает остаток сообщения и создает объект нужного типа. Если получено недостаточно данных, то фабрика возвращает управление, а содержимое очереди не изменяется до на ступления следующего события ввода/вывода. Только при получении полного сообщения соответствующие ему блоки удаляются из начала очереди и возвраща ются в пул памяти. Если разобрать сообщение не удается изза некорректного со держимого, считается, что отправитель послал запорченные данные, и соединение разрывается.
31.5.1. Кэп, эта посудина не может идти быстрее! Итак, у нас наклевывается несколько хорошеньких абстракций. Некоторые из них относятся к службам масштаба предприятия, о которых распространяться нельзя, однако, вы, наверное, уже получаете сигналы от своего скептически на строенного подсознания. Мы манипулируем блоками непрерывной памяти, но сама природа класса acestl::message_queue_sequence::iterator подразуме вает, что байты, хранящиеся в этих блоках, обрабатываются по одному. Это долж но отражаться на производительности. И отражается. Прежде чем продолжить, я хочу еще раз напомнить мудрую мысль: избегайте преждевременной оптимизации. Точнее, в системе в каждый момент времени обычно есть только одно узкое место (хотя это несколько упрощенная картина). Неэффективность обычно ощущается после того, как устранена другая, более се рьезная неэффективность. Ни в одном приложении, в котором я использовал класс message_queue_sequence, он не являлся причиной низкой производитель ности. Однако я помешан на производительности (что? вы тоже заметили?), а, по скольку STLSoft – открытая библиотека, то не исключено, что этот компонент окажется узким местом еще в чьемто проекте, а такого быть не должно. Поэтому я хочу показать, как сесть на ежа и не уколоться, а точнее, как превратить содер жимое блоков данных в линейную STLпоследовательность, сохранив при этом эффективность, характерную для блочной обработки.
Ввод/вывод с разнесением и сбором
475
31.5.2. Класс acestl::message_queue_sequence, версия 2 Вопервых, нужно понять, когда копирование блока допустимо. В библиотеке ACE неструктурированная память описывается указателями типа char const* или char*, возможно, для того чтобы упростить арифметические операции над указателями. Мне такая стратегия не нравится, но это не существено: что есть, то есть. При копировании диапазона, описываемого итераторами типа acestl:: message_queue_ sequence::iterator, в диапазон, обозначенный STLитерато ром типа char* или char const* мы хотим, чтобы содержимое последовательно сти копировалось поблочно. Иначе говоря, следующий код должен сводиться к двум вызовам memcpy(), а не к 120 вызовам shared_handle::advance(): ACE_Message_Queue mq; // 2 áëîêà ñîîáùåíèé, 120 áàéòîâ char results[120]; acestl::message_queue_sequence mqs(mq); std::copy(mqs.begin(), mqs.end(), &results[120]);
Такая же эффективность желательна при копировании из непрерывной памя ти в последовательность, адаптирующую очередь сообщений: std::copy(&results[0], &results[0] + STLSOFT_NUM_ELEMENTS(results) , mqs.begin());
Первое, что для этого нужно сделать, – это определить блочные операции ко пирования в классе message_queue_sequence. В листинге 31.10 приведены опре деления двух перегруженных статических методов fast_copy(): Листинг 31.10. Определение вспомогательных методов для поддержки работы алгоритмов в классе message_queue_sequence template class message_queue_sequence { . . . static char* fast_copy(iterator from, iterator to, char* o) { #if defined(ACESTL_MQS_NO_FAST_COPY_TO) for(; from != to; ++from, ++o) { *o = *from; } #else /* ? ACESTL_MQS_NO_FAST_COPY_TO */ from.fast_copy(to, o); #endif /* ACESTL_MQS_NO_FAST_COPY_TO */ return o; } static iterator fast_copy(char const* from, char const* to , iterator o) { #if defined(ACESTL_MQS_NO_FAST_COPY_FROM) for(;from != to; ++from, ++o) {
476
Наборы
*o = *from; } #else /* ? ACESTL_MQS_NO_FAST_COPY_FROM */ o.fast_copy(from, to); #endif /* ACESTL_MQS_NO_FAST_COPY_FROM */ return o; } . . .
Я сознательно оставил директивы #define, подавляющие блочные операции, чтобы показать альтернативное поведение, подразумеваемое по умолчанию. Ме няя значения символов препроцессора, мы можем прогонять тесты, разрешая или запрещая блочные операции. (Догадываетесь, что впереди нас ждет тест на произ водительность?) В блочном режиме используются новые методы iterator:: fast_copy(), приведенные в листинге 31.11. Листинг 31.11. Определение вспомогательных методов для поддержки работы алгоритмов в классе iterator class message_queue_sequence::iterator { . . . void fast_copy(char const* from, char const* to) { if(from != to) { ACESTL_ASSERT(NULL != m_handle); m_handle->fast_copy(from, to, static_cast<size_type>(to - from)); } } void fast_copy(class_type const& to, char* o) { if(*this != to) { ACESTL_ASSERT(NULL != m_handle); m_handle->fast_copy(to.m_handle, o); } }
Мы гоняемся за тенью – эти методы тоже почти ничего не делают, кроме вызо ва одноименных методов из класса shared_handle (листинг 31.12). А вот после дние при копировании в любом направлении вычисляют, какую часть блока нуж но прочитать или записать, и обращаются к memcpy(). Листинг 31.12. Определение вспомогательных методов для поддержки работы алгоритмов в классе shared_handle struct message_queue_sequence::iterator::shared_handle { . . . void fast_copy(char const* from, char const* to, size_type n) { ACESTL_ASSERT(0 != n); ACESTL_ASSERT(from != to);
Ввод/вывод с разнесением и сбором
477
if(0 != n) { size_type n1 = m_entryLength - m_entryIndex; if(n rd_ptr()], from, n); } else { ::memcpy(&m_entryIndex[m_entry->rd_ptr()], from, n1); from += n1; m_entry = nextEntry(); ACESTL_ASSERT(NULL != m_entry); fast_copy(from, to, n - n1); } } } void fast_copy(class_type const* to, char* o) { size_type n1 = m_entryLength - m_entryIndex; if( NULL != to && m_entry == to ->m_entry) { ::memcpy(o, &m_entryIndex[m_entry->rd_ptr()], n1); } else { ::memcpy(o, &m_entryIndex[m_entry->rd_ptr()], n1); o += n1; m_entry = nextEntry(); if(NULL != m_entry) { fast_copy(to, o); } } } . . .
31.5.3. Специализация стандартной библиотеки Все вроде бы и неплохо, но кому понравится писать такой клиентский код: ACE_Message_Queue mq; // 2 áëîêà; âñåãî 120 áàéòîâ char results[120]; acestl::message_queue_sequence mqs(mq); acestl::message_queue_sequence::fast_copy(mqs.begin() , mqs.end(), &results[120]);
Мы хотим, чтобы алгоритм std::copy() автоматически выбирал быструю версию, когда третий итератор имеет тип char (const)*. Для этого необходимо его специализировать. По целому ряду причин определять частичные специализации шаблонов, на ходящихся в пространстве имен std, запрещено. Это неудобно в двух отношени ях. Первое и самое важное: поскольку message_queue_sequence – шаблон, мы хотим обслужить все его специализации, то есть иметь возможность сделать так,
478
Наборы
как показано в листинге 31.13. (Для краткости в этом и следующем листингах я опускаю указание пространства имен acestl во всех упоминаниях message_ queue_sequence<S>::iterator.) Листинг 31.13. Недопустимые специализации std::copy() // Â ïðîñòðàíñòâå èìåí std template char* copy( typename message_queue_sequence<S>::iterator from , typename message_queue_sequence<S>::iterator to, char* o) { return message_queue_sequence<S>::fast_copy(from, to, o); } template typename message_queue_sequence<S>::iterator copy(char* from, char* to , typename message_queue_sequence<S>::iterator o) { return message_queue_sequence<S>::fast_copy(from, to, o); }
Раз такое невозможно, то не остается ничего другого, как предвидеть все воз можные специализации message_ queue_sequence и для каждой из них полнос тью специализировать std::copy(), как показано в листинге 31.14. Обратите внимание, что для типов char* и char const* требуются отдельные специализа ции, если мы хотим, чтобы наша оптимизация применялась, когда указатель на область памяти имеет как тип char*, так и тип char const*. Листинг 31.14. Допустимые специализации std::copy() // Â ïðîñòðàíñòâå èìåí std template char* copy( typename message_queue_sequence::iterator from , typename message_queue_sequence::iterator to , char* o) { return message_queue_sequence::fast_copy(from, to, o); } . . . // Òî æå, ÷òî è âûøå, íî äëÿ ACE_MT_SYNCH template typename message_queue_sequence::iterator copy(char* from , char* to , typename message_queue_sequence::iterator o) { return message_queue_sequence::fast_copy(from, to, o); } . . . // Òî æå, ÷òî è âûøå, íî äëÿ ACE_MT_SYNCH template typename message_queue_sequence::iterator copy(char const* from , char const* to , typename message_queue_sequence::iterator o)
Ввод/вывод с разнесением и сбором
479
{ return message_queue_sequence::fast_copy(from, to, o); } . . . // Òî æå, ÷òî è âûøå, íî äëÿ ACE_MT_SYNCH
На наше счастье, в библиотеке ACE есть всего две специализации: ACE_NULL_ SYNCH (#define для ACE_Null_Mutex, ACE_Null_Condition) и ACE_MT_SYNCH (#define для ACE_Thread_Mutex, ACE_Condition_Thread_Mutex), так что всего получается лишь шесть специализаций. Но это еще не все. Если вы, как и я, стараетесь не употреблять char в качестве отсутствующего в C++ типа byte, то, наверное, пишете вместо этого signed char или unsigned char. А с точки зрения разрешения перегрузки (в том числе шабло нов) эти типы отличаются от char. Если передать их при вызове std::copy(), то оптимизированные для блочной передачи методы вызваны не будут. Поэтому, смиренно склонив главу, мы напишем еще по шесть специализаций для signed char и unsigned char, так что в итоге их станет восемнадцать вместо двух, макси мум трех, которые пришлось бы написать, если бы в пространстве имен std была разрешена частичная специализация. К счастью, эти усилия окупаются. Но прежде чем в этом убедиться, хочу отве тить на вопрос, который, наверное, вертится у вас на языке: «Почему только std::copy()?» В принципе, ничто не мешает специализировать все возможные стандартные алгоритмы. Не сделал я этого по двум причинам. Вопервых, это, мягко говоря, обременительно; чтобы не набивать все вручную, пришлось бы при бегнуть к макросам, а кто любит макросы? Вторая причина более прагматична. Всю эту оптимизацию мы затеяли для того, ускорить интерпретацию данных, хра нящихся в исходном блоке памяти и быстро скопировать их в новое хранилище. Мой опыт показывает, что для этого чаще всего используется std::copy(). Прав да, в нашем проекте программного обеспечения промежуточного уровня один раз потребовался алгоритм copy_n(). По чьемуто недосмотру copy_n() не включи ли в стандарт C++98 (но включат в следующую версию), поэтому он оказался в библиотеке STLSoft. Для него определены специализации, на этот раз в про странстве имен stlsoft, таким же способом, как для std::copy(). Поэтому всего в заголовочном файле содержится 36 специализаций шаблонов.
31.5.4. Производительность Теперь, познакомившись с механизмом оптимизации, самое время убедиться, что наши титанические усилия не пропали даром. Для демонстрации различий в производительности исходной и оптимизированной версии я написал тестовую программу, которая создает экземпляр ACE_Message_Queue, добавляет в очередь сколькото блоков, копирует содержимое из массива char с помощью std:: copy(), затем обратно в массив char (тоже посредством std::copy()) и наконец проверяет, что оба массива идентичны. Число блоков варьировалось от 1 до 10, а размер блока – от 10 до 10000. Время копирования из массива char в последова тельность и обратно замерялось порознь с помощью компонента performance_ counter из библиотеки PlatformSTL. Каждая операция копирования повторя
Наборы
480
лась 20000 раз, чтобы добиться миллисекундной точности. Текст программы име ется в дополнительных материалах к этой главе на компактдиске. В таблице 31.1 приведена репрезентативная выборка из полученных результа тов в виде процентной доли от времени (в миллисекундах) работы неоптимизиро ванной версии. Как и следовало ожидать, для очень маленьких блоков длины 10 разница пренебрежимо мала. Если размер буфера равен 100, то преимущества опти мизированной версии начинают проявляться, но не поражают воображение. Одна ко при переходе к более реалистичным размерам – 1000 и 10000 – исходная версия оказывается неконкурентоспособной, оптимизированная в 4050 раз быстрее. Таблица 31.1. Сравнение производительности блочных операций копирования Массив в итератор Число блоков 1 2 5 10 1 2 5 10 1 2 5 10 1 2 5 10
Итератор в массив
Размер НеоптимиZ блока зированное
БлочZ ное
%
10 10 10 10 100 100 100 100 1000 1000 1000 1000 10000 10000 10000 10000
6 8 10 14 7 9 14 23 10 16 32 60 29 101 109 102
85.7% 88.9% 62.5% 51.9% 28.0% 19.6% 13.0% 10.9% 4.8% 3.8% 3.1% 2.9% 1.4% 2,5% 2,6% 2,5%
7 9 16 27 25 46 108 211 207 416 1025 2042 2038 4100 4143 4103
НеоптимиZ зированное
БлочZ ное
7 9 16 26 23 42 99 188 184 391 898 1793 1786 3570 3606 3573
9 7 10 14 7 9 14 23 11 17 32 61 29 101 101 102
% 128.6% 77.8% 62.5% 53.8% 30.4% 21.4% 14.1% 12.2% 6.0% 4.3% 3.6% 3.4% 1.6% 2.8% 2.8% 2.9%
31.6. Резюме В этой главе мы говорили об особенностях ввода/вывода с разнесением и сбо ром. Для адаптации соответствующего API к STL пришлось преодолеть серьез ные трудности. Мы рассмотрели адаптацию в виде компонента scatter_slice_ sequence и убедились, что у таких последовательностей должны быть итераторы с настоящим произвольным доступом (то есть не непрерывные), для которых со отношение &*it + 2 == &* (it + 2) не выполняется (см. раздел 2.3.6). Тем не менее, оказалось, что даже частичной непрерывностью можно воспользоваться для рез кого повышения производительности, что особенно важно, поскольку весь меха низм предназначен для ввода/вывода в файл или в сокет. Пойдя на минимальные отступления от принципа прозрачности, мы сильно выиграли с точки зрения принципа композиции (и попутно отдали дань принципу разнообразия).
Глава 32. Изменение типа возвращаемого значения в зависимости от аргументов Покажите мне мужчину, который никогда не ревновал. – Лу Рид – Нет ничего обольстительного ни в парне, который носит смешную шляпу, ни в про% граммисте, который знает Фортран. – Джордж Фразье
32.1. Введение В: Как вдвое расширить набор типов значений, возвращаемых функцией? О: Применить к ней ARV! (ArgumentDependent ReturnType Variance) В этой главе мы рассмотрим, как другие языки – в данном случае Ruby – мо гут повлиять на идиомы C++. Конкретно, речь пойдет о наборе, который может выступать как в роли массива, так и в роли ассоциативного массива (отображе ния, хэша, словаря – называйте, как хотите). Для этого необходимы функции с одним и тем же именем, которые возвращают значения разных типов в зависимо сти от своих аргументов. Наверное, у вас сейчас мелькнула мысль: «Эка невидаль! Это же всего лишь перегрузка». Конечно. Но история на этом не заканчивается.
32.2. Одолжим рубин у Ruby Рассмотрим следующий код на языке Ruby, в котором используется привязка OpenRJ/Ruby: # Îòêðûòü áàçó äàííûõ, õðàíÿùóþñÿ â óêàçàííîì ôàéëå db = OpenRJ::FileDatabase('pets.orj', OpenRJ::ELIDE_BLANK_RECORDS) # Ïîëó÷èòü ïåðâóþ çàïèñü rec = db[0] # Ðàñïå÷àòàòü ïîëÿ èç ýòîé çàïèñè (0 ... rec.numFields).each \ { |i| fld = rec[i] puts "Field#{i}: name=#{fld.name}; value=#{fld.value}" }
482
Наборы
Типичный для Ruby код и типичное использование привязки OpenRJ/Ruby. (То же самое проще было бы сделать с помощью конструкции each_with_index, но выбранный способ лучше отвечает моим педагогическим устремлениям.) Для демонстрационной базы данных Pets, которая поставляется в составе дистрибу тива OpenRJ, будет напечатано следующее: Field0: name=Name; value=Barney Field1: name=Species; value=Dog Field2: name=Breed; value=Bichon Frise
Но можно обращаться к полям не по индексу, а по имени: # Ðàñïå÷àòàòü ïîëÿ èç ýòîé çàïèñè puts "Name=" + name=rec["Name"] puts "Species=" + name=rec["Species"] puts "Breed=" + name=rec["Breed"] if rec.include?("Breed")
Такой вариант более полезен, когда у вас имеются априорные представления о структуре данных, так что в случае отсутствия поля Name или Species должно возбуждаться исключение. Вот что напечатает эта программа: Name=Barney Species=Dog Breed=Bichon Frise
Теперь внимательно взгляните на два способа употребления оператора индек сирования. В первом случае его аргументом является целое число, а во втором – строка. Обратите внимание также на то, какой тип возвращает каждый из двух вызовов. Если аргумент целый, то возвращается экземпляр Field, то есть запись ведет себя как массив. Если же аргумент – строка, то возвращается строка (поле value в поименованной записи Field). Теперь запись стала вести себя как ассо циативный массив. В подходящих обстоятельствах такая двойственность бывает очень полезна. А записи OpenRJ – как раз такой случай, потому что содержимое базы данных не изменяется, поля представлены парами строк Name+Value, и в структуре, описывающей каждую запись, хранится массив указателей на поля. Эта функциональность реализована в привязке OpenRJ/Ruby (написанной на C) с помощью функции Record_subscript() и двух вспомогательных функций Record_subscript_ string() и Record_subscript_fixnum() (листинг 32.1). Если аргумент index – строка (типа T_STRING), то вызывается функция Record_ subscript_string(), которая возвращает строку, представляющую значение по именованного поля. Если же index – целое число (типа T_FIXNUM), то возвращается поле, находящееся в указанной позиции. Если индекс не задан, находится вне допу стимого диапазона или имеет недопустимый тип, то возбуждается исключение. Листинг 32.1. Реализация привязки OpenZRJ/Ruby на C static VALUE Record_subscript(VALUE self, VALUE index) { switch(rb_type(index)) { case T_STRING:
Изменение типа возвращаемого значения
483
return Record_subscript_string(self, StringValuePtr(index)); case T_FIXNUM: return Record_subscript_fixnum(self, FIX2INT(index)); default: rb_raise(rb_eTypeError, "èäåíòèôèêàòîð ïîëÿ äîëæåí áûòü öåëûì ÷èñëîì èëè ñòðîêîé"); } } static VALUE Record_subscript_string( VALUE self, char const* index) { ORJRecord const* record = Record_get_record_(self); ORJFieldA const* field = ORJ_Record_FindFieldByNameA(record, index , NULL); if(NULL == field) { rb_raise(cFieldNameError, "ïîëå íå íàéäåíî: %s", index); } return rb_str_from_ORJStringA(&field->value); } static VALUE Record_subscript_fixnum(VALUE self, int index) { ORJRecord const* record = Record_get_record_(self); size_t cFields = ORJ_Record_GetNumFieldsA(record); if( 0 fields[index]); } else { rb_raise(rb_eIndexError, "èíäåêñ ïîëÿ âíå äèàïàçîíà", index); } }
32.3. Двойственная семантика индексирования на C++ Я захотел эмулировать двойственную семантику индексирования в привязке OpenRJ/C++. Наивный подход мог бы выглядеть так: // Â ïðîñòðàíñòâå èìåí openrj::stl class Record { public: // Äîñòóï ê ýëåìåíòàì const Field operator [](size_t index) const; const String operator [](char const* name) const; . . .
Этот код работает, но только до поры до времени: Record r; r["Species"]; // Âîçâðàùàåò çíà÷åíèå (String) ïîëÿ ñ èìåíåì "Species" r[1]; // Âîçâðàùàåò âòîðîé ýêçåìïëÿð ïîëÿ (Field)
484
Наборы
Увы, у этого подхода есть ряд недостатков. Сначала посмотрим, что произой дет в таком случае: r[0]; // Îøèáêà êîìïèëÿöèè!
Проблема в том, что литерал 0 можно с равным успехом преобразовать и в интегральный тип, отличный от int, и в тип указателя. Можно решить эту про блему, заменив тип size_t на int, но тогда индекс сможет принимать отрица тельные значения, что для записей в базе OpenRJ бессмысленно. class Record { public: // Äîñòóï ê ýëåìåíòàì const Field operator [](int index) const; const String operator [](char const* name) const; . . .
Но есть и гораздо более серьезная проблема. Единственный строковый тип, с которым совместим второй перегруженный вариант – это Cстрока (char const*). Вы уже довольно знаете о моем пристрастии к обобщенному программи рованию, когда типы различаются тем, что они делают, а не тем, как определены, поэтому не удивитесь тому, что эта форма меня ни в коей мере не удовлетворяет. Максимально гибкий класс должен уметь работать со всеми строковыми типами, а не только с char const* или std::string const&.
32.4. Достижение обобщенной совместимости с помощью прокладок строкового доступа Мы можем перегрузить оператор индексирования по имени в классе Record, так чтобы он работал с любым типом, для которого определена прокладка строко вого доступа c_str_ptr (раздел 9.3.1): class Record { public: // Äîñòóï ê ýëåìåíòàì const Field operator [](size_t index) const; const String operator [](char const* name) const; template const String operator [](S const& name) const { return operator [](stlsoft::c_str_ptr(name)); } . . .
Теперь можно именовать поля с помощью значений разных типов: std::string s1("Name"); ACE_CString s2("Species"); stlsoft::simple_string s3("Breed"); r[s1];
Изменение типа возвращаемого значения
485
r[s2]; r[s3];
32.5. Как распознать целочисленность? Однако до конца проблема еще не решена. Еще раз взглянув на определение типа Record, мы увидим три перегруженных оператора индексирования. Если ар гумент имеет тип char const* или size_t (либо int, если мы остановимся на та кой форме), то выбирается нужный нешаблонный вариант. Для аргументов лю% бого другого типа перегрузка разрешится в пользу шаблонной функции, и будет произведена попытка применить к аргументу прокладку c_str_ptr. Если у аргу мента окажется какойто другой интегральный тип, возникнут неприятности: long n = 1; r[n]; // Îøèáêà: äëÿ òèïà long íåò ïðîêëàäêè c_str_ptr()
Оно и понятно, интерпретация long как чегото такого, что может быть пре образовано в имя поля, противоречит самой идее операторов индексирования в классе Record. Исправить эту ошибку можно, например, определив нешаблон ные перегрузки для всех интегральных типов: class Record { public: // Äîñòóï ê ýëåìåíòàì const Field operator [](unsigned char index) const { return operator [](static_cast(index)); } const Field operator [](signed char index) const; const Field operator [](unsigned short index) const; const Field operator [](signed short index) const; const Field operator [](unsigned int index) const; const Field operator [](signed int index) const; const Field operator [](unsigned long index) const; // Ñèíîíèì size_t const Field operator [](signed long index) const; #if Visual C++ 6 èëè êîìïèëÿòîð Intel â ðåæèìå ñîâìåñòèìîñòè ñ VC6 const Field operator [](unsigned __int32 index) const; const Field operator [](signed __int32 index) const; #endif #if ïîääåðæèâàþòñÿ 64-ðàçðÿäíûå öåëûå const Field operator [](uint64_t const& index) const; const Field operator [](sint64_t const& index) const; #endif const String operator [](char const* name) const; . . .
Не очень красиво, правда? Много повторяющегося кода, да еще и уродливые директивы препроцессора в нагрузку. Должен быть способ лучше, и он есть. Нам нужно, чтобы для аргументов интегральных типов компилятор выбирал оператор индексирования по целочисленному индексу, а для всех остальных – оператор поиска по строке.
Наборы
486
Мы не можем просто добавить шаблонную функциючлен для обработки ин тегральных типов: public: // Äîñòóï ê ýëåìåíòàì . . . template const Field operator [](I const& index) const { return operator [](static_cast(index)); }
У нас уже есть шаблонный оператор индексирования, предназначенный для строк, и компилятор, понятное дело, расстроится. Необходимо совместить два поведения в одном. Это было бы просто, если бы оба метода возвращали значения одного и того же типа, но, коль скоро это не так, придется призвать на помощь метапрограммирование шаблонов.
32.6. Выбор типа возвращаемого значения и перегрузки Нам требуется выбрать подходящую перегрузку и тип возвращаемого значе ния, а этого можно достичь, применив два приема из области метапрограммирова ния: распознавание типа (раздел 13.4.2) и выбор типа (раздел 13.4.1). Распознава нием типа – выяснением того, является ли нечто целым, – занимается шаблон is_integral_type (раздел 12.1.4), а выбором типа – шаблон select_first_ type_if (раздел 13.4.1). В итоге выбор возвращаемого типа выглядит так: public: // Äîñòóï ê ýëåìåíòàì . . . template typename select_first_type_if::type
Перегруженные
варианты
вспомогательного
закрытого
метода
subscript_operator_() определены следующим образом: template String subscript_operator_(S const& name, no_type) const { return operator [](stlsoft::c_str_ptr(name)); } template Field subscript_operator_(I const& index, yes_type) const { return operator [](static_cast<size_t>(index)); }
Правильный перегруженный вариант выбирается внутри реализации шаб лонного оператора индексирования с помощью временного экземпляра типачле на type шаблонного класса is_integral_type: template typename select_first_type_if::type operator [](S const& name) const { typedef typename is_integral_type::type yesno_type; return subscript_operator_(name, yesno_type()); }
Отметим неизбежное нарушение принципа DRY SPOT (глава 5), выражающе еся в дублировании специализации is_integral_type в сигнатуре метода (для выведения типа возвращаемого значения) и в его теле (для выбора перегруженно го варианта вспомогательной функции).
32.6.1. Запрет индексов в виде целого со знаком Дисциплины ради мы могли бы добавить ограничение, требующее, чтобы ин тегральный тип был беззнаковым, воспользовавшись шаблоном is_signed_type (раздел 12.1.5), как показано ниже: template Field subscript_operator_(I const& index, yes_type) const { STLSOFT_STATIC_ASSERT(0 == is_signed_type::value); return operator [](static_cast<size_t>(index)); }
Такие ограничения применяются после того, как распознан интегральный тип, поэтому опасности перейти на строковую сторону нет.
32.7. Резюме Вот и все. Эту технику я называю изменением типа возвращаемого значения в зависимости от аргументов (argumentdependent returntype variance – ARV), поскольку тип возвращаемого функцией значения выбирается не автором кода, а компилятором от имени пользователя, на основе типа аргумента. (Название предложил Бьерн Карлсон, хитроумнейший из всех лингвистов.) Техника ARV обладает следующими достоинствами: позволяет избежать неуклюжих языковых конструкций и противных нео днозначностей; позволяет выполнять перегрузку на основе концепции без написания боль шого числа перегруженных методов с идентичными телами; позволяет вывести тип значения, возвращаемого шаблонной функцией членом на основе типа аргумента. Конечно, приходится писать или переваривать метапрограммный код, зато в ре зультате получается исключительно эффективный, гибкий и функциональный библиотечный код, позволяющий избежать разного рода неприятностей в клиент ской программе, поэтому сложность окупается. Никаких накладных расходов на этапе выполнения нет. Соблюдаются принципы композиции и наименьшего удив% ления с минимальным (на мой взгляд) нарушением принципа прозрачности.
Глава 33. Порча итератора извне Правильность должна быть локальным свойством. – Нейлс Фергюсон и Брюс Шнейер Простота окупается, особенно если у вас репутация хитреца. – Айзек Азимов
33.1. Когерентность элемента и интерфейса В разделе 1.2 мы говорили, что для различных стандартных контейнеров дей ствуют разные правила относительно того, когда и как их итераторы (а также ука затели и ссылки на элементы) могут стать недействительными в результате изме няющих операций. Например, в следующем фрагменте недействительными становятся и b, и e: std::vector ints; ints.push_back(1); ints.push_back(2); std::vector::iterator b = ints.begin(); std::vector::iterator e = ints.end(); ints.erase(b); // b è e íåäåéñòâèòåëüíû
Объясняется это тем, что, согласно стандарту, все итераторы в экземпляре std::vector после точки вставки недействительны (C++03: 23.2.4.3). Напротив, в следующем фрагменте недействительным оказывается только итератор b: std::list ints; ints.push_back(1); ints.push_back(2); std::list::iterator b = ints.begin(); std::list::iterator e = ints.end(); ints.erase(b); // Íåäåéñòâèòåëåí òîëüêî b
А это потому, что операция std::list::erase() делает недействительными только итераторы, указывающие на удаленные элементы (C++03: 23.2.2.3). Эти правила можно сформулировать столь точно и недвусмысленно по одной простой причине. Контейнеры владеют своими элементами. То, как контейнеры и их эле менты связаны и взаимодействуют между собой – когерентность элемента и ин терфейса, – жестко фиксировано. Поэтому, если вы следуете правилам для STL
Порча итератора извне
489
контейнеров и не пытаетесь использовать недействительные итераторы, то ника ких неприятных сюрпризов не будет. Но с STLнаборами все не так определенно: когерентность может быть и не стопроцентной. И непререкаемых правил тут не существует, в чем мы неоднок ратно убеждались в примерах из разных глав в этой части. Последовательность glob_sequence (глава 17) эксклюзивно владеет эк земпляром glob_t и является неизменяемой. Функция glob() возвращает все сопоставленные с образцом файлы в одном мгновенном снимке. Поэто му вопрос о недействительности итераторов не возникает. Последователь ности для перебора процессов pid_sequence и process_module_sequence (глава 22) работают аналогично, то есть возвращают полный снимок всех активных в данный момент процессов. Последовательности readdir_sequence (глава 19) и findfile_sequence (глава 20) получают элементы файловой системы по одному. Однако API файловой системы не дает вызывающей программе сделать текущую точку обхода «недействительной». Вполне может статься, что действия в других потоках или процессах, а равно действия самой операционной системы приведут к модификации того элемента, информацию о котором мы как раз хотим вернуть вызывающей программе, но это не нарушит правиль ность работы функций самого API. Нам безразлично, как это достигает ся, – возможно, ОС кэширует снимок файловой системы в момент начала обхода или сериализует доступ к списку индексных узлов (inode). Мы зна ем лишь, что сможем полностью обойти последовательность, содержимое которой в данный момент времени логически непротиворечиво. Будучи пользователем API обхода файловой системы или обертывающего ее STL набора, мы осознаем, что тот вид, который нам представлен, может изме ниться, но никакой проблемы в этом нет, и наш набор не «испортится», пока мы с ним работаем. Хотя синтаксис наборов, обертывающих энумераторы (глава 28) и наборы (глава 30) COM, совершенно иной, но они обеспечивают ту же изоляцию от изменений элементов, поскольку она обеспечивается энумераторами COM. Вопрос о поведении наборов Fibonacci_sequence (глава 23) и string_ tokeniser (глава 27) носит академический характер, поскольку они сами генерируют свои элементы. У порождаемых значений нет физического воплощения, существуют они лишь постольку, поскольку интерпретиру ются наборами. Ни для одного из этих наборов итераторы не могут стать недействительными изза неполной когерентности элемента и интерфейса, поэтому они не страдают от того, что я называю порчей итератора извне. Однако мы встречали случаи, ког да граница между обертываемыми элементами и концепциями STL оказывается куда более размытой. Например, в наборах scatter_slice_sequence и message_ queue_sequence (глава 31) порядок элементов не изменяется (раздел 2.2.1), од нако когерентность можно гарантировать лишь тогда, когда поток, в котором ра ботает клиент, имеет исключительный доступ к буферам ввода/вывода.
490
Наборы
Есть два набора, которые волейневолей должны смириться со снижением ко герентности: environment_map (глава 25) и последовательность окон в Zпорядке (глава 26). Обертываемые ими элементы подвержены непредсказуемым измене ниям. В environment_map для доступа к элементам по имени используется стан дартная функция getenv(), а для итерации – короткоживущие кэши с подсчетом ссылок. При реализации последовательности для обхода окон в Zпорядке мы от казались от доступа по индексу и от предоставления двунаправленного итерато% ра, воспользовавшись вместо этого шаблонным классом обратного самому себе итератора. В случае environment_map некогерентность является следствием дей ствий в других потоках того же процесса и возможных законных побочных эф фектов стандартной (getenv()) и нестандартной (putenv(), setenv(), environ) частей API системного окружения. Мы сознательно пренебрегаем первым, но не можем игнорировать второе. И это первый вид порчи извне: порча в результате побочных эффектов API в том же потоке. В случае последовательности окон в Zпорядке некогерентность проистекает из того факта, что функция API GetWindow() опрашивает одно конкретное отно шение между двумя окнами, принадлежащими набору окон, который в любой мо мент может быть изменен в результате действий пользователя или других процес сов. Это второй вид порчи извне: порча в результате действий вне потока. Существует еще и третий случай, достойный обсуждения.Если набор высту пает в роли фасада, скрывающего существующий API, то обертываемые элемен ты могут измениться в результате вызова функций API в обход методов набора. В идеале следует воздерживаться от таких действий. Но бывает, что изза сложно сти приложения вы изменяете обертываемые элементы, сами того не желая. И это третий вид порчи извне: порча в результате побочных эффектов приложения внутри потока. (Это тоже порча извне, так как она является внешней по отноше нию к набору.) В отличие от двух других типов здесь не так просто решить, следу ет ли принимать такую возможность во внимание. Можно, конечно, сказать, что к подобным проблемам следует относиться внимательно, осознавая пагубность последствий, а пренебрежение к ним – признак плохого программирования. Я по нимаю такую точку зрения, но на практике она мало применима как раз потому, что вероятность внешнего манипулирования, не осознаваемого программистом (в момент компиляции), возрастает по мере усложнения кода приложения и уров ня абстракции, присущего адаптации (STL). Разбираться надо в каждом конкрет ном случае отдельно. На примере класса environment_map было продемонстрировано, как можно избежать порчи в результате побочных эффектов API в том же потоке. Но это не всегда возможно или желательно. В этой главе мы рассмотрим компоненты, кото рые ведут себя в таких ситуациях активно. Они должны обнаружить порчу и сооб щить о ней клиентскому коду. Сначала (раздел 33.2) мы рассмотрим некоторые расширения STL, ведущие себя как фасады, обертывающие элементы управле ния Windows и умеющие обрабатывать порчу в результате побочных эффектов API в том же потоке. Затем, в основной части главы (раздел 33.3) мы обратимся к порче в результате действий вне потока на примере неоднократно раскритико
Порча итератора извне
491
ванного API реестра Windows и рассмотрим соответствующее расширение STL в виде фасада из библиотеки WinSTL Registry. (В дополнительных материалах к этой главе на компактдиске речь идет о том, почему некоторые расширения STL для XML должны, в силу сложности обертываемых библиотек, обрабатывать порчу в результате побочных эффектов приложения внутри потока. В таких ком понентах можно для обнаружения и извещения о порче применять технику, опи санную в разделах 33.2 и 33.3.)
33.2. Элементы управления Windows ListBox и ComboBox В операционных системах Windows описатели окон – глобальные объекты. Текст окна, равно как и многие другие его атрибуты, может читать и изменять не только тот поток, в котором окно создано и который им владеет. Не станем крити ковать такой дизайн с общих позиций, поскольку я хотел бы закончить книгу, прежде чем уйду на пенсию. Вместо этого просто посмотрим, каковы последствия этого подхода для стандартного элемента управления ListBox. Для манипуляций элементом ListBox, как и любым другим элементом управ ления Windows, ему нужно послать сообщение с помощью одной из семейства функций SendMessage(). Для вставки строки в список служат сообщения LB_ADDSTRING и LB_INSERTSTRING, а для удаления – сообщения LB_DELETESTRING и LB_RESETCONTENT. Сообщение LB_GETCOUNT возвращает количество элементов в списке. В листинге 33.1 иллюстрируется работа с этим сообщениями. (Отметим, что третий и четвертый параметры SendMessage(), известные под названиями wParam и lParam, – это целые числа (32разрядные на платформе Win32), в кото рых передается различная информация – как собственно целые числа, так и при веденные указатели. Если в конкретном сообщении какойто из этих параметров не используется, клиентская программа должна передать вместо него 0.) Листинг 33.1. Манипуляция элементом управления ListBox с помощью функций Windows API HWND hwndListBox = ::CreateWindow("LISTBOX", "", LBS_SORT, . . . ); // Âñòàâèòü ñòðîêè â ïîðÿäêå ñîðòèðîâêè ::SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)»String 3"); ::SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)»String 1"); // Ñåé÷àñ ListBox ñîäåðæèò 2 ñòðîêè: "String 1" è "String 3" assert(2 == ::SendMessage(hwndListBox, LB_GETCOUNT, 0, 0)); // Âñòàâèòü ñòðîêó â çàäàííîå ìåñòî ::SendMessage(hwndListBox, LB_INSERTSTRING, 1, (LPARAM)"String 2"); // Ñåé÷àñ ListBox ñîäåðæèò 3 ñòðîêè: "String 1", "String 2", // è "String 3"
492
Наборы
assert(3 == ::SendMessage(hwndListBox, LB_GETCOUNT, 0, 0)); // Óäàëèòü ñòðîêó ñ èíäåêñîì 1 ::SendMessage(hwndListBox, LB_DELETESTRING, 1, 0); // Ñåé÷àñ ListBox ñîäåðæèò 2 ñòðîêè: "String 1" è "String 3" assert(2 == ::SendMessage(hwndListBox, LB_GETCOUNT, 0, 0)); // Óäàëèòü âñå ñòðîêè ::SendMessage(hwndListBox, LB_RESETCONTENT, 0, 0); assert(0 == ::SendMessage(hwndListBox, LB_GETCOUNT, 0, 0));
Авторам STLнаборов, обертывающих элемент управления ListBox (и ComboBox) важно понимать, что сообщения LB_UPDATESTRING или LB_REPLACESTRING не су ществует. Иначе говоря, добавить или удалить строку можно, а изменить нельзя. Следовательно, предоставить осмысленный изменяемый набор невозможно; на боры должны быть неизменяемыми.
33.2.1. Гонка при выборке? Чтобы выбрать строки из списка ListBox, надо отправить ему сообщение LB_GETTEXT, как показано ниже. (LB_GETTEXT на самом деле определено с по мощью директивы #define как LB_GETTEXTA или LB_GETTEXTW в зависимости от
того, компилируется программа для кодировки ANSI или Unicode, причем послед няя выбирается, когда определен символ препроцессора UNICODE.) char str[100]; // Ïîëó÷èòü òåêñò ñòðîêè ñ èíäåêñîì 1, ñ÷èòàÿ, ÷òî åãî äëèíà íå ïðåâûøàåò // 100 áàéòîâ ::SendMessage(hwndListBox, LB_GETTEXT, 1, (LPARAM)&str[0]);
Тутто нас и поджидает переполнение буфера. Что если строка в позиции 1 содержит больше 100 символов? Мы не указали длину буфера в сообщении, по этому его обработчик ничего о нем и не знает. А надо было сначала послать сооб щение LB_GETTEXTLEN, чтобы узнать длину строки (в символах без учета заверша ющего нуля), а потом выделить буфер достаточного размера, как показано в листинг 33.2. Обратите внимание, что код возврата сравнивается со значением LB_ERR (-1), которое возвращается, если указан недопустимый индекс (ссылаю щийся на несуществующую позицию). Листинг 33.2. Получение длины текста от элемента управления ListBox int r = ::SendMessage(hwndListBox, LB_GETTEXTLEN, 1, 0); if(LB_ERR != r) { stlsoft::auto_buffer str(1 + static_cast<size_t>(r)); // ñêîïèðîâàòü òåêñò ýëåìåíòà â ïîçèöèè 1 â áóôåð äîñòàòî÷íîãî ðàçìåðà ::SendMessage(hwndListBox, LB_GETTEXT, 1, (LPARAM)&str[0]); }
Порча итератора извне
493
Знатоки многопоточного программирования, наверное, чешут затылок, по дозревая, что здесь возможна гонка (race condition). Так как описатели окон до ступны всем потокам в системе, то может случиться так, что в позицию 1 будет вставлена строка, длина которой больше, чем у той, что занимала эту позицию в момент опроса, и что если это произойдет между двумя обращениями к SendMessage()? В 16разрядных версиях Windows такого случиться не могло, так как в них была реализована невытесняющая многозадачность. Один процесс уступал про цессор другому только в результате явного действия (обычно в момент обраще ния к очереди за следующим сообщением). Но 32разрядные версии Windows – это операционные системы с вытесняющей многозадачностью, то есть один поток может быть прерван другим в любой момент. Переход от 16разрядной к 32разрядной версии Windows должен был быть максимально простым (иначе кто бы захотел переходить?), поэтому API по воз можности должен был остаться неизменным. Надо отдать должное корпорации Microsoft – она приложила титанические усилия для обеспечения обратной со вместимости, и с точки зрения коммерческого успеха это было правильно. Но как же разрешается наша головоломка? Если вы напишете две программы для Win32, одна из которых создает и изменяет строки в ListBox, а другая их читает, то обна ружите, что гонка никогда не возникает. Доступ к окнам, принадлежащим друго му потоку, сериализуется с помощью какогото объекта синхронизации (предпо ложительно, мьютекса). Иными словами, Win32 ведет себя как Win16. Ну и как вам это нравится? Как бы ни расценивать операционную систему, которая предоставляет всем процессам доступ к объектам графического интерфейса, надо признать, что ис пользование плохо спроектированного API передачи сообщений для 16разряд ных версий на платформе Win32 безопасно, за исключением разве что самых из вращенных и нереалистичных сценариев. (Такой пример есть на компактдиске, и он таки действительно извращенный и нереалистичный.) Тем не менее, раз возникновение гонки возможно, принцип программирова ния по контракту (глава 7) требует рассматривать порчу итератора в результате действий вне потока как одну из ситуаций во время выполнения. Поэтому класс обертка должен учитывать возможность изменения содержимого ListBox. Есть только два способа обнаружить, что содержимое изменилось. Вопервых, можно послать сообщение LB_GETCOUNT, запомнить количество элементов в ListBox и проверять его при каждом изменении итератора. Тем самым мы обнаружим порчу итератора на самой ранней стадии. Но, как вы, возможно, поняли по использова нию индексов, API сообщений для ListBox поддерживает и итерацию с произволь% ным доступом. Существует несколько операций, изменяющих итератор, в частно сти ++, –, += и –=, а также возможность сдвинуть его с помощью арифметических операций над указателями. Поскольку порча итератора извне – это, по всей види мости, редкое событие, то я решил, что буду обнаруживать ее в момент получения значения элемента (строки) в операторах разыменования и индексирования.
494
Наборы
33.2.2. Классы listbox_sequence и combobox_sequence в библиотеке WinSTL Позвольте мне предложить краткое введение в класс winstl::listbox_ sequence и соответствующий ему класс итератора winstl::listbox_const_ iterator. Любой, кто программирует для Windows, знает, что API сообщений для элементов ListBox и ComboBox очень похожи. У последнего есть сообщения CB_GETLBTEXT, CB_GETLBTEXTLEN, CB_GETCOUNT и CB_ADDSTRING, семантически аналогичные соответствующим сообщениям LB_*. Следовательно, вышеупомя нутый класс итератора сможет обслужить и класс winstl::combobox_sequence; различия абстрагируются в простом характеристическом классе. Такой класс listbox_operation_traits для ListBox приведен в листинге 33.3. Обратите внимание на два варианта метода get_text(): для типов char и wchar_t. Это по зволяет классу последовательности, параметризованному строковым типом, ра ботать с кодировками ANSI и Unicode вне зависимости от того, как скомпилиро ван код. Листинг 33.3. Определение класса listbox_operation_traits // Â ïðîñòðàíñòâå èìåí winstl struct listbox_operation_traits { public: // Îïåðàöèè static int err_constant() { return LB_ERR; } static int get_count(HWND hwnd) { return ::SendMessage(hwnd, LB_GETCOUNT, 0, 0); } static int get_text_len(HWND hwnd, int index) { return ::SendMessage(hwnd, LB_GETTEXTLEN, (WPARAM)index, 0); } static int get_text(HWND hwnd, int index, char* s) { return ::SendMessageA(hwnd, LB_GETTEXT, (WPARAM)index, (LPARAM)s); } static int get_text(HWND hwnd, int index, wchar_t* s) { return ::SendMessageW(hwnd, LB_GETTEXT, (WPARAM)index, (LPARAM)s); } };
Этот характеристический класс используется в классе listbox_sequence для параметризации listbox_const_iterator с целью выведения типачлена после довательности const_iterator, как показано в листинге 33.4. Все остальные типычлены определяются в терминах этого.
Порча итератора извне Листинг 33.4. Определение класса listbox_sequence // In namespace winstl template // Ñòðîêîâûé òèï, íàïðèìåð, std::wstring, CString class listbox_sequence { public: // Òèïû-÷ëåíû typedef listbox_sequence<S> class_type; typedef listbox_const_iterator< S , listbox_operation_traits > const_iterator; typedef typename const_iterator::char_type char_type; . . . // È òàê æå äëÿ îñòàëüíûõ òèïîâ-÷ëåíîâ private: typedef listbox_operation_traits control_traits_type; public: // Êîíñòðóèðîâàíèå explicit listbox_sequence(HWND hwndListBox) : m_hwnd(hwndListBox) {} public: // Ñîñòîÿíèå size_type size() const { return size_type(control_traits_type::get_count(m_hwnd)); } bool empty() const; // Âîçâðàùàåò 0 == size() public: // Èòåðàöèÿ const_iterator begin() const { return const_iterator(m_hwnd, 0); } const_iterator end() const { return const_iterator(m_hwnd, int(size())); } const_reverse_iterator rbegin() const { return const_reverse_iterator(end()); } const_reverse_iterator rend() const { return const_reverse_iterator(begin()); } public: // Element Access value_type operator [](difference_type index) const { return const_iterator::get_value_at_(m_hwnd, index); } private: // Member Variables HWND m_hwnd; };
Шаблонный класс listbox_const_iterator показан в листинге 33.5. Листинг 33.5. Определение класса listbox_const_iterator template< typename S , typename CT
// Ñòðîêîâûé òèï // Óïðàâëÿþùèé õàðàêòåðèñòè÷åñêèé òèï
495
496
Наборы
> class listbox_const_iterator : public std::iterator<std::random_access_iterator_tag , S, ptrdiff_t , S const*, S const& > { public: // Òèïû-÷ëåíû typedef S value_type; typedef value_type const& const_reference; typedef value_type const* const_pointer; . . . typedef CT control_traits_type; public: // Êîíñòðóèðîâàíèå listbox_const_iterator(HWND hwndListBox, int index) : m_hwnd(hwndListBox) , m_index(index) , m_bRetrieved(false) {} public: // Ìåòîäû èòåðàöèè const_reference operator *() const; const_pointer operator ->() const { return &operator *(); } class_type& operator ++() { ++m_index; m_bRetrieved = false; return *this; } . . . class_type& operator —() { —m_index; m_bRetrieved = false; return *this; } . . . difference_type compare(class_type const& rhs) const { return m_index - rhs.m_index; } bool operator == (class_type const& rhs) const { return 0 == compare(rhs); } private: // Ïåðåìåííûå-÷ëåíû HWND m_hwnd; int m_index; mutable bool m_bRetrieved; mutable value_type m_value; };
Порча итератора извне считается обнаруженной, если какойто метод харак теристического класса, вызываемый из оператора разыменования в классе итера тора, вернет код LB_ERR (или CB_ERR) (листинг 33.6).
Порча итератора извне
497
Листинг 33.6. Определение оператора разыменования const_reference listbox_const_iterator::operator *() const { if(!m_bRetrieved) { int len; if(control_traits_type::err_constant() == (len = control_traits_type::get_text_len(m_hwnd, m_index))) { throw stlsoft::external_iterator_invalidation("ïîð÷à èòåðàòîðà èçâíå: ñîäåðæèìîå ýëåìåíòà óïðàâëåíèÿ, âîçìîæíî, áûëî èçìåíåíî âíåøíåé ïðîãðàììîé"); } buffer_type buffer(1 + len); if(control_traits_type::err_constant() == (len = control_traits_type::get_text(m_hwnd, m_index , &buffer[0]))) { throw stlsoft::external_iterator_invalidation("ïîð÷à èòåðàòîðà èçâíå: ñîäåðæèìîå ýëåìåíòà óïðàâëåíèÿ, âîçìîæíî, áûëî èçìåíåíî âíåøíåé ïðîãðàììîé"); } m_value.assign(&buffer[0], buffer.size() - 1); m_bRetrieved = true; } return m_value; }
Вследствие этой схемы, значение size() не обязательно равно std:: distance(begin(), end()), а полный перебор диапазона, из которого были уда лены строки, завершается неудачно только в случае, когда оператор разыменова ния пытается получить строку из несуществующей позиции. Но поведение этого кода в ситуации, когда содержимое элемента управления изменяется извне, опре делено корректно. Порча в результате побочных эффектов приложения внутри потока обрабатывается. Что касается действий, выполняемых вне потока, то вопрос о гонке между LB_GETTEXT и LB_GETTEXTLEN решается просто: постулируется, что поведение классов listbox_sequence и combobox_sequence не определено, если они ис пользуются вне обработчика сообщения. (Это стандартный способ вежливо, хотя и несколько уклончиво, сказать: «Так делать нельзя».) Это ограничение не кос нется 99.999% программистов, потому что они только в обработчиках сообщений и манипулируют этими элементами управления. Так или иначе, альтернативы нет, поскольку в общем случае не существует способа безопасно разобраться с си туацией, когда сообщение LB_GETTEXT возвращает строку длиннее, чем результат, полученный от сообщения LB_GETTEXTLEN.
33.3. Перебор разделов и значений реестра Вот мы и подошли к самой интересной теме в этой главе: обработка порчи ите ратора в результате действий вне потока. Мы рассмотрим эту ситуацию и методы борьбы с ней на примере API реестра Windows.
Наборы
498
В Windows реестр – это разделяемая системная иерархическая база данных, в которой можно хранить строковую, целочисленную и двоичную информацию. Если вы с этой темой не знакомы, то проще всего представить реестр в виде набора файловых систем (они называются ульями (hives)), в которых разделы реестра играют роль каталогов для хранения значений (аналоги файлов) и родителей под разделов (аналоги подкаталогов). Между этими двумя концепциями есть не сколько существенных различий, в частности, невозможно перейти к родитель скому разделу с помощью имени «..», но для начала такая аналогия подойдет. API реестра представляет собой набор функций для манипуляции разделами и значениями, в том числе: RegCreateKey(), RegOpenKey(), RegCloseKey(), RegDeleteKey(), RegEnumKey(), RegEnumKeyEx(), RegSetValue(), RegQueryValue(), RegEnum Value() и т.д. С точки зрения расширения STL наибольший интерес представляют функции RegEnumKeyEx() и RegEnumValue(), поскольку в качестве механизма перебора в них используются индексы, что предполагает индексируемые наборы и итерато ры с произвольным доступом (раздел 1.3.5). LONG RegEnumKeyEx( HKEY hKey, DWORD dwIndex, LPTSTR lpName, LPDWORD lpcName, LPDWORD lpReserved, LPTSTR lpClass, LPDWORD lpcClass, PFILETIME lpftLastWriteTime );
// // // // // // // //
Îïèñàòåëü ïåðåáèðàåìîãî ðàçäåëà Èíäåêñ ïîäðàçäåëà Èìÿ ïîäðàçäåëà Ðàçìåð áóôåðà ïîäðàçäåëà Çàðåçåðâèðîâàíî Áóôåð äëÿ ñòðîêè êëàññà Ðàçìåð áóôåðà äëÿ ñòðîêè êëàññà Âðåìÿ ïîñëåäíåé çàïèñè
Поле hKey содержит описатель раздела, подразделы которого мы хотим пере брать. Поле dwIndex – это индекс подраздела, информацию о котором нужно по лучить. Поле lpName – указатель на буфер, в который будет помещено имя разде ла. Поле lpcName –указатель на значение типа DWORD (32разрядное целое без знака), задающее размер буфера, на который указывает lpName; после успешного вызова функции туда же будет записана истинная длина имени раздела. Послед ние четыре параметра не имеют отношения к рассматриваемой теме, поэтому го ворить о них я не буду. Чтобы выполнить перебор, нужно при первом вызове за дать индекс 0, а при каждом последующем увеличивать его на 1. Если вызов завершается успешно, возвращается код ERROR_SUCCESS (0), а если больше под разделов не осталось, то ERROR_NO_MORE_ITEMS (листинг 33.7). Если буфер недо статочно велик, функция возвращает код ERROR_MORE_DATA. Любой другой код свидетельствует о настоящей ошибке, например, о нехватке привилегий. Листинг 33.7. Длинная версия перебора разделов реестра с помощью Windows API HKEY DWORD stlsoft::auto_buffer
key = . . . index = 0; buff(100);
Порча итератора извне
499
for(;;) { DWORD n = buff.size(); LONG res = ::RegEnumKeyEx(key, index, &buff[0], &n , NULL, NULL, NULL, NULL); if(ERROR_MORE_DATA == res) { buff.resize(2 * buff.size()); // Íåäîñòàòî÷íàÿ äëèíà áóôåðà } else if(ERROR_SUCCESS == res) { ::printf("èìÿ ïîäðàçäåëà=%.*s\n", int(buff.size()), buff.data()); ++index; } else if(ERROR_NO_MORE_ITEMS == res) { break; // Áîëüøå äàííûõ íåò } else { . . . // Ñîîáùèòü îá îøèáêå è ïðåêðàòèòü îáðàáîòêó } }
Хотя здесь и используется класс auto_buffer (раздел 16.2), чтобы упростить работу с локальной памятью, все равно объем кода слишком велик для такой про стой задачи. В листинге 33.8 приведена эквивалентная версия на базе шаблонного класса winstl::reg_key_sequence, к обсуждению которого мы вскоре приступим. Листинг 33.8. Короткая версия перебора разделов реестра с помощью класса reg_key_sequence using winstl::reg_key_sequence; HKEY key = . . . reg_key_sequence keys(key); for(reg_key_sequence::iterator b = keys.begin(); b != keys.end(); ++b) { ::printf("èìÿ ïîäðàçäåëà=%s\n", (*b).name().c_str()); }
Все очень просто. Похоже, класс winstl::reg_key_sequence обладает обычными достоинствами: лаконичность, общеупотребительная идиома (STL), безопасность относительно исключений, выразительность, надежность и т.д. Но ведь подобные примеры мы рассматривали в этой книге не раз. В чем же здесь «фишка»?
33.3.1. Так в чем проблема? Поскольку реестр доступен всем процессам в системе, то один процесс может модифицировать подразделы или значения в данном разделе, пока другой еще не закончил перебор. Допустим, к примеру, что мы воспользовались представлен
500
Наборы
ным выше кодом, чтобы обойти раздел HKEY_CURRENT_USER\Software\XSTL\ Vol1\test\ EII\Registry, который в начале перебора содержит такие подразделы: HKEY_CURRENT_USER \Software \XSTL \Vol1 \test \EII \Registry \Key#1 \Key#2 \Key#3 \Key#4 \Key#5
Предположим, что наш процесс только что напечатал Key#3 и собирается пе рейти в начало цикла и снова вызвать RegEnumKeyEx(). Если другой процесс в этот момент удалит подраздел Key#4, то далее наша программа напечатает Key#5 после чего функция RegEnumKeyEx() вернет код ERROR_NO_MORE_ITEMS, и перебор завершится. Ее представление о реестре было изменено внешним аген том; мы наблюдаем порчу итератора извне в результате действия вне потока. Воз можно, вы не видите тут никакой проблемы. В конце концов, если бы мы переби рали содержимое каталога файловой системы, а файл, о котором мы ничего не знаем, в процессе перебора был бы удален, то мы бы не жаловались, – ведь пред ставленная картина на текущий момент не противоречива. Такто оно так, только это не вся картина – самой безобразной ее части не хва тает. Вопервых, нужно помнить о том, что функции API обхода файловой систе мы opendir() и FindNextFile() возвращают следующий файл в соответствии с тем, как это понимает операционная система. С API реестра дело обстоит иначе, здесь клиентский код хранит индекс, используемый в интерфейсе с якобы произ вольным доступом. А давайте представим, что элемент не удален, а добавлен. Пусть мы находимся в той же точке перебора, и в этот момент другой процесс вста вил подраздел Key#2.1. Поскольку разделы реестра упорядочены лексикографи чески, этот подраздел станет третьим. При следующем вызове RegEnumKeyEx() (с индексом 3) мы получим имя четвертого подраздела, которым теперь стал Key#3. Результат перебора будет таким: èìÿ èìÿ èìÿ èìÿ èìÿ èìÿ
ïîäðàçäåëà=Key#1 ïîäðàçäåëà=Key#2 ïîäðàçäåëà=Key#3 ïîäðàçäåëà=Key#3 ïîäðàçäåëà=Key#4 ïîäðàçäåëà=Key#5
Это, конечно, никуда не годится. К счастью API реестра предоставляет меха низм обнаружения таких ситуаций в виде функции RegNotifyChangeKeyValue(), которая объявлена следующим образом: LONG RegNotifyChangeKeyValue( HKEY hKey, // Îïèñàòåëü ðàçäåëà, çà êîòîðûì âåäåòñÿ // íàáëþäåíèå
Порча итератора извне BOOL DWORD HANDLE BOOL
501
bWatchSubtree, // Íàáëþäàòü ëè òàêæå çà ïîäðàçäåëàìè? dwNotifyFilter, // Òèïû èíòåðåñóþùèõ ñîáûòèé hEvent, // Îáúåêò ñèíõðîíèçàöèè, êîòîðîìó ñèãíàëèçèðî// âàòü îá èçìåíåíèè fAsynchronous, // Æäàòü èçìåíåíèé èëè ñèãíàëèçèðîâàòü îáúåêòó?
);
Семантика этой функции зависит от того, какие события вы хотите отслежи вать, и задается это с помощью параметра dwNotifyFilter. Он представляет собой комбинацию флагов, позволяющих следить за изменениями значений, ат рибутов, информации о безопасности и т.д. Нас в данном случае будет интересо вать флаг REG_NOTIFY_CHANGE_NAME, который сообщает о добавлении и удалении подразделов. Функция может работать синхронно или асинхронно. Если флаг fAsynchronous не равен нулю и задан описатель объекта синхронизации – собы тия (которое в данный момент свободно (unsignalled)), то функция возвращает управление немедленно и при первом изменении, отвечающем заданным усло виям, событию посылается сигнал. Именно в таком режиме эта функция ис пользуется здесь и в компонентах из библиотеки WinSTL Registry. Если флаг fAsynchronous равен нулю, то функция не возвращает управление, пока измене ние не произойдет. (Я никогда не слышал о программах, в которых эта функция использовалась бы в синхронном режиме, и не представляю, для чего он может пригодиться.) Смысл параметра bWatchSubtree ясен: если он отличен от нуля, наблюдение ведется за разделом и всеми его подразделами; в противном случае изменения в подразделах игнорируются. А теперь вставим эту функцию в длин ную версию и посмотрим, как можно предотвратить логическую порчу результа та. Изменения показаны в листинге 33.9. Листинг 33.9. Модификация программы ручного перебора для обработки внешних изменений HKEY DWORD stlsoft::auto_buffer winstl::event
key = . . . index = 0; buff(100); ev(true, false);
::RegNotifyChangeKeyValue(key , true , REG_NOTIFY_CHANGE_NAME , ev.get() , true); for(;;) { . . . // Òî æå, ÷òî â ïðåäûäóùåé âåðñèè if(WAIT_OBJECT_0 == ::WaitForSingleObject(ev.get(), 0)) { printf("Ïîð÷à èçâíå!\n"); break; } }
Наборы
502 При тех же условиях, что и выше, получаем такой результат: èìÿ ïîäðàçäåëà=Key#1 èìÿ ïîäðàçäåëà=Key#2 èìÿ ïîäðàçäåëà=Key#3 Ïîð÷à èçâíå!
Итак, мы добились разумного поведения прогаммы, но возлагать такую ответ ственность на пользователя API реестра было бы неправильно. Особенно если при нять во внимание, что наблюдение – это одноразовая акция: после того, как событие получит сигнал, работа RegNotifyChangeKeyValue() прекращается и надо начи нать все сначала! Мне думается, что самое время написать расширение STL. (Как это ни смешно, но при последующем вызове для того же самого описате ля раздела параметры bWatchSubtree и dwNotifyFilter игнорируются. Если вы хотите изменить характеристики наблюдения, то должны будете открыть другой описатель раздела и начать наблюдать за ним. И как вам нравятся API, абстрак ции которых дают большую утечку, чем личный секретарь премьерминистра?)
33.3.2. Библиотека WinSTL Registry Библиотека WinSTL Registry предоставляет классы фасадов, обертывающие API реестра Windows, и включает несколько STLнаборов. Следуя примеру дру гих расширений на платформе Windows, поддерживается компиляция для коди ровок ANSI и Unicode, для чего необходимо абстрагировать кодировку символов и пользоваться настоящими функциями с суффиксами A/W. Следовательно, всего есть шесть шаблонных и три обычных класса. Все они перечислены в табл. 33.1. Таблица 33.1. Компоненты из библиотеки WinSTL Registry Класс или шаблон класса
Назначение
registry_exception
Базовый класс исключений, возбуждаемых библиотекой Это исключение означает, что имется конфликт между ожидаемым и фактическим типом значе) ния; наследует registry_exception Это исключение означает, что у вызывающей программы недостаточно прав для выполнения запрошенной операции; наследует registry_exception Характеристический класс, абстрагирующий A/W формы API реестра, для специализации символьного типа Класс)фасад, представляющий конкретный раздел реестра и содержащий методы для манипуляции подразделами и значениями Класс)фасад, представляющий значение конкретного раздела реестра и содержащий методы для доступа к содержимому в зависимо) сти от фактического типа
wrong_value_type_exception
access_denied_exception
reg_traits
basic_reg_key
basic_reg_value
Порча итератора извне
503
Таблица 33.1. Компоненты из библиотеки WinSTL Registry (окончание) Класс или шаблон класса
Назначение
reg_blob
Специальный класс, используемый в basic_reg_value для представления двоичных значений STL)набор для перебора подразделов данного раздела ( HKEY либо basic_reg_key) STL)набор для перебора значений данного раздела реестра (экземпляр HKEY либо basic_reg_key)
basic_reg_key_sequence basic_reg_value_sequence
Шаблонные классы basic_reg_key_sequence и basic_reg_value_sequence по структуре очень напоминают шаблоны basic_findfile_sequence из библио тек InetSTL (раздел 21.1) и WinSTL (раздел 20.4). В каждом из них есть специ альный класс итератора – basic_reg_key_sequence_iterator и basic_reg_ value_sequence_iterator, а в качестве классов значений выступают basic_ reg_key и basic_reg_value соответственно. Единственное отступление от стро гой объектноориентированной модели заключается в том, что сам класс basic_reg_key не ведет себя как набор подразделов или значений; вы должны явно создать экземпляр требуемого набора (для обоих имеются перегруженные конструкторы, принимающие аргумент типа basic_reg_key ). Класс reg_traits выполняет те же функции, что и filesystem_traits (раз дел 16.3), то есть позволяет записывать другие классы в единой синтаксической форме, не заботясь о том, какая из A/W функций Windows реально используется.
33.3.3. Обработка порчи итератора извне В классахфасадах мы можем обработать порчу итератора двумя способами. Вопервых, можно завести событие в классе последовательности и заставить каж дый итератор проверять его, передавая обратный указатель. С точки зрения потреб ления ресурсов, это наиболее привлекательный вариант, так как событие – это объект ядра, а мы инстинктивно привыкли считать, что объекты ядра – дорогие и дефицитные ресурсы. Альтернатива – включить событие в каждый экземпляр ите ратора. Такое расточительное использование объектов ядра тревожит, но все не так плохо, как кажется, поскольку событие будет общим для взаимосвязанных экземп ляров итераторов точно так же, как это было в классе environment_map (глава 25). Тем не менее, нам придется создавать объект события при каждом вызове begin(), что должно заставить хотя бы призадуматься. Впрочем, в данном случае семантические соображения заставляют принять единственно возможный вариант. Взгляните на показанный ниже код. Проблема очевидна: диапазон [b2, e2) был «создан» после удаления подраздела. В точке его создания раздела Vamoose уже нет. Но при попытке обойти диапазон все равно возникает исключение. reg_key reg_key_sequence
key(HKEY_CURRENT_USER, "SOFTWARE\\ . . "); keys(key);
Наборы
504 reg_key_sequence::iterator reg_key_sequence::iterator reg_key_sequence::iterator reg_key_sequence::iterator
b1; e1; b2; e2;
b1 = keys.begin(); e1 = keys.end(); key.delete_sub_key("Vamoose"); b2 = keys.begin(); e2 = keys.end(); ++b2; // Âîçáóæäàåò èñêëþ÷åíèå!
Все еще хуже. Рассмотрим теперь случай, когда мы перехватываем одно из исключений. В предположении, что событие автосбрасываемое – то есть система автоматически переводит событие в состояние ожидания после возобновления одного потока, – некорректный инкремент b1 останется незамеченным. b1 = keys.begin(); e1 = keys.end(); key.delete_sub_key("Vamoose"); b2 = keys.begin(); e2 = keys.end(); try { ++b2; // Âîçáóæäàåò èñêëþ÷åíèå } catch(stlsoft::iterator_invalidation&) {} ++b1; // Íå âîçáóæäàåò èñêëþ÷åíèå!
Наоборот, событие с ручным сбросом остается в занятом состоянии, пока не будет явно сброшено функцией ResetEvent(), поэтому следующий код возбуж дает исключение вопреки ожиданиям бедного программиста. b1 = keys.begin(); e1 = keys.end(); key.delete_sub_key("Vamoose"); b2 = keys.begin(); e2 = keys.end(); try { ++b1; // Âîçáóæäàåò èñêëþ÷åíèå, êàê è îæèäàëîñü } catch(stlsoft::iterator_invalidation&) {} ++b2; // È çäåñü òî æå âîçáóæäàåò èñêëþ÷åíèå. À ýòî óæå íåîæèäàííî. . .
Получается, что экземпляр reg_key_sequence вместо того, чтобы открывать доступ к подразделам данного раздела, оказывается какимто химерическим мгновенным снимком его состояния: значения и атрибуты могут изменяться без уведомления, но при этом любое изменение подраздела делает недействительны ми все возможные итераторы, полученные от данного экземпляра последователь ности. Стало быть, никуда не денешься – придется ассоциировать наблюдение с экземплярами итераторов. Я уже отмечал, что стоимость этого решения будет
Порча итератора извне
505
разложена на все ассоциированные экземпляры итераторов за счет того, что опи сатель объекта события будет разделяемым, как мы уже делали в других наборах. Поэтому в следующем коде создаются только два события: reg_key_sequence::iterator b1 = keys.begin(); // Íîâûé îáúåêò ñîáûòèÿ reg_key_sequence::iterator b2 = b1; // Îáùåå ñîñòîÿíèå reg_key_sequence::iterator b3; b3 = keys.begin(); // Íîâûé îáúåêò ñîáûòèÿ b1 = b3; // Îáùåå ñîñòîÿíèå
Теперь мы знаем, что делать. Пора познакомиться с реализацией.
33.3.4. Класс winstl::basic_reg_key_sequence В листинге 33.10 представлен интерфейс класса basic_reg_key_sequence. Схожесть с basic_findfile_sequence (глава 20) нам тут очень поможет. В клас се есть пять конструкторов, деструктор, методы прямой и обратной итерации и два метода получения размера. Листинг 33.10. Определение класса basic_reg_key_sequence //  ïðîñòðàíñòâå èìåí winstl template< typename C // Character type , typename T = reg_traits , typename A = processheap_allocator > class basic_reg_key_sequence { public: // Òèïû-÷ëåíû typedef C char_type; typedef T traits_type; typedef A allocator_type; typedef basic_reg_key_sequence class_type; typedef basic_reg_key key_type; typedef key_type value_type; typedef typename traits_type::size_type size_type; typedef basic_reg_key_sequence_iterator iterator; typedef key_type& reference; typedef key_type const& const_reference; typedef HKEY hkey_type; typedef ptrdiff_t difference_type; typedef std::reverse_iterator reverse_iterator; public: // Êîíñòðóèðîâàíèå basic_reg_key_sequence( hkey_type hkey , char_type const* sub_key_name , REGSAM accessMask = KEY_READ); basic_reg_key_sequence( hkey_type hkey , char_type const* sub_key_name , REGSAM accessMask , bool bMonitorExternalInvalidation); explicit basic_reg_key_sequence(key_type const& key);
506
Наборы
basic_reg_key_sequence( key_type const& key , REGSAM accessMask); basic_reg_key_sequence( key_type const& key , REGSAM accessMask , bool bMonitorExternalInvalidation); ~basic_reg_key_sequence() throw(); public: // Èòåðàöèÿ iterator begin(); iterator end(); reverse_iterator rbegin(); reverse_iterator rend(); public: // Ðàçìåð size_type current_size() const; bool empty()const; private: // Ïåðåìåííûå-÷ëåíû hkey_type m_hkey; const REGSAM m_accessMask; const bool m_bMonitorExternalInvalidation; private: // Íå ïîäëåæèò ðåàëèçàöèè basic_reg_key_sequence(class_type const&); class_type& operator =(class_type const&); }; typedef basic_reg_key_sequence reg_key_sequence_a; typedef basic_reg_key_sequence<wchar_t> reg_key_sequence_w; typedef basic_reg_key_sequence reg_key_sequence;
Для обеспечения разумной семантики параметров accessMask и bMonitorExternalInvalidation есть пять конструкторов. В тех случаях, bMonitorExternalInvalidation отсутствует, член когда параметр m_bMonitorExternalInvalidation инициализируется в зависимости от нали чия или отсутствия флага KEY_NOTIFY в параметре accessMask. Наоборот, если параметр bMonitorExternalInvalidation имеется, то наличие или отсутствие флага KEY_NOTIFY в параметре accessMask игнорируется. Такой набор перегру женных конструкторов позволяет поддержать последовательности, итераторы которых как отслеживают, так и не отслеживают изменения. Последний вариант можно выбрать, если вам наплевать на изменения или у вас имеется исключитель ный доступ к разделу (например, изза того, что так установлен дескриптор безо пасности раздела). Наличие метода current_size() вместо size() подчеркивает тот факт, что размер может изменяться и не отражать количества элементов в диапазоне [begin(), end()) или [rbegin(), rend()). Семантика остальных методов не нуждается в пояснениях. В листинге 33.11 показана реализация методов begin() и end(). Листинг 33.11. Определение методов итерации template iterator basic_reg_key_sequence::begin() const { size_type cchKeyName = 0; size_type numEntries = 0;
Порча итератора извне
507
result_type res = traits_type::reg_query_info(m_hkey, NULL, NULL , &numEntries, &cchKeyName, NULL, NULL , NULL, NULL, NULL, NULL); if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } else { if(0 != numEntries) { registry_util::shared_handle* handle= create_shared_handle_(res); ref_ptr ref(handle, false); auto_buffer buffer(++cchKeyName); for(; !buffer.empty(); buffer.resize(2 * buffer.size())) { cchKeyName = buffer.size(); res = traits_type::reg_enum_key(m_hkey, 0, &buffer[0] , &cchKeyName); if(ERROR_MORE_DATA == res) { continue; // È âñå ñíà÷àëà. . . } else if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } else { handle->test_reset_and_throw(); return iterator(handle, buffer.data(), cchKeyName, 0 , m_accessMask); } } } } return end(); } template iterator basic_reg_key_sequence::end() const { result_type res; registry_util::shared_handle*handle = create_shared_handle_(res); ref_ptr ref(handle, false); return iterator(handle, NULL,0, iterator::sentinel_(), m_accessMask); }
Кода, как видите, много, но большая часть его относится к обработке ошибок. Поскольку мы имеем дело с ресурсами, получаемыми непосредственно от API, этого следовало ожидать. Сама же функциональность реализована довольно про сто. При первом обращении к traits_type::reg_query_info() производится вызов функции RegQueryInfoKeyA/W(), чтобы получить количество подразделов и текущую максимальную длину имени подраздела. Первое необходимо для того, чтобы решить, есть ли вообще, что перебирать; если нет, то возвращается итератор
508
Наборы
end(). Второе используется для того, чтобы понять, какой буфер выделять для
получения имени подраздела с индексом 0. Сам буфер – экземпляр класса auto_buffer. Если оказывается, что есть хотя бы один подраздел, то перед началом обхода с помощью метода create_shared_handle_() создается разделяемый описатель (листинг 33.12). Этот метод дублирует описатель раздела и затем вызывает shared_handle::create_shared_handle() – фабричную функцию, которая со здает экземпляр класса shared_handle или monitored_shared_handle в зависи мости от значения члена m_bMonitorExternalInvalidation. Вспомогательный шаблонный класс scoped_handle гарантирует, что дубликат описателя раздела будет освобожден при возникновении любого исключения в методе create_ shared_handle(). Перед тем как вернуть управление, метод sh.detach() осво бождает полностью сконструированный раздел, которым теперь владеет экземп ляр разделяемого. Листинг 33.12. Определение вспомогательного метода create_shared_handle_() template registry_util::shared_handle* basic_reg_key_sequence:: create_shared_handle_(result_type& res) { hkey_type hkey2 = traits_type::key_dup(m_hkey, m_accessMask, &res); if(NULL == hkey2) { . . . // Âîçáóæäàåò èñêëþ÷åíèå } else { scoped_handle sh(hkey2, ::RegCloseKey); registry_util::shared_handle* handle = registry_util::create_shared_handle(hkey2 , m_bMonitorExternalInvalidation , REG_NOTIFY_CHANGE_NAME); sh.detach(); return handle; } }
Возвращаемся к методу basic_reg_key_sequence::begin (листинг 33.11). Еще один вспомогательный шаблонный класс ref_ptr принимает на себя управ ление экземпляром разделяемого описателя, чтобы он не потерялся в случае ис ключения. В оставшейся части функции мы получаем полное имя нулевого эле мента; если получен код ERROR_MORE_DATA, необходимо изменить размер буфера на случай, если между вызовами reg_query_info() и reg_enum_key() будет до бавлен новый раздел с именем длиннее, чем cchKeyName, и лексикографически предшествующий всем остальным. Получив полное имя, мы вызываем конструк тор итератора, передавая ему описатель, имя (указатель и длину), индекс (0) и маску доступа.
Порча итератора извне
509
Метод end() (листинг 33.11) создает экземпляр разделяемого описателя и пе редает его конструктору итератора вместе со специальным индексом, который возвращает метод итератора sentinel_(). Поскольку итераторы двунаправлен ные, должна быть возможность декрементировать концевой итератор. Следова тельно, ему, так же, как итератору begin(), необходим разделяемый описатель. Познакомившись с наиболее интересными частями класса последовательно сти, обратимся к классу итератора basic_reg_key_sequence_iterator. В лис тинге 33.13 представлен его интерфейс. Листинг 33.13. Определение класса basic_reg_key_sequence_iterator //  ïðîñòðàíñòâå èìåí winstl template< typename C , typename T , typename V , typename A > class basic_reg_key_sequence_iterator : public std::iterator< std::bidirectional_iterator_tag , V, ptrdiff_t , void, V // BVT > { public: // Òèïû-÷ëåíû typedef C char_type; typedef T traits_type; typedef V value_type; typedef A allocator_type; typedef basic_reg_key_sequence_iterator class_type; typedef typename traits_type::size_type size_type; . . . // À òàêæå difference_type, string_type, index_type, hkey_type private: typedef typename traits_type::result_type result_type; private: // Êîíñòðóèðîâàíèå friend class basic_reg_key_sequence; basic_reg_key_sequence_iterator( registry_util::shared_handle* handle , char_type const* name , size_type cchName , index_type index , REGSAM accessMask) : m_handle(handle) , m_index(index) , m_name(name, cchName) , m_accessMask(accessMask) { WINSTL_ASSERT(NULL != m_handle); m_handle->test_reset_and_throw(); m_handle->AddRef(); } public: basic_reg_key_sequence_iterator(); basic_reg_key_sequence_iterator(class_type const& rhs);
510
Наборы
~basic_reg_key_sequence_iterator() throw(); class_type& operator =(class_type const& rhs); public: // Ìåòîäû äîñòóïà const string_type& get_key_name() const; // Âîçâðàùàåò m_name public: // Ìåòîäû äâóíàïðàâëåííîé èòåðàöèè class_type& operator ++(); class_type& operator —0(); const class_type operator ++(int); const class_type operator —(int); const value_type operator *() const; bool equal(class_type const& rhs) const; bool operator ==(class_type const& rhs) const; bool operator !=(class_type const& rhs) const; private: // Ðåàëèçàöèÿ static index_type sentinel_(); // Âîçâðàùàåò 0x7FFFFFFF private: // Ïåðåìåííûå-÷ëåíû registry_util::shared_handle* m_handle; index_type m_index; string_type m_name; REGSAM m_accessMask; };
Этот класс представляет двунаправленный итератор. Причина, по которой это не итератор с произвольным доступом, довольно прозаична: я испугался со путствующих трудностей. Класс и так достаточно сложен. Итератор поддержива ет временные по значению ссылки на элементы. Для этого в нем хранится имя пе ребираемого подраздела; зная его, он может вызвать функцию RegOpenKeyExA/ W() (передав ей еще маску доступа) для создания экземпляра basic_reg_key в момент разыменования. Он хранит также индекс, который описывает состоя ние итерации и позволяет сравнивать два экземпляра итераторов. Описатель (m_handle), который разные экземпляры итераторов разделяют с помощью меха низма подсчета ссылок, содержит как описатель раздела реестра, так и (при необ ходимости) объект события. Как все это устроено, мы скоро увидим, но сначала я хочу показать реализации операторов прединкремента и предекремента. Опера тор прединкремента приведен в листинге 33.14. Листинг 33.14. Определение оператора прединкремента template class_type& basic_reg_key_sequence_iterator::operator ++() { WINSTL_MESSAGE_ASSERT("Ïîïûòêà èíêðåìåíòà íåäåéñòâèòåëüíîãî èòåðàòîðà!" , sentinel_() != m_index); size_type cchKeyName = 0; result_type res = traits_type::reg_query_info(m_handle->m_hkey, NULL , NULL, NULL, &cchKeyName, NULL, NULL , NULL, NULL, NULL, NULL); if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } else
Порча итератора извне
511
{ auto_buffer buffer(++cchKeyName); for(; !buffer.empty(); buffer.resize(2 * buffer.size())) { cchKeyName = buffer.size(); res = traits_type::reg_enum_key(m_handle->m_hkey, 1 + m_index , &buffer[0], &cchKeyName); if(ERROR_MORE_DATA == res) { continue; // È âñå ñíà÷àëà. . . } else if(ERROR_NO_MORE_ITEMS == res) { m_index = sentinel_(); // Ñòàíîâèòñÿ êîíöåâûì èòåðàòîðîì break; } else if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } m_name.assign(buffer.data(), cchKeyName); ++m_index; break; } } m_handle->test_reset_and_throw(); return *this; }
После того как мы видели реализацию метода begin(), многое здесь уже ка жется знакомым. Но есть и два важных отличия. Вопервых, при достижении по следнего элемента, индексу присваивается значение, полученное от sentinel_() (оно равно 0x7FFFFFFF). Вовторых, вызывается метод экземпляра разделяемого описателя test_reset_and_throw(). На первый взгляд, он проверяет действи тельность итератора, но на самом деле поведение зависит от того, какой класс раз деляемого описателя вернула фабрика. Теперь рассмотрим определение классов разделяемого описателя. Класс без отслеживания изменений называется shared_handle и определен в подпростран стве имен registry_util (листинг 33.15). Листинг 33.15. Определение класса shared_handle //  ïðîñòðàíñòâå èìåí winstl::registry_util struct shared_handle { public: // Òèïû-÷ëåíû typedef shared_handle class_type; typedef HKEY handle_type; public: // Ïåðåìåííûå-÷ëåíû handle_type m_hkey; private: sint32_t m_refCount; public: // Êîíñòðóèðîâàíèå
512
Наборы
explicit shared_handle(handle_type hkey) : m_hkey(hkey) , m_refCount(1) {} protected: shared_handle(handle_type hkey, sint32_t refCount) : m_hkey(hkey) , m_refCount(refCount) {} protected: virtual ~shared_handle() throw() { WINSTL_MESSAGE_ASSERT("Ðàçäåëÿåìûé îïèñàòåëü óíè÷òîæåí, êîãäà íà íåãî åùå åñòü ññûëêè!", 0 == m_refCount); if(NULL != m_hkey) { ::RegCloseKey(m_hkey); } } public: // Îïåðàöèè sint32_t AddRef() { return ++m_refCount; } sint32_t Release() { sint32_t rc = —m_refCount; if(0 == rc) { delete this; } return rc; } virtual void test_reset_and_throw() {} private: // Íå ïîäëåæèò ðåàëèçàöèè . . . // Çàïðåùàåì êîíñòðóêòîð êîïèðîâàíèÿ è êîïèðóþùèé îïåðàòîð // ïðèñâàèâàíèÿ };
Этот класс очень похож на другие классы shared_handle, с которыми мы встречались ранее (разделы 19.3.7 и 20.5.3), но в данном случае деструктор вирту альный, и определен виртуальный метод test_reset_and_throw(). В этом клас се метод test_reset_and_throw() не делает ничего, что и понятно, поскольку shared_handle не следит за изменениями. В классе monitored_shared_handle (листинг 33.16), производном от shared_ handle, имеются дополнительные члены, необходимые для отслежива ния изменений: экземпляр класса event и тип события. Чтобы наблюдать за изме нениями подразделов, тип события должен быть REG_NOTIFY_CHANGE_NAME. Листинг 33.16. Определение класса monitored_shared_handle // Â ïðîñòðàíñòâå èìåí winstl::registry_util struct monitored_shared_handle
Порча итератора извне
513
: public shared_handle { public: // Òèïû-÷ëåíû typedef shared_handle parent_class_type; typedef monitored_shared_handle class_type; public: // Êîíñòðóèðîâàíèå monitored_shared_handle(handle_type hkey, int eventType) : parent_class_type(hkey, 0) , m_monitor(true, false) , m_eventType(eventType) { set(); AddRef(); } private: // Îïåðàöèè virtual void test_reset_and_throw() { // 1. Ïðîâåðèòü if(WAIT_OBJECT_0 == ::WaitForSingleObject(m_monitor.get(), 0)) { // 2. Ñáðîñèòü set(); // 3. Âîçáóäèòü èñêëþ÷åíèå throw stlsoft::external_iterator_invalidation("ñîäåðæèìîå ðååñòðà èçìåíèëîñü"); } } void set() { try { dl_call("ADVAPI32.DLL", "S:RegNotifyChangeKeyValue", m_hkey , false, (int)m_eventType, m_monitor.get(), true); } catch(missing_entry_point_exception&) { if( 0 != (::GetVersion() & 0x80000000) && LOBYTE(LOWORD(GetVersion())) == 4 && HIBYTE(LOWORD(GetVersion())) < 10) { ; // Íè÷åãî íå äåëàåì è ãëîòàåì èñêëþ÷åíèå } else { throw; } } } private: // Ïåðåìåííûå-÷ëåíû const int m_eventType; event m_monitor; // Ñîáûòèå äëÿ îòñëåæèâàíèÿ èçìåíåíèÿ â ðååñòðå private: // Íå ïîäëåæèò ðåàëèçàöèè . . . // Çàïðåùàåì êîíñòðóêòîð êîïèðîâàíèÿ è êîïèðóþùèé îïåðàòîð // ïðèñâàèâàíèÿ }
514
Наборы
В этом случае метод test_reset_and_throw() играет важную роль. Вопервых, он проверяет состояние события с помощью системного вызова WaitForSingleObject(). Если объект занят, он сбрасывается на случай, если пользователь захочет обнаружить порчу извне и продолжить выполнение. Затем возбуждается исключение. Метод set(), также вызываемый в конструкторе для того, чтобы начать слежение, реализован с помощью dl_call(), поскольку функ ция RegNotifyChangeKeyValue() в системе Windows 95 отсутствует. На этой платформе dl_call() возбудит исключение, которое перехватывается и не рас пространяется дальше, так как библиотека WinSTL Registry в этом случае не под держивает наблюдение за порчей итератора извне. В листинге 33.17 приведена реализация фабричной функции registry_ util:: create_shared_handle(), которая создает подходящий экземпляр разде ляемого описателя. Листинг 33.17. Определение вспомогательной функции create_shared_handle() // Â ïðîñòðàíñòâå èìåí winstl::registry_util static shared_handle* create_shared_handle(HKEY hkey , bool bMonitorExternalInvalidation, int eventType) { if(bMonitorExternalInvalidation) { return new monitored_shared_handle(hkey, eventType); } else { return new shared_handle(hkey); } }
Заключительная часть головоломки – оператор предекремента в классе ите ратора – показана в листинге 33.18. Листинг 33.18. Определение оператора предекремента template class_type& basic_reg_key_sequence_iterator::operator —() { WINSTL_MESSAGE_ASSERT("Ïîïûòêà äåêðåìåíòà íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_handle); size_type cchKeyName = 0; size_type numEntries = 0; result_type res = traits_type::reg_query_info(m_handle->m_hkey , NULL, NULL, &numEntries, &cchKeyName , NULL, NULL, NULL, NULL, NULL, NULL); if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } else {
Порча итератора извне
515
auto_buffer buffer(++cchKeyName); DWORD index; if(m_index == sentinel_()) { index = numEntries - 1; } else { index = m_index - 1; } for(; !buffer.empty(); buffer.resize(2 * buffer.size())) { cchKeyName = buffer.size(); res = traits_type::reg_enum_key(m_handle->m_hkey, index , &buffer[0], &cchKeyName); if(ERROR_MORE_DATA == res) { continue; // È âñå ñíà÷àëà } else if(ERROR_SUCCESS != res) { . . . // Âîçáóäèòü ïîäõîäÿùåå èñêëþ÷åíèå } m_name.assign(buffer.data(), cchKeyName); m_index = index; break; } } m_handle->test_reset_and_throw(); return *this; }
Все это очень похоже на оператор прединкремента с тем отличием, что но вое значение индекса либо вычисляется уменьшением на единицу текущего, либо, если итератор сейчас находится в концевой точке (то есть m_index == sentinel_()), –присваиванием ему значения, на единицу меньшего общего чис ла элементов. Сейчас порча после выполнения begin() обнаруживается при следующей операции инкремента или декремента. Но мы все еще можем разыменовать недей ствительный итератор, и порча не будет обнаружена до следующего инкремента или декремента. Поэтому мы завершим реализацию, добавив проверку действи тельности и в оператор разыменования, как показано в листинге 33.19. Листинг 33.19. Определение оператора разыменования template value_type basic_reg_key_sequence_iterator::operator *() const { WINSTL_MESSAGE_ASSERT("Ïîïûòêà ðàçûìåíîâàíèÿ íåäåéñòâèòåëüíîãî èòåðàòîðà" , NULL != m_handle); m_handle->test_reset_and_throw(); return value_type(m_handle->m_hkey, m_name, m_accessMask); }
516
Наборы
33.4. Резюме Как видите, проблема порчи итератора извне довольно сложна, и написание классов для ее корректной обработки – занятие нетривиальное. Не буду подвер гать сомнению ваш интеллект, притворяясь, что это не так. Существует три вида порчи: в результате побочных эффектов API внутри потока, в результате побоч ных эффектов приложения внутри потока и в результате действий вне потока. Мы детально изучили вопрос о том, как обрабатывать эти ситуации в классах рас ширения STL, и продемонстрировали корректные реализации последовательнос тей и их итераторов в условиях возможной порчи извне, а также механизма обна ружения такой порчи. Важно отметить, что именно здесь отчаянно необходим принцип наибольшего удивления, и мы ему неукоснительно следовали.
33.5. На компакт"диске На компактдиске имеется раздел, в котором описывается ситуация порчи итератора извне на примере популярных библиотек для работы с XML.
Часть III. Итераторы В этой части книги рассматриваются две основных темы: итераторы вывода (раз дел 1.3.2) и адаптеры итераторов. В главе 34 мы познакомимся с усовершенство ванием класса std::ostream_iterator, которая наряду с суффиксами предос тавляет и префиксы, сохраняя полную обратную совместимость. За ней следует интерлюдия, глава 35, в которой показано, как общепринятая стратегия реализа ции итераторов вывода может приводить к бессмысленному коду, а также описан новый паттерн, позволяющий избежать таких казусов. Далее, в главе 36 представлен первый пример адаптера итератора, который трансформирует значения или типы элементов в диапазоне. Показано, что транс формирующий итератор должен порождать временные по значению ссылки на элементы (раздел 3.3.5), если хочет поддержать семантику произвольного доступа (раздел 1.3.5), или недолговечные ссылки на элементы (раздел 3.3.4) – для поддер жки семантики двунаправленности (раздел 1.3.4) или более низкой категории. В этой главе также показано, что применение трансформирующего итератора мо жет дать на удивление большой прирост производительности. Затем еще в одной интерлюдии, главе 37, рассматривается смежный вопрос о несовершенстве схем именования порождающих функций. В главе 38 описывается адаптер итератора, который позволяет манипулиро вать элементами из диапазона структур в терминах всего одного поля структуры. Хотя само определение адаптера итератора прямолинейно, за ним последует дол гая и запутанная история об определении порождающих функций, которые были бы в состоянии справиться с разными сочетаниями категории итератора и изме няемости его самого и полей структуры. И здесь тоже описывается и количествен но измеряется выигрыш в производительности. В главах 39 и 40 мы возвращаемся к теме итераторов вывода и описываем ком поненты для построения строк – в виде обычных буферов символов или экземп ляров строкового класса – из исходных последовательностей с помощью стандар тных алгоритмов. В обоих случаях исследуются различные приемы повышения гибкости ради совместимости с широким спектром источников и типов конечной строки и обсуждаются компромиссы. В главе 41 итераторов нет и следа! Зато мы детально обсудим реализацию компонента adapted_iterator_traits – характеристического класса, предос тавляющего средства сверх тех, что уже есть в классе std::iterator_traits. Такие средства бывают необходимы при определении итераторов в расширени ях STL (да и наборов тоже). Этот компонент различает ряд характеристик ите раторных типов, которыми специализируется, в том числе изменяемость и кате
518
Итераторы
горию ссылок на элементы, позволяя тем самым упростить реализацию адапте ров итераторов. Применение класса adapted_iterator_traits иллюстрируется в главе 42 на примере компонента для фильтрации элементов в диапазоне. Обсуждаются воп росы, всплывающие в связи с поддержкой различных категорий итераторов над возникающим в результате новым диапазоном, и, что особенно важно, формули руются ограничения на клиентский код, в котором используются фильтрующие итераторы. На компактдиске есть дополнительная глава «Индексная итерация», в которой рассмотрен еще один пример использования класса adapted_ iterator_traits. В последней главе 43 демонстрируется, как заставить адаптеры итераторов работать вместе. Вы увидите, что достичь этого можно на удивление простым спо собом.
Глава 34.Усовершенствованный класс ostream_iterator Я много выиграл от критики и никогда не страдал от ее недостатка. – Уинстон Черчилль
34.1. Введение Вам когданибудь приходилось выводить элементы последовательности с от ступами, формируемыми, например, с помощью табуляторов ('\t'), как в сле дующем примере: Çàãîëîâî÷íûå ôàéëû: H:\freelibs\b64\current\include\b64\b64.h H:\freelibs\b64\current\include\b64\cpp\b64.hpp Ôàéëû ðåàëèçàöèè: H:\freelibs\b64\current\src\b64.c H:\freelibs\b64\current\test\C\C.c H:\freelibs\b64\current\test\Cpp\Cpp.cpp
С помощью класса std::ostream_iterator достичь такого результата труд но, а код получается «корявым». Предположим, что мы ищем исходные файлы в текущем каталоге с помощью библиотеки recls/STL (которая сама представляет собой адаптацию набора в стиле, характерном для расширений STL). В этой биб лиотеке имеется набор recls::stl::search_sequence (синоним типа recls:: stl::basic_search_sequence), которому мы передаем начальный ката лог поиска, образец и флаги. Для получения желаемого результата его можно ис пользовать в сочетании с алгоритмом std::copy() и итератором std::ostream_ iterator, как показано в листинге 34.1. Листинг 34.1. Форматирование вывода с помощью std::ostream_iterator 1 2 3 4 5 6 7 8 9
typedef recls::stl::search_sequence srchseq_t; using recls::RECLS_F_RECURSIVE; srchseq_t headers(".", "*.h|*.hpp", RECLS_F_RECURSIVE); srchseq_t impls(".", "*.c|*.cpp", RECLS_F_RECURSIVE); std::cout lst = . . . std::copy( member_selector(lst.begin(), &S::i) , member_selector(lst.end(), &S::i) , std::ostream_iterator(std::cout, " "));
Если не забывать, что Digital Mars не должен видеть версию с указателем на const, а Visual C++ – неконстантную версию оператора разыменования, то все
будет хорошо.
Итератор селекции членов
565
38.4.5. Неизменяющий доступ к константному набору с итераторами типа класса Вот что я имею в виду: const std::list<S> lst = . . . std::copy(member_selector(lst.begin(), &S::i) , member_selector(lst.end(), &S::i) , std::ostream_iterator(std::cout, " "));
Удивительно, но после этого вроде бы безобидного изменения перестали ра ботать все компиляторы, кроме Digital Mars. Проблема в том, что мы вернулись к ситуации, когда неконстантный оператор разыменования пытается преобразо вать ссылку на const в ссылку на неconst. К несчастью, необходимое исправле ние никак не назовешь простым, поскольку итераторы, которые возвращают кон стантные версии методов begin() и end(), – это не const iterator, а const_ iterator. Поэтому не получится написать код, подобный следующему: template member_selector_iterator member_selector(const I iterator, M C::*member) { return member_selector_iterator(iterator, member); }
Нам необходимо выяснить, манипулирует ли итератор константными или не константными типами значений. Поскольку предстоит выводить тип, а не поведе ние, то для того чтобы указать тип возвращаемого значения, придется прибегнуть к механизму распознавания типа (раздел 13.4.2); выбор специализации с по мощью перегрузки шаблона функции не годится. Нужно, чтобы нечто исследовало тип итератора и на его основе сделало вывод о том, что возвращать: member_selector_iterator или member_selector_ iterator. Мы уже видели, как производится выбор типа (раздел 13.4.1), поэтому все, что нам необходимо, – это значение, анализируемое на этапе компиляции и обозначающее, что требуется: const или неconst. Первое, что приходит в голову, – попытаться распознать, является ли тип зна чения константным, с помощью характеристических классов std::iterator_ traits и base_type_traits (раздел 12.1.1): base_type_traits::is_const
Использовать это надо было бы, как показано в следующей громоздкой конст рукции, куда входят шаблоны select_first_type_if (раздел 13.4.1) и base_ type_traits: template typename select_first_type_if< member_selector_iterator , member_selector_iterator , base_type_traits::is_const >::type member_selector(I iterator, M C::*member)
Итераторы
566 {
typedef typename select_first_type_if< member_selector_iterator , member_selector_iterator , base_type_traits::is_const >::type iterator_t; return iterator_t(iterator, member); }
Если, читая этот код, вы не пришли к выводу, что мир сошел с ума, то вы луч ше меня. Это ужасно. Посмотрим, нельзя ли чтонибудь упростить. Как насчет такого варианта: template typename msi_traits::type member_selector(I iterator, M C::*member) { typedef typename msi_traits::type iterator_t; return iterator_t(iterator, member); }
Мы перенесли это кошмарное распознавание и выбор типа в шаблонгенера тор msi_traits и тем самым довели сложность до приемлемого уровня. Вот как выглядит определение, в котором все разнесено по типамчленам, чтобы было проще читать: template struct msi_traits { private: // Òèïû-÷ëåíû typedef member_selector_iterator const_msi_type; typedef member_selector_iterator non_const_msi_type; typedef typename std::iterator_traits::value_type tested_member_type; public: typedef typename select_first_type_if< const_msi_type , non_const_msi_type , base_type_traits::is_const >::type type;
Но, хотя пара компиляторов такой код «съедают», это, к сожалению, не реше ние. Причина в том, что, согласно стандарту (C++03: 24.3.1;2), специализация std:: iterator_traits для указателей на const должна иметь такой вид: template struct std::iterator_traits { typedef random_access_iterator_tag typedef T typedef T const* typedef T const* typedef ptrdiff_t };
iterator_category; value_type; pointer; reference; difference_type;
Ясно, что при проверке константности типа значения итератора, являющегося константным указателем, получится неверный результат. (Я не выяснял, почему
Итератор селекции членов
567
некоторые компиляторы дают желательное, но неправильное поведение при такой проверке, потому что особого смысла в этом не вижу. Было подозрение, что в биб лиотеках, поставляемых с некоторыми компиляторами, value_type определен как const T, но оказалось, что это не так. Поэтому, наверное, все дело в ошибке реализа ции самого компилятора. Как бы то ни было, мы больше об этом думать не будем.) Хотя проверка value_type и не дает решения, но показанное выше определение частичной специализации содержит многообещающий ключ. Так как типычлены pointer и reference специализации std::iterator_traits указателем на const таки содержат информацию о const, то мы можем воспользоваться любым из них: template struct msi_traits { . . . typedef typename std::iterator_traits::pointer tested_member_type;
После такого исправления все становится гораздо лучше. CodeWarrior, GCC, Intel и Visual C++ 7.1 компилируют этот код без единой жалобы. Памятуя о том, что Visual C++ 6 мы уже отложили в сторону, недовольны остались только Borland и Digital Mars. Что касается Digital Mars, то мы можем оставить первона чальный вариант, который правильно работал, пусть даже исходя из неправиль ных предпосылок. А вот Borland 5.6 придется отправить в ту же урну, что и Visual C++ 6 изза того, что поддержка сложных манипуляций с шаблонами недостаточ на для отыскания пригодного на практике обходного решения.
38.4.6. Изменяющий доступ к набору с итераторами типа класса Вот что это такое: std::list lst = . . . std::transform( member_selector(lst.begin(), &S::i) , member_selector(lst.end(), &S::i) , member_selector(lst.begin(), &S::i) , doubler());
К счастью, проделанная ранее работа принесла плоды. CodeWarrior, GCC, Intel, и Visual C++ 7.1 компилируют этот код, а Digital Mars продолжает работать с первоначальной простой порождающей функцией, поэтому мы можем добиться очень хорошей поддержки для оператора выбора члена с помощью единственной директивы #ifdef. Что касается Borland 5.6 и Visual C++ 6, то мы можем обеспе чить для них неизменяющий доступ, а для Borland – даже изменяющий, включив дополнительные #ifdef. Хоть и не идеальное, но все же приемлемое решение для поддержки отнюдь не простой техники.
38.4.7. Выбор константных членов До сих пор мы говорили о доступе для чтения или чтения/записи к некон стантным членам объектов классов, являющихся элементами константных и не
Итераторы
568
константных последовательностей. В частности, речь шла о классе pan_slice_t, в котором переменнаячлен len объявлена как неconst. Но мы пока ни слова не сказали о доступе к константным членам объектов, входящих в последователь ность, например, объектов такого класса: struct cpan_slice_t { const size_t char const* const };
len; str;
Мы опустим вопрос о том, как инициализировать и манипулировать массивами такого типа, и сосредоточимся на том, что произойдет при попытке обойти последо вательность, содержащую неконстантные экземпляры класса cpan_slice_t. Можно было бы ожидать, что разработанный ранее механизм выведения типа не сможет затребовать константный типчлен, когда итератор неконстантный. struct CS { const int i; }; CS acs[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::copy( member_selector(&acs[0], &CS::i) , member_selector(&acs[0] + n, &CS::i) , std::ostream_iterator(std::cout, " "));
К счастью, существующая реализация все это учитывает. Причина в том, что, когда мы берем адрес CS::i, создается указатель типа const int C::*, следова тельно, тип M выводится как const int, а не int. Так как параметры M и I шаблона порождающей функции не связаны между собой, то неконстантность итератора (типа I) никак не отражается на константности M. Потому все и работает. Вещь!
38.5. Резюме Эта глава оказалась клубком противоречий. Мы показали, что класс member_ selector_iterator, прекрасно согласуясь с принципами композиции и разнообра% зия, в то же время обладает понятным интерфейсом, что позволяет писать прозрач ный клиентский код. Однако выяснилось, что для достижения этой кажущейся простоты пришлось преодолеть немало препятствий, а получившаяся реализация большинства порождающих функций далека от прозрачности. Она не идеальна – всякий (не исключая меня самого), кто попытается внести в реализацию суще ственные изменения, никакой радости не испытает, но иногда приходится идти на компромиссы. Принимая во внимание удобства, которые класс member_ selector_ iterator несет пользователям, я доволен тем, что получилось.
38.6. На компакт"диске На компактдиске имеется раздел, в котором описываются еще более кошмар ные детали, касающиеся устранения несогласованности между различными ком пиляторами и библиотеками.
Глава 39. Конкатенация С-строк Я могу не разделять ваших убеждений, но отдам жизнь за то, чтобы вы могли их выс% казать – Вольтер Я обдумывал такой же план; мы уже мыс% лить стали одинаково! – Кот в сапогах, Шрек2
39.1. Мотивация В предыдущей главе мы видели, как класс member_selector_iterator ис пользуется в библиотеке Pantheios для вычисления полной длины строки, со ставляемой из кусочков, хранящихся в массиве структур (pan_slice_t const*). Для этого применялся стандартный алгоритм accumulate(). В этой главе мы зай мемся другой стороной операции протоколирования: конкатенацией кусочков строки в уже выделенном буфере с помощью стандартного алгоритма copy(). Да вайте не будет откладывать и сразу же обратимся к реализации (префикс про странства имен stlsoft, проверки выполнения контракта и обработка ошибок опущены): int pantheios_log_n(pan_sev_t severity , pan_slice_t const* slices , size_t numSlices) { // 1. Âû÷èñëèòü äëèíó áóôåðà size_t n = std::accumulate(member_selector(slices, &pan_slice_t::len) , member_selector(slices + numSlices, &pan_slice_t::len) , size_t(0)); // 2. Âûäåëèòü ïàìÿòü auto_buffer buffer(1 + n); // 3. Çàïèñàòü ñòðîêè â áóôåð std::copy(slices, slices + numSlices , cstring_concatenator(&buffer[0])); // 4. Äîáàâèòü çàâåðøàþùèé íóëü buffer[n] = '\0'; // 5. Ïåðåäàòü ñåðâåðíîé ÷àñòè return pantheios_be_logEntry(. . ., severity, &buffer[0], n); }
570
Итераторы
Шаг 1 рассматривался в предыдущей главе, посвященной адаптеру member_ selector_iterator и его порождающим функциям. Шаг 2 гарантирует, что име ется область памяти необходимого размера, для чего применяется класс auto_ buffer (раздел 16.2). Шаги 4 и 5 пояснений не требуют. Остался только шаг 3. В этом предложении мы обращаемся к алгоритму std::copy(), передавая ему итераторы, обозначающие начало и конец массива строк, а также результат, возвращенный порождающей функцией cstring_concatenator(). Мы знаем, что это должен быть полноценный итератор вывода (раздел 1.3.2), и могу сказать, что он является экземпляром класса, который и станет предметом настоящей гла вы, – stlsoft::cstring_concatenator_iterator.
39.2. Негибкая версия Существенное отличие от шага 1 заключается в том, что мы здесь не выбираем члены, как в случае member_selector<slices, &pan_slice_t::ptr), а исполь зуем объекты класса pan_slice_t целиком. Исходя из этого, вы могли бы предпо ложить, что класс cstring_concatenator_iterator написан специально для ра боты с pan_slice_t. Такое определение могло бы выглядеть так, как показано в листинге 39.1. Хотя это и не настоящее определение cstring_concatenator_ iterator, некоторые его особенности присутствуют, поэтому рассмотрим его внимательнее. Секция Типычлены вполне типична для итераторов вывода, и большинство типов выражено в терминах специализации родительского класса std::iterator. Поскольку это итератор вывода, я воспользовался паттерном Dereference Proxy (глава 35). Листинг 39.1. Первая попытка определения cstring_concatenator_iterator class cstring_concatenator_iterator : public std::iterator { public: // Òèïû-÷ëåíû typedef char char_type; typedef cstring_concatenator_iterator class_type; private: class deref_proxy; friend class deref_proxy; public: // Êîíñòðóèðîâàíèå explicit cstring_concatenator_iterator(char* s) : m_dest(s) { STLSOFT_ASSERT(NULL != s); } public: // Ìåòîäû èòåðàòîðà âûâîäà class_type& operator ++(); class_type& operator ++(int); deref_proxy operator *(); private: // Ðåàëèçàöèÿ class deref_proxy { public: // Êîíñòðóèðîâàíèå
Конкатенация С8строк
571
deref_proxy(cstring_concatenator_iterator* it) : m_it(it) {} public: // Ïðèñâàèâàíèå void operator =(pan_slice_t const& slice) { m_it->invoke_(slice); } private: // Ïåðåìåííûå-÷ëåíû cstring_concatenator_iterator* const m_it; private: // Íå ïîäëåæèò ðåàëèçàöèè void operator =(deref_proxy const&); }; private: // Ðåàëèçàöèÿ void concat_(char const* s, size_t len) { stlsoft::copy_n(m_dest, s, len); m_dest += len; } void invoke_(pan_slice_t const& slice) { this->concat_(slice.ptr, slice.len); } private: // Ïåðåìåííûå-÷ëåíû char_type* m_dest; };
При разыменовании этот итератор возвращает экземпляр класса deref_ proxy, а когда ему присваивается значение типа pan_slice_t, он вызывает метод cstring_concatenator_iterator::invoke_(). Последний в свою очередь вы зывает метод concat_(), передавая указатель на отрезок массива и его длину. (Зачем мы поместили эти действия в отдельный метод concat_(), станет ясно позже.) Метод concat_() копирует строки, хранящиеся в элементах диапазона, в конечный буфер, указатель на который был передан конструктору итератора, с помощью алгоритма stlsoft::copy_n() и продвигает указатель на соответ ствующее число символов. (Алгоритм copy_n() не включен в стандарт C++98, но будет включен в версию C++0x. Он уже входит в дистрибутивы некоторых стандартных библиотек, но для пущей переносимости я пользуюсь версией из STLSoft. Этот и многие другие алгоритмы будут обсуждаться в томе 2.) Таким образом, каждое присваивание приводит к дописыванию в конец буфе ра, и в результате получается непрерывная строка: char cstring_concatenator_iterator
buff[12]; cci(&buff[0]);
*cci = pan_slice_t("Black", 5); *cci = pan_slice_t(" ", 1); *cci = pan_slice_t("Grape", 5); assert(0 == ::strncmp(&buff[0], "Black Grape", 11));
Однако в этой реализации есть очевидная проблема – она жестко привязана к типу pan_slice_t. И, следовательно, для повторного использования совершен
572
Итераторы
но непригодна. Быть может, правильнее было бы обрабатывать только Cстроки (то есть char const*)? В конце концов, этот итератор предназначен для конкате нации Cстрок, разве не так? В таком случае определение cstring_concatenator_iterator::invoke_() должно было бы выглядеть примерно так: void invoke_(char const* s) { this->concat_(s, ::strlen(s)); } . . .
Тогда на шаге 3 следовало бы воспользоваться классом member_selector_ iterator для выбора из каждого элемента члена ptr, который передается итера тору вывода: std::copy( member_selector(slices, &pan_slice_t::ptr) , member_selector(slices + numSlices, &pan_slice_t::ptr) , cstring_concatenator(&buffer[0]));
Увы, так не получится. В разделе 9.3.1 мы говорили, что в библиотеке Pan theios на прикладном уровне работают шаблонные функции, в которых для создания экземпляра pan_slice_t, соответствующего протоколируемой переменной, исполь зуются прокладки c_str_data_a и c_str_len_a, а не c_str_ptr (или c_str_ptr_a). Это повышает эффективность в тех случаях, когда содержимое протоколируемого типа представлено непрерывным массивом символов, не завершающимся нулем. Для таких типов функции c_str_ptr_a() могут не выделять память, не копиро вать в нее содержимое и не добавлять завершающий нуль только для того, чтобы после вызова прокладки эту память освободить. Выигрыш в производительности очевиден. Поэтому нет никакой гарантии, что pan_slice_t::ptr будет указывать на за вершающуюся нулем Cстроку, и, стало быть, strlen() будет возвращать непра вильные значения. В лучшем случае это приведет к затиранию памяти, в худ шем – к аварийному завершению программы. Так определенный класс cstring_ concatenator_iterator смог бы работать только с последовательностями типов, для которых не существует неявного преобразования в Cстроки (char*, char[], char const*, const char[], CString из библиотеки MFC и т.д.). Так как неявные преобразования, особенно в тип char const*, – вообще не очень удачная идея, вряд ли такой подход стоит рекомендовать. К счастью, есть более общий подход – ну конечно же, прокладки строкового доступа (раздел 9.3.1)!
39.3. Класс stlsoft::cstring_concatenator_iterator В окончательном определении итератора (листинг 39.2) показаны только от личия от предыдущего. В этом классе итераторы хранят еще общее число симво лов, уже записанных в буфер; это может оказаться полезно, если вы захотите вста
Конкатенация С8строк
573
вить в конечную строку дополнительные последовательности символов в опреде ленных точках итерации. (Заодно это позволяет клиенту проверять, что число за писанных символов не отличается от ожидаемого, и тем самым облегчает про граммирование по контракту (глава 7)). Листинг 39.2. Окончательное определение cstring_concatenator_iterator template class cstring_concatenator_iterator : public std::iterator { public: // Òèïû-÷ëåíû typedef C char_type; typedef cstring_concatenator_iterator class_type; . . . public: // Êîíñòðóèðîâàíèå explicit cstring_concatenator_iterator(char_type* dest , size_t* pNumWritten = NULL) : m_dest(dest) , m_numWritten((NULL != pNumWritten) ? pNumWritten : dummy_()) { *m_numWritten = 0; } public: // Ìåòîäû èòåðàòîðà âûâîäà . . . private: // Ðåàëèçàöèÿ class deref_proxy { . . . public: // Ïðèñâàèâàíèå template void operator =(S const& s) { m_it->invoke_(s); } . . . }; private: // Ðåàëèçàöèÿ void concat_(char_type const* s, size_t len) { std::copy_n(m_dest, s, len); m_dest += len; *m_numWritten += len; } template void invoke_(S const& s) { // Âûçâàòü ïðîêëàäêè è ïåðåäàòü ýêçåìïëÿðó, çàìåùàþùåìó èòåðàòîð this->concat_(::stlsoft::c_str_data(s), ::stlsoft::c_str_len(s)); } static size_t* dummy_() {
Итераторы
574 static size_t s_dummy; return& s_dummy; } private: // Ïåðåìåííûå-÷ëåíû char_type* m_dest; size_t* m_numWritten; };
И cstring_concatenator_iterator::deref_proxy::operator =(), и cstring_concatenator_ iterator::invoke_() – шаблонные функции, который принимают в качестве аргумента тип, являющийся строкой или имеющий строко вое представление. Теперь понятно, почему метод invoke_() извлекает указатель на строку и ее длину из аргумента, переданного deref_proxy::operator =(), пе ред обращением к concat_(). Это проявление общего паттерна, согласно которо му значения, возвращаемые прокладками, сохраняются в том же предложении, где вызывается функция, следуя правилам обращения со результатами, получае мыми от прокладок доступа (раздел 9.3). Заодно мы избегаем многократного вы зова прокладок в случаях, когда результирующие значения используются более одного раза. Стоимость таких вызовов может быть довольно высока, если возвра щается экземпляр строки, представляющей преобразованное значение. Совет. Применяйте вспомогательные функции, чтобы в одной логической операции не вызывать прокладки несколько раз.
Уже в который раз относительно простое использование прокладок строково го доступа позволило нам существенно повысить гибкость без какого бы то ни было ущерба для производительности, надежности и понятности, лишь чутьчуть уменьшив прозрачность компонента. Класс cstring_concatenator_iterator можно теперь использовать с любым типом, для которого определены прокладки c_str_data и c_str_len, и он честно конкатенирует правильное количество симво лов (возвращаемое функцией c_str_len()) вне зависимости от того, завершает ся нулем Cстрока, которую возвращает c_str_data(), или нет.
39.4. Порождающие функции Осталось лишь определить шаблон порождающей функции, которая прини мает неконстантный указатель на буфер символов, куда будут записываться ре зультаты. Одновременно с добавлением возможности получить общее число за писанных символов я определил еще второй перегруженный вариант: template cstring_concatenator_iterator cstring_concatenator(C* s) { return cstring_concatenator_iterator(s, NULL); } template cstring_concatenator_iterator
Конкатенация С8строк
575
cstring_concatenator(C* s, size_t& numWritten) { return cstring_concatenator_iterator(s, &numWritten); }
Обратите внимание, что во втором варианте numWritten – ссылка на size_t, а не указатель. Адрес этой ссылки передается в конструктор итератора. В общем случае этот прием полезен, когда вы хотите быть уверены, что обертываемой фун кции или конструктору не будет передан нулевой указатель. Впрочем, в данном случае конструктор указателя вполне допускает нулевой указатель, поэтому это лишь ненужное и сбивающее с толку усложнение. В результате я оставил един ственную порождающую функцию: template cstring_concatenator_iterator cstring_concatenator(C* s, size_t* pNumWritten = NULL) { return cstring_concatenator_iterator(s, pNumWritten); }
Это лучше и с точки зрения клиентского кода. Тот факт, что numWritten мо жет быть изменен, становится яснее при такой записи: std::copy(. . . , stlsoft::cstring_concatenator(&result[0] , &numWritten));
чем при такой: std::copy(. . . , stlsoft::cstring_concatenator(&result[0] , numWritten));
Совет. Если нулевой указатель допустим, лучше передавать параметр в виде указателя, чтобы подчеркнуть, что его можно изменять в клиентской программе.
39.5. Резюме Познакомившись с определением адаптера cstring_concatenator_iterator (и порождающей его функции cstring_concatenator()), мы поняли, как pantheios_log_n() поддерживает требования, предъявляемые к шаблонным функциям из библиотеки Pantheios, обеспечивая максимальную гибкость. На шаге 1 (раздел 38.3) вычисляется длина потребного буфера, для чего необходим лишь один проход по массиву фрагментов. На шаге 2 в стеке выделяется память на 2048 символов, поэтому обращение к куче производится лишь в случае, когда полная длина не менее 2048. На шаге 3 все фрагменты строки (не обязательно завершающиеся нулем) конкатенируются в выделенном буфере за один проход без каких бы то ни было преобразований (так как прокладка c_str_data() для типа pan_slice_t просто возвращает член ptr). На шаге 4 добавляется завер шающий нуль, а на шаге 5 готовая строка передается серверной функции прото
576
Итераторы
колирования, которая находится во внешней библиотеке. Хотя строка завершает ся нулем, мы дополнительно передаем ее длину, чтобы максимально упростить обработку на стороне сервера. Я полагаю, что тело функции pantheios_log_n() – отличный пример прак тического применения расширений STL. В нем нет никакого постороннего кода, а вызовы различных алгоритмов ясно и недвусмысленно свидетельствуют о на значении и применяемых механизмах.
39.6. На компакт"диске На компактдиске есть веселая сказка о компиляторе, генерирующем некор ректный объектный код, и нахальное средство для борьбы с этой напастью.
Глава 40. Конкатенация строковых объектов И, самое главное, никогда не думайте, что вы недостаточно хороши. Человек не должен так думать о себе. Я считаю, что люди ви% дят в вас то, что видите вы сами. – Айзек Азимов Удача приходит к тем, кто готов к встрече с ней! – Эдна Моул
40.1. Введение В нескольких проектах мне приходилось разбивать строку на части, а затем собирать их в другом виде. В главе 27 мы видели, как можно просто и эффективно разбить строку. А с помощью описываемого ниже компонента мы сумеем так же просто собрать из них новую строку. В предыдущей главе мы написали итератор вывода для конкатенации Cстрок, а теперь обратимся к более объектноориентированной стороне той же проблемы: конкатенации в экземпляр строкового класса. У обеих реализаций есть ряд общих черт, которые я повторно обсуждать не буду, но есть и заметные отли чия. Вот имито мы и займемся в этой главе.
40.2. Класс stlsoft::string_concatenator_iterator В листинге 40.1 приведена простая версия нужного нам итератора. Листинг 40.1. Первая string_concatenator_iterator template< typename S // Òèï ñòðîêè â êîòîðóþ ïèøåò èòåðàòîð , typename D // Òèï ðàçäåëèòåëÿ > class string_concatenator_iterator : public std::iterator<std::output_iterator_tag , void, void, void, void> { public: // Òèïû-÷ëåíû typedef S string_type; typedef D delimiter_type;
578
Итераторы
typedef string_concatenator_iterator<S, D> class_type; private: class deref_proxy; friend class deref_proxy; public: // Êîíñòðóèðîâàíèå string_concatenator_iterator(string_type& s , delimiter_type const& delim) : m_s(s) , m_delim(delim) {} public: // Ìåòîäû èòåðàòîðà âûâîäà deref_proxy operator *() { return deref_proxy(this); } class_type& operator ++(); // Âîçâðàùàåò *this class_type& operator ++(int);// Âîçâðàùàåò *this private: // Ðåàëèçàöèÿ class deref_proxy { public: // Êîíñòðóèðîâàíèå deref_proxy(string_concatenator_iterator* it) : m_it(it) {} public: // Ïðèñâàèâàíèå template void operator =(S3 const& value) { m_it->invoke_(value); } private: // Ïåðåìåííûå-÷ëåíû string_concatenator_iterator* const m_it; }; template void invoke_(S3 const& value) { if(0 != c_str_len(m_str)) { m_str += c_str_ptr(m_delim); } m_str += c_str_ptr(value); } private: // Ïåðåìåííûå-÷ëåíû string_type& m_str; delimiter_type const& m_delim; };
Естественно, к классу string_concatenator_iterator прилагается порож дающая функция: template string_concatenator_iterator<S, D> string_concatenator(S& s, D const& delim) { return string_concatenator_iterator<S, D>(s, delim); }
Конкатенация строковых объектов
579
На первый взгляд, совершенно бесхитростный итератор вывода. Определены все обязательные типычлены, операторы пред и постинкремента возвращают *this, и в их реализации применен паттерн Dereference Proxy (глава 35). Как мы видели при обсуждении класса cstring_concatenator_iterator (раздел 39.3), метод deref_proxy::operator =() представляет собой шаблонную функцию, пе редающую свой аргумент методу string_concatenator_iterator::invoke_(), который и сам является шаблоном. Именно в методе invoke_() и производится конкатенация строк с помощью трех функций прокладки строкового доступа (раздел 9.3.1). Сначала c_str_len определяет, содержит ли чтонибудь конечная строка m_str. Если да, то одна из перегруженных функций c_str_ptr() сначала дописывает в ее конец копию раз делителя, хранящегося в переменнойчлене m_delim. И наконец другая перегру женная функция c_str_ptr() дописывает в конец строки параметр value. (Если конечная строка и разделитель принадлежат одному и тому же типу, то в действи тельности вызывается одна и та же перегруженная функция.) Таким образом, строки вида abc,def,gh,i,j,klmn строятся без лишнего разделителя в начале или в конце, а именно так составная строка и должна как правило выглядеть. Завершает картину порождающая функция string_concatenator(), благо даря которой клиентский код выглядит лаконично и прозрачно: char const* strings[] = { "abc" , "defg" , "h" , "ijklm" }; std::string result; std::copy(&strings[0], &strings[0] + STLSOFT_NUM_ELEMENTS(strings) , stlsoft::string_concatenator(result, "")); assert("abcdefghijklm" == result);
Ее можно использовать в сочетании с классом string_tokeniser (раздел 27.6) и унарным объектомфункцией, если нужно преобразовать лексемы в стро ку. Вот как это делается в модуле обработки параметров командной строки в од ном из моих инструментов разработки: stlsoft::string_tokeniser<string_t, char> notTokens((*it).second, ','); string_t translatedItems; std::transform(notTokens.begin(), notTokens.end() , stlsoft::string_concatenator(translatedItems, ",") , not_hyphen()); // Äîáàâëÿåò â íà÷àëî '-' èëè óäàëÿåò, // åñëè îí óæå åñòü (*it).second.swap(translatedItems);
В результате строка "bc56,-cw8,dm,gcc34,-vc6,vc7" преобразуется в "-bc56, cw8,-dm,-gcc34,vc6,-vc7".
Итераторы
580
40.3. Гетерогенные строковые типы В итераторе может встречаться до трех разных строковых типов. Вопервых, тип конечной строки S. Вовторых, тип разделителя D. Втретьих, тип элемента, передаваемого оператору присваивания (из класса deref_proxy). Поскольку в методе invoke_() используется прокладка строкового доступа c_str_ptr, то эти типы могут как совпадать, так и различаться. Можно даже использовать итератор с такими невообразимыми сочетаниями, как MFC, STL и STLSoft: typedef CArray VariantArray_t; VariantArray_t CComboBox& CString
ar; wnd = . . . // Îêíî ðåäàêòèðîâàíèÿ, ñîäåðæàùåå ðàçäåëèòåëü result;
ar.Add(COleVariant("Space"); ar.Add(COleVariant(long(1999)); ar.Add(COleVariant("- More like it’s"); ar.Add(COleVariant(COleDateTime(2005, 12, 23, 13, 14, 52)); // Èñïîëüçóåòñÿ àäàïòåð ýêçåìïëÿðà CArray (ðàçäåë 24.7), ïîýòîìó ñ ar ìîæíî // îáðàùàòüñÿ, êàê ñ STL-ïîñëåäîâàòåëüíîñòüþ mfcstl::CArray_iadaptor arr(ar); std::copy( arr.begin(), arr.end() , stlsoft::string_concatenator(result, wnd)); std::cout { public: // Òèïû-÷ëåíû . . . private: // Êîíñòðóèðîâàíèå string_concatenator_iterator(string_type* s , delimiter_type const* delim) : m_s(s) , m_delim(delim) {} public: static class_type create(string_type& s, delimiter_type const& delim) { return class_type(&s, &delim); } public: // Ìåòîäû èòåðàòîðà âûâîäà . . . private: // Ðåàëèçàöèÿ
582
Итераторы
class deref_proxy { . . . // Òî æå, ÷òî â ïðåäûäóùåé âåðñèè }; template void invoke_(S3 const& value) { if(0 != c_str_len(*m_str)) { *m_str += c_str_ptr(*m_delim); } *m_str += c_str_ptr(value); } private: // Ïåðåìåííûå-÷ëåíû string_type* m_str; delimiter_type const* m_delim; }; template string_concatenator_iterator<S, D> string_concatenator(S& s, D const& delim) { return string_concatenator_iterator<S, D>::create(s, delim); }
Вопервых, переменныечлены сделаны указателями. Это касается только конструктора и метода invoke_(). Вовторых, конструктор сделан закрытым и добавлен открытый статический метод create(), вызываемый порождающей функцией. Это, конечно, не запрещает объявлять невременные экземпляры ите ратора, но означает, что пользователь вынужден будет использовать для этой цели метод create(), и разумно предположить, что это заставит его на минутку задуматься, прочесть документацию по классу и решить, прав ли он в своем наме рении. Вот, собственно, и все. Теперь итератор ведет себя, как должно, и обеспечива ет всю продемонстрированную выше гибкость.
40.5. Резюме Мы видели, как с минимальными усилиями и с помощью прокладок строково го доступа получить очень гибкий компонент, который позволяет применить для конкатенации строк стандартные алгоритмы. Компонент stlsoft::string_ concatenator_ iterator согласуется с принципами композиции, наименьшего удивления, модульности и прозрачности. Его интерфейс абсолютно понятен, так как состоит всего из одной простой функции, и, стало быть, обеспечивает прозрач ность клиентского кода.
Глава 41. Характеристики адаптированных итераторов Это интересная и сложная проблема, на ис% следование всех аспектов которой ушло бы больше времени, чем мы располагаем. А что, если выделить вопрос, который смущает вас больше всего, и четко сформулировать его? – Джонатон Хемлок
41.1. Введение Для чтения оставшихся глав части III нам понадобятся коекакие инструмен ты. В следующей главе мы посмотрим, как с помощью предикатаселектора от фильтровать некоторые элементы из диапазона. А в дополнительной главе «Ин дексная итерация» на компактдиске, обсуждается вопрос об индексирующих итераторах, то есть о применении такого адаптера, который ассоциирует с итера тором некоторую порядковую величину, отражающую состояние итерации, со храняя при этом интерфейс, характеристики типов и поведение базового итерато ра. В обоих случаях нам понадобится умение распознавать определенные аспекты типа базового итератора, манипулировать ими и представлять в другом виде. Поэтому в этой главе мы рассмотрим шаблонный класс adapted_iterator_ traits из библиотеки STLSoft. В этой главе много трудного материала, немало горьких разочарований (моих) и, к сожалению, совсем чутьчуть веселья. Если вы не хотите погружаться в трясину адаптации типов и неотъемлемую от нее вонь под названием «нестандартные особенности стандартных библиотек», можете пропустить эту главу и позже вернуться к ней, как к справочнику.
41.2. Класс stlsoft::adapted_iterator_traits Все еще не ушли? Хорошо. Я не стану приводить мотивирующие примеры, поскольку их хватает и в последующих главах, а просто представлю характерис тический класс в том виде, в котором он определен в текущей версии. Однако сна чала я покажу упрощенный вариант, в котором не используются нестандартные особенности стандартных библиотек, чтобы вы лучше прониклись смыслом и на значением этого класса. Все действительно уродливые детали рассматриваются в дополнительном материале, который записан на компактдиске. (Попробуйте
584
Итераторы
произнести такую скороговорку: «Taking a stand on standardizing nonstandard standard libraries stands to leave you in misunderstanding».)1 Первое, что следует отметить, – это полное отсутствие в классе исполняемых функций. Хотя класс довольно длинный, он целиком посвящен выведению типа шаблона на этапе компиляции. Это во всех смыслах чистое метапрограммирование. В главе 13 мы рассматривали механизм выводимой адаптации интерфейса (IIA) и три его составляющие: распознавание типа, исправление типа и выбор типа. IIA – это основная техника, с помощью которой характеристический класс адаптированного итератор достигает своей цели. В листинге 41.1 приведена до предела упрощенная форма шаблона класса. Листинг 41.1. Набросок определения класса adapted_iterator_traits // Â ïðîñòðàíñòâå èìåí stlsoft template struct adapted_iterator_traits { typedef ???? iterator_category; typedef ???? value_type; typedef ???? difference_type; typedef ???? pointer; typedef ???? reference; typedef ???? const_pointer; typedef ???? const_reference; typedef ???? effective_reference; typedef ???? effective_const_reference; typedef ???? effective_pointer; typedef ???? effective_const_pointer; };
Адаптер итератора специализирует шаблон adapted_iterator_traits в тер минах типа своего базового итератора и пользуется типамичленами последнего для определения собственных типовчленов. Первые пять – обычные типычле ны, который всегда ассоциируются с характеристиками итераторов. С типами effective_reference и effective_const_reference мы уже встречались при рассмотрении шаблонного класса адаптера transform_iterator в разделе 36.4. Назначение четырех оставшихся скоро прояснится. Точно так же, как std::iterator_traits, шаблон adapted_iterator_traits частично специализируется для указателей (с различными cvквалификаторами). Листинг 41.2. Частичные специализации шаблона adapted_iterator_traits для указательных типов template struct adapted_iterator_traits { 1 Конечно, скороговорка на русский язык непереводима, но смысл ее такой: «Попытка стандартизовать нестандартные особенности стандартных библиотек оставит вас в недоуме нии». (Прим. перев.)
Характеристики адаптированных итераторов typedef typedef typedef typedef typedef typedef typedef typedef typedef typedef typedef
std::random_access_iterator_tag T ptrdiff_t value_type* value_type const* value_type& value_type const& reference const_reference pointer const_pointer
585
iterator_category; value_type; difference_type; pointer; const_pointer; reference; const_reference; effective_reference; effective_const_reference; effective_pointer; effective_const_pointer;
}; template struct adapted_iterator_traits { . . . // Âñå îñòàëüíûå ÷ëåíû, êàê â ñïåöèàëèçàöèè äëÿ typedef value_type const* pointer; typedef value_type const* const_pointer; typedef value_type const& reference; typedef value_type const& const_reference; }; template struct adapted_iterator_traits { . . . // Âñå îñòàëüíûå ÷ëåíû, êàê â ñïåöèàëèçàöèè äëÿ typedef value_type volatile* pointer; typedef value_type volatile const* const_pointer; typedef value_type volatile& reference; typedef value_type volatile const& const_reference; }; template struct adapted_iterator_traits { . . . // Âñå îñòàëüíûå ÷ëåíû, êàê â ñïåöèàëèçàöèè äëÿ typedef value_type volatile const* pointer; typedef value_type volatile const* const_pointer; typedef value_type volatile const& reference; typedef value_type volatile const& const_reference; };
Никаких сюрпризов здесь нет, хотя парочку моментов стоит отметить. Во первых, во всех вариантах тип effective_reference определен так же, как reference, а effective_const_reference – так же, как const_reference. Это объясняется тем, что указатели, будучи непрерывными итераторами (раздел 2.3.6), никогда не порождают ссылок из категорий временные по значению (раздел 3.3.5) или отсутствующие (раздел 3.3.6). (Если есть чтото, на что можно указать, то вряд ли это представимо только как временный экземпляр.) Правило. Непрерывные итераторы никогда не порождают ссылок из категорий времен) ные по значению или отсутствующие.
Вовторых, в constвариантах типы pointer и reference определены одина ково. Иными словами, pointer – это указатель на const, а reference – ссылка на
Итераторы
586
const. Сложной задачей является выведение в основном шаблоне разумных оп
ределений этих типов для итераторов, не являющихся указателями. Основной шаблон я покажу чуть ниже.
41.2.1. iterator_category Эта часть совсем проста. В любом типе итератора, не являющегося указате лем, должен быть определен типчлен iterator_category, поэтому первый эле мент выглядит так: template struct adapted_iterator_traits { typedef typename I::iterator_category . . .
iterator_category;
41.2.2. value_type Те же соображения. Тип, в котором отсутствует типчлен value_type, невоз можно разумно интерпретировать как итератор, поэтому: typedef typename I::value_type . . .
value_type;
41.2.3. difference_type Здесь уже начинаются осложнения. В некоторых старых библиотеках тип difference_type для итераторов не определен, а вместо него либо нет вообще ничего, либо есть тип distance_type. Связано это с тем, что библиотеки писались
в то время, когда стандарт C++98 только обретал очертания, поэтому критико вать их не за что. Но нам надо с этим чтото делать. Прежде всего, неплохо бы выяснить, есть ли в типе итератора типчлен difference_type. Для этого применяется механизм распознавания типа (раз дел 13.4.2): private: enum { HAS_MEMBER_DIFFERENCE_TYPE = has_difference_type::value }; . . .
Предвидя, что этот типчлен может отсутствовать, мы с помощью техники ис правления типа (раздел 13.4.3) определяем предполагаемый тип difference_ type, используя в качестве исправляющего тип с незамысловатым именем fixer_difference_type: private: enum { HAS_MEMBER_DIFFERENCE_TYPE = has_difference_type::value }; typedef typename fixer_difference_type< I , HAS_MEMBER_DIFFERENCE_TYPE >::difference_type putative_difference_type_; . . .
Характеристики адаптированных итераторов
587
Обратите внимание, что этот тип закрытый и имя заканчивается подчерком; тем самым мы подчеркиваем, что это внутренний вспомогательный тип и умень шаем шансы вступить в конфликт с другими способами метапрограммирования. Это мое личное соглашение, вы можете придумать другое представление. Просто старайтесь не выставлять на публичное обозрение типы, до которых никому не должно быть дела. Последний шаг в данном случае – выбрать тип, с помощью которого мы опреде лим difference_type, воспользовавшись механизмом выбора типа (раздел 13.4.1). private: enum { HAS_MEMBER_DIFFERENCE_TYPE = has_difference_type::value }; typedef typename fixer_difference_type< I , HAS_MEMBER_DIFFERENCE_TYPE >::difference_type putative_difference_type_; . . . public: typedef typename select_first_type_if< putative_difference_type_ , ptrdiff_t >::type difference_type; . . .
Вот и все! Если в фактическом типе итератора определен типчлен difference_ type, то он и будет использован; в противном случае в качестве разумного умол чанию мы возьмем тип ptrdiff_t. То, что мы только что видели, называется метапрограммным IFTHENELSE. Определение всех остальных типовчленов производится аналогично и может ока заться более сложным лишь потому, что в ветви IF часто бывает несколько элементов.
41.2.4. pointer Если исключить старые не соответствующие стандарту библиотеки, то в от сутствие у итератора типачлена pointer безопасно предположить только, что он поддерживает временные по значению ссылки и, значит, типчлен pointer дол жен быть определен как void. Поскольку вырожденный случай исправляющего типа – void, нам нужно выполнить только распознавание и исправление типа: private: enum { HAS_MEMBER_POINTER = has_pointer::value }; typedef typename fixer_pointer< I , HAS_MEMBER_POINTER >::pointer putative_pointer_; . . . public: typedef putative_pointer_ pointer; . . .
41.2.5. reference По причинам, слишком сложным даже для этой главы, компиляторы (за ис ключением некоторых с чрезмерно развитым чувством долга) не способны рас познать типычлены reference и const_reference. К счастью, насколько мне
588
Итераторы
известно, ни в одной реализации стандартной библиотеки (какой бы нестандарт ной она ни была) нет итераторов, для которых определен типчлен pointer, но не определен типчлен reference, или наоборот. Поэтому можно просто позаим ствовать значение HAS_MEMBER_POINTER и на его основе сделать ввод о наличии или отсутствии типа reference. private: enum { HAS_MEMBER_POINTER = has_pointer::value }; enum { HAS_MEMBER_REFERENCE = HAS_MEMBER_POINTER }; typedef typename fixer_reference< I , HAS_MEMBER_REFERENCE >:: reference putative_reference_; . . . public: typedef putative_reference_ reference; . . .
41.2.6. const_pointer и const_reference Вот тут придется повозиться. Мы хотим правильно определить типычлены const_pointer и const_reference для любого типа итератора. Если итератор
поддерживает временные по значению (или отсутствующие) ссылки на элементы, то эти типычлены должны быть определены как void. Но для других категорий ссылок их следует определять в терминах типа значения. Таким образом, нам нужно знать категорию ссылок на элементы, которую поддерживает итератор. В части II и III мы видели, что итераторы, поддерживающие две низших катего рии ссылок, можно отличить по тому, что типчлен pointer определен как void. В случае отсутствующих ссылок тип value_type тоже определен как void. Сле довательно, определить типычлены, обозначающие категорию ссылок, можно с помощью компонента is_same_type, устанавливающего эквивалентность ти пов (раздел 12.1.7): private: enum { REF_CAT_IS_VOID = is_same_type::value }; enum { REF_CAT_IS_BVT = !REF_CAT_IS_VOID && is_same_type<pointer, void>::value }; . . .
Зная это, мы можем теперь правильно определить типычлены const_pointer и const_reference, применив механизм выбора типа: public: typedef typename select_first_type_if::type const_pointer; typedef typename select_first_type_if::type const_reference; . . .
Характеристики адаптированных итераторов
589
Генератор типа add_const_ref служит для применения квалификатора const& к любому типу, кроме void, который, понятно, не модифицируется. Это обязательно, так как язык запрещает ссылки на void.
41.2.7 effective_reference и effective_const_reference В разделе 36.4.7, обсуждая типы значений, возвращаемых операторами разы менования и индексирования в классе transform_iterator, мы говорили, что типычлены effective_reference и effective_const_reference должны быть определены как void для итераторов, поддерживающих отсутствующие ссылки на элементы, как value_type и const value_type – для итераторов с вре менными по значению ссылками, и как value_type& и value_type const& – для прочих категорий ссылок. Поэтому в последний раз применим механизм выбора типов следующим образом: public: typedef typename select_first_type_if::type effective_reference; typedef typename select_first_type_if< typename add_const::type , const_reference , REF_CAT_IS_BVT >::type effective_const_reference; . . .
Отметим, что здесь нет ни различения, ни определения промежуточных ти пов. Нужно лишь проверить, не работаем ли мы с категорией временных по значе нию ссылок, поскольку тип значения для итераторов с отсутствующими ссылка ми – void. Единственное новшество – это использование шаблонагенератора типов add_const, который добавляет квалификатор const к любому типу, кото рым специализирован, кроме void (так как квалифицировать void с помощью const запрещено).
41.2.8. effective_pointer и effective_const_pointer И остаются только типычлены effective_pointer и effective_const_ pointer, которые используются для значений, возвращаемых операторами выбо ра члена. Их структура такая же, как для эффективных ссылок, только для итера торов с временными по значению ссылками оба определены как void. public: typedef typename select_first_type_if::type effective_pointer; typedef typename select_first_type_if::type
effective_const_pointer;
. . .
41.2.9. Использование характеристического класса Пользуясь классом adapted_iterator_traits, можно сравнительно просто на писать реализацию шаблонного адаптера итератора, как показано в листинге 41.3. Листинг 41.3. Пример реализации адаптера итератора с использованием класса adapted_iterator_traits template< typename I // Àäàïòèðóåìûé èòåðàòîð , typename T = adapted_iterator_traits > class some_kind_of_iterator_adaptor { public: // Òèïû-÷ëåíû typedef I base_iterator_type; typedef T traits_type; typedef some_kind_of_iterator_adaptor class_type; typedef typename traits_type::iterator_category iterator_category; // À òàêæå value_type, pointer, reference, difference_type, // const_pointer, const_reference, effective_reference, // effective_const_reference, effective_pointer è // effective_const_pointer. . . . public: // Ìåòîäû èòåðàöèè effective_reference operator *(); effective_const_reference operator *() const; effective_pointer operator ->(); effective_const_pointer operator ->() const; . . . public: // Ìåòîäû äîñòóïà ê ýëåìåíòàì effective_reference operator [](difference_type index); effective_const_reference operator [](difference_type index) const; . . .
Если пользователь попытается воспользоваться этим адаптером таким спосо бом, который не поддерживается типом базового итератора, то компилятор сооб щит, что функция не может вернуть void, так как в соответствии с характеристи ками типа базового итератора данный типчлен в классе adapted_iterator_ traits будет определен как void.
41.3. Резюме Цитировать себя самого, конечно, неприлично, но в данном случае я думаю, бу дет уместно привести выдержку из моей «философии неидеального программиста» (в предисловии к книге Imperfect C++), основанной на четырех принципах: 1. C++ – замечательный, но не идеальный язык. 2. Носите власяницу. 3. Сделайте компилятор своим слугой. 4. Никогда не сдавайтесь; решение есть всегда.
Характеристики адаптированных итераторов
591
То, что C++ не идеален, с очевидностью вытекает из того, какие умственные усилия нам пришлось приложить для достижения цели, поставленной перед ха рактеристическим классом. Кстати, самое время сформулировать эту цель еще раз: сделать так, чтобы адаптеры итераторов было легко писать, а работали они в соответствии с ожиданиями пользователя, каким бы ни был тип базового итера тора. Невозможно переоценить важность такого средства! Хотя в данный момент это, быть может, и не покажется вам очевидным, но заме чательность C++ прямо следует из того факта, что такой шаблонный класс можно создать и с пользой применить, что мы и продемонстрируем в оставшихся главах. Хотя сильные и слабые стороны есть у всех языков, не будет преувеличением ска зать, что немногие способны предложить такую же выразительную мощь, как С++. Тезис о ношении власяницы не связан с трудностью только что проделанного путешествия. Иллюстрируется он тем, что есть много мест, в которых мы были вынуждены налагать ограничения и сокращать функциональность, а также тем, что я упорно искал (и в большинстве случаев преуспел) способ обеспечить макси мальную мощь, учитывая особенности конкретных компиляторов и библиотек, с которыми они (обычно) работают. Мы безусловно превратили компилятор в своего слугу, приказав ему проде лать акробатические трюки с метапрограммированием. Честно сознаюсь, что мое любопытство не простирается в те края, где разрабатывают компиляторы, а из того, что я знаю со слов друзей, работающих по ту сторону, делаю вывод, что мне комфортнее «ничего не знать», а просто и дальше отдавать компиляторам распо ряжения. Последний принцип, на мой взгляд, самый главный. Как часто, найдя нужную библиотеку, вы обнаруживали, что ваш компилятор «устарел и не поддерживает ся». (Честно говоря, готовность, с которой иногда бросают эту удобную, но дест руктивную фразу, кажется мне признаком откровенного высокомерия. Осмелюсь предложить вам сторониться таких «передовых» библиотек так же, как рукопожа тия политика.) Принимая во внимание, что C++ сложен и продолжает развивать ся, просто неразумно предполагать, что весь мир C++ будет правильными рядами маршировать от одного стандарта к другому. Кроме того, поскольку количество компаний, разрабатывающих компиляторы C++, неуклонно сокращается, необ ходимо удвоить бдительность, не допуская контроля со стороны какойто одной коммерческой организации. Поэтому приемы обеспечения максимальной обрат ной совместимости попрежнему важны. И я надеюсь, что сумел убедить вас в том, что решение почти всегда существует. Не сдавайтесь!
41.4. На компакт"диске На компактдиске имеется дополнительный раздел «Старые библиотеки с но выми компиляторами», где описываются еще несколько препятствий, которые пришлось преодолеть, чтобы заставить характеристический класс adapted_ iterator_traits работать с новым компилятором (например, Intel 8) и старой стандартной библиотекой (например, поставляемой вместе с Visual C++ 6).
Глава 42. Фильтрующая итерация Если вы думаете, что добились успеха, будь% те готовы к тому, что вам укажут на дверь. – Стив Форбс Я могу дать слово, но я%то знаю, чего оно стоит, а вы – нет. – Ниро Вульф
42.1. Введение В главе 36 мы видели, что трансформация итератора сводится к применению унарной функции к результату разыменования. Можем ли мы использовать в ана логичной ситуации предикат, чтобы отфильтровать некоторые элементы? Ска жем, написать нечто подобное: using recls::stl::search_sequence; search_sequence files(".", "*", recls::FILES | recls::RECURSIVE); std::copy(filter(files.begin(), is_readonly()) , filter(files.end(), is_readonly()) , std::ostream_iterator<search_sequence::value_type>(std::cout , "\n"));
42.2. Неправильная версия Как это могло бы работать? Естественно, filter() будет порождающей функ цией, которая возвращает экземпляр (должным образом специализированного) типа фильтрующего итератора. Можно представить себе такой шаблонный класс итератора: Листинг 42.1. Неправильная версия filter_iterator template< typename I // Àäàïòèðóåìûé èòåðàòîð , typename P // Óíàðíûé ïðåäèêàò, îòáèðàþùèé ýëåìåíòû , typename T = adapted_iterator_traits > class filter_iterator { public: // Òèïû-÷ëåíû typedef I base_iterator_type; typedef P filter_predicate_type;
Фильтрующая итерация
593
typedef T traits_type; typedef filter_iterator class_type; typedef typename traits_type::iterator_category iterator_category; typedef typename traits_type::value_type value_type; . . . // È òàê äàëåå äëÿ îáû÷íûõ ÷ëåíîâ (èç adapted_iterator_traits) public: // Êîíñòðóèðîâàíèå filter_iterator(I it, P pr) : m_it(it) , m_pr(pr) { for(; !m_pr(*m_it); ++m_it) // Ïîëó÷èòü ïåðâóþ «îòîáðàííóþ» ïîçèöèþ {} } public: // Ìåòîäû îäíîíàïðàâëåííîé èòåðàöèè class_type& operator ++() { for(++m_it; !m_pr(*m_it); ++m_it) // Ñäâèíóòü è ïîëó÷èòü ñëåäóþùóþ // ïîçèöèþ {} return *this; } class_type operator ++(int); // Îáû÷íàÿ ðåàëèçàöèÿ reference operator *(); // Îáû÷íàÿ ðåàëèçàöèÿ const_reference operator *() const; // Îáû÷íàÿ ðåàëèçàöèÿ private: // Ïåðåìåííûå-÷ëåíû I m_it; P m_pr; };
К сожалению, предложение, которое должно выводить имена файлов, пред назначенных только для чтения, работать не будет и, скорее всего, приведет к ава рийному завершению программы. Проблемы две. Вопервых, при конструировании первого итератора (активного) использу ются предикат и оператор инкремента, чтобы установить экземпляр filter_ iterator в правильную позицию еще до использования. Правильной считается первая позиция, для которой предикат возвращает true, но она вполне может ока заться за пределами диапазона [files.begin(), files.end()). Вовторых, конструктор второго итератора (того, который адаптирует конце вой итератор), разыменовывает экземпляр своего базового итератора. Но концеп ция итератора в STL (раздел 1.3) подразумевает, что мы «никогда не должны предполагать возможность разыменования значений за концом диапазона» (C++03: 24.1;5). (Заодно это означает, что реализация operator *() некорректна, но этот вопрос представляет только академический интерес, так как для того, чтобы доб раться до места программы, где этот оператор вызывается, нам предстоит пройти через конструктор, поведение которого не определено.)
42.3. Итераторы"члены определяют диапазон Ясно, что экземпляр фильтрующего итератора должен иметь доступ к паре итераторов, чтобы избежать выхода за границы диапазона. А раз так, то клиент ский код получится более громоздким:
594
Итераторы
search_sequence files(".", "*", recls::FILES | recls::RECURSIVE); std::copy(filter(files.begin(), files.end(), is_readonly()) , filter(files.end(), files.end(), is_readonly()) , std::ostream_iterator<search_sequence::value_type>(std::cout , "\n"));
42.4. Ну и что будем делать. . . ? Можно было бы по умолчанию определить второй концевой итератор как I(): template class filter_iterator { . . . public: // Êîíñòðóèðîâàíèå filter_iterator(I it, P pr, I end = I()) : m_it(it) , m_end(end) , m_pr(pr) { . . . private: // Ïåðåìåííûå-÷ëåíû I m_it; I m_end; P m_pr; };
Однако тем самым мы предполагаем, что конструируемый по умолчанию ите ратор эквивалентен концевому. Для некоторых итераторов так оно и есть, напри мер, для readdir_sequence::const_iterator (раздел 19.3) и findfile_ sequence::const_iterator (раздел 20.5), но для других, например glob_ sequence::const_iterator (раздел 17.3), это условие не выполнено. Или, если вам угодно, это решение может сработать для std::list, std::deque и std:: map, но заведомо не годится для std::vector и, что самое важное, для указателей. Далее, при таком подходе пользователь должен знать, справедливо ли указан ное предположение для конкретного итератора, а возлагать на него такую ответ ственность неразумно и чревато ошибками. Протекают наши абстракции! Добавь те еще тот факт, что такие ошибки могут не проявляться при тестировании, терпеливо дожидаясь промышленной эксплуатации, и вы согласитесь, что это ре шение неприемлемо. «Минуточку!», слышу я упрямый возглас. «Мы же можем специализировать шаблон порождающей функции, так чтобы она отвергала указатели». Можем, ко нечно. Но существует немало итераторов, которые не являются указателями и все равно не удовлетворяют условию эквивалентности экземпляра, сконструирован ного по умолчанию, и концевого экземпляра. Например, итератор с произволь ным доступом, не являющийся указателем. «А что если специализировать так, чтобы отвергались и итераторы с произ вольным доступом?» Помогло бы, если бы не тот факт, что не подходят и итерато
Фильтрующая итерация
595
ры, принадлежащие другим категориям. Короче говоря, никуда не деться от сле дующего правила и совета. Правило. Никогда не предполагайте, что сконструированный по умолчанию экземпляр итератора эквивалентен концевому итератору для той последовательности или диапазо) на, в котором итератор действует.
Совет. Никогда не пользуйтесь фильтрующим адаптером итератора, который предпола) гает или разрешает пользователю предположить, что сконструированный по умолчанию экземпляр адаптируемого типа эквивалентен концевому итератору.
Приняв это к сведению, попробуем определить надежный фильтрующий ите ратор.
42.5. Класс stlsoft::filter_iterator В этом классе многое предстоит сделать, поэтому последовательно рассмот рим категории итераторов от простых к сложным. Начнем с итераторов ввода и однонаправленных итераторов.
42.5.1. Семантика однонаправленных итераторов В листинге 42.2 показано, как учесть семантику однонаправленных итераторов. Листинг 42.2. Определение класс filter_iterator, поддерживающего однонаправленную итерацию template< typename I // Áàçîâûé èòåðàòîð , typename P // Óíàðíûé ïðåäèêàò, îòáèðàþùèé ýëåìåíòû , typename T = adapted_iterator_traits > class filter_iterator { public: // Òèïû-÷ëåíû . . . // Âñå îáû÷íûå òèïû-÷ëåíû, áîëüøèíñòâî â òåðìèíàõ T (adapted_iterator_traits) public: // Êîíñòðóèðîâàíèå filter_iterator(I begin, I end, P pr) : m_it(begin) , m_end(end) , m_pr(pr) { for(; m_it != m_end; ++m_it) { if(m_pr(*m_it)) { break; } }
596
Итераторы
} public: // Ìåòîäû îäíîíàïðàâëåííîé èòåðàöèè class_type& operator ++() { STLSOFT_MESSAGE_ASSERT( "Ïîïûòêà èíêðåìåíòà êîíöåâîãî èòåðàòîðà", m_it != m_end); for(++m_it; m_it != m_end; ++m_it) { if(m_pr(*m_it)) { break; } } return *this; } class_type& operator ++(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ effective_reference operator *() { return *m_it; } effective_const_reference operator *() const; // Òàê æå, êàê operator *() effective_pointer operator ->() { enum { is_iterator_pointer_type = is_pointer_type::value }; typedef typename value_to_yesno_type::type yesno_t; return invoke_member_selection_operator_(yesno_t()); } effective_const_pointer operator ->() const; // Òàê æå, êàê operator ->() . . . private: // Ïåðåìåííûå-÷ëåíû I m_it; I m_end; P m_pr; };
Все типычлены определены в терминах типов, предоставляемых классом adapted_iterator_traits (как и в случае адаптера index_iterator, описание
которого имеется на компактдиске). Конструктор принимает пару итераторов [begin, end), определяющую диапазон, и предикат фильтрации. Отметим, что предикат идет последним, как напоминание о том, что задавать концевой итера тор по умолчанию – безумие. Конструктор обязан предположить, что экземпляр базового итератора, опре деляющий начало диапазона, может быть отфильтрован предикатом. Он проверя ет этот факт и, если необходимо, инкрементирует итератор, пока не найдет подхо дящий элемент. Сравните с реализацией метода operator ++(), который точно знает, что текущий элемент не отфильтровывается, и инкрементирует итератор перед началом цикла. Это объясняется тем, что пользователь обязан предвари тельно сравнить его с концевым итератором, который заведомо отфильтровыва ется. Это согласуется с базовой идиомой в STL, гласящей, что пригодность итера
Фильтрующая итерация
597
тора определяется путем сравнения на равенство с итератором, который заведомо не пригоден. Необходимость смещаться в подходящую точку итерации приводит к доволь но любопытному соотношению, когда различные начальные итераторы после приведения к фильтрующей форме оказываются одинаковыми. Рассмотрим по следовательность целых чисел 0, 2, 4, 5, 6, 7, 8, 9. При использовании фильтра is_odd, который отбирает только нечетные числа, есть несколько способов задать эквивалентные итераторы: int ints[] = { 0, 2, 4, 5, 6, 7, 8, 9 }; stlsoft::filter(&ints[0], stlsoft::filter(&ints[1], stlsoft::filter(&ints[2], stlsoft::filter(&ints[3],
&ints[0] &ints[0] &ints[0] &ints[0]
+ + + +
8, 8, 8, 8,
is_odd()); is_odd()); is_odd()); is_odd());
// Ýêâèâàëåíòåí // òàêîìó // è òàêîìó // è òàêîìó òîæå
Каждый из этих итераторов на самом деле ссылается на элемент с индексом 3, который равен 5, так как это первое нечетное число в последовательности. В ос тавшейся части реализации нет ничего выдающегося, если помнить все сказанное в разделе 36.4.5 об операторе выбора члена. Пока все хорошо.
42.5.2. Семантика двунаправленных итераторов Надеюсь, что уже все поняли. Ограничившись только описанными выше пе ременнымичленами, мы не сможем реализовать семантику двунаправленного итератора изза опасности выхода за пределы диапазона, уже обсуждавшейся в разделе 42.2. Только на этот раз нам угрожает выход за начало, а не за конец. Решение состоит в том, чтобы запомнить начальную точку. Поэтому мы добавим еще одну переменнуючлен m_begin типа итератора и соответствующим образом подправим конструктор. Листинг 42.3. ПеременныеZчлены, необходимые для поддержки двунаправленного итератора class filter_iterator { . . . public: // Êîíñòðóèðîâàíèå filter_iterator(I begin, I end, P pr) : m_it(begin) , m_begin(begin) , m_end(end) , m_pr(pr) { . . . } . . . private: // Ïåðåìåííûå-÷ëåíû I m_it; I m_begin;
598
Итераторы
I m_end; P m_pr; };
После добавления этой переменной реализация методов двунаправленного итератора становится на удивление простой. Листинг 42.4. Операторы предекремента . . . public: // Ìåòîäû äâóíàïðàâëåííîãî èòåðàòîðà class_type& operator —() { STLSOFT_MESSAGE_ASSERT( "Ïîïûòêà äåêðåìåíòà íà÷àëüíîãî èòåðàòîð", m_it != m_begin); for(—m_it; m_it != m_begin; —m_it) { if(m_pr(*m_it)) { break; } } return *this; } class_type& operator —(int); // Êàíîíè÷åñêàÿ ðåàëèçàöèÿ . . .
Теперь мы можем обходить диапазон в любом направлении: template void fn(I from, I to) { ++it; —it; } struct is_odd; int ints[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; fn(stlsoft::filter(&ints[0], &ints[0] + 8, is_odd())); // Âñå êîððåêòíî
42.5.3. Семантика итераторов с произвольным доступом Тут вообще все просто. Нет никакого разумного обоснования для реализации семантики произвольного доступа в фильтрующем итераторе, поэтому мы и не станем этого делать. Единственный мыслимый способ оказался бы безумно доро гим, так как каждая операция якобы произвольного доступа на самом деле пре вращалась бы в поэлементный обход в поисках нефильтруемых элементов. Но даже если бы не это, я все равно не могу представить ситуацию, когда произволь ный доступ с фильтрацией имел бы смысл. Конечно, я могу ошибаться, в таком случае напишите мне и разбейте в пух и прах мои ошибочные гипотезы.
Фильтрующая итерация
599
Рекомендация. Откажитесь от поддержки произвольного доступа в фильтрующих итера) торах.
42.6. Ограничение категории итераторов Решив не поддерживать семантику произвольного доступа, мы тут же получи ли очередную проблему. Если мы адаптируем итератор с произвольным досту пом, то и адаптированная форма будет считать, что обладает такими же возмож ностями, поскольку типчлен iterator_category определен как std::random_ access_iterator_tag, хотя в реальности мы наделили ее лишь умением вести себя, как двунаправленный итератор. Это нехорошо. Стоит передать такой итера тор алгоритму, у которого есть специализация для итераторов с произвольным доступом, и мы попадем в очень неприятную ситуацию. Вы увидите огромный список сообщений об ошибках и, если повезет, гдето в его недрах обнаружится упоминание о том, что отсутствует метод operator -() или operator +() или еще какаято операция, свойственная только итераторам с произвольным доступом. Почему мы не сталкивались с этой проблемой для других адаптеров? Да пото му, что и transform_iterator (глава 36), и member_selector_iterator (гла ва 38), и index_iterator (дополнительная глава на компактдиске) могут под держать ту же категорию, что и базовый итератор. А filter_iterator, по самой своей природе, не может. Таким образом, последняя хитрость заключается в использовании шаблона min_iterator_category и 16 его специализаций, каждая из которых соответ ствует комбинации двух стандартных категорий итераторов. Основной шаблон и некоторые его специализации приведены в листинге 42.5. В каждой комбинации типчлен iterator_category определен как наименее функциональная из двух категорий. Листинг 42.5. Основной шаблон min_iterator_category и некоторые его специализации template< typename C1 // Ïåðâàÿ êàòåãîðèÿ , typename C2 // Âòîðàÿ êàòåãîðèÿ > struct min_iterator_category; template struct min_iterator_category< std::input_iterator_tag , std::input_iterator_tag > { typedef std::input_iterator_tag iterator_category; }; template struct min_iterator_category< std::forward_iterator_tag , std::input_iterator_tag >
600
Итераторы
{ typedef std::input_iterator_tag iterator_category; }; . . . template struct min_iterator_category< std::bidirectional_iterator_tag , std::random_access_iterator_tag > { typedef std::bidirectional_iterator_tag iterator_category; }; template struct min_iterator_category< std::random_access_iterator_tag , std::random_access_iterator_tag > { typedef std::random_access_iterator_tag iterator_category; };
Этот характеристический класс служит для того, чтобы ограничить типчлен iterator_category максимально возможной из поддерживаемых категорий ите
раторов (листинг 42.6). Листинг 42.6. Определение класса filter_iterator template< typename I // Áàçîâûé èòåðàòîð , typename P // Óíàðíûé ïðåäèêàò, îòáèðàþùèé ýëåìåíòû , typename T = adapted_iterator_traits > class filter_iterator { public: // Òèïû-÷ëåíû . . . typedef filter_iterator class_type; typedef typename min_iterator_category< typename traits_type::iterator_category , std::bidirectional_iterator_tag >::iterator_category iterator_category; . . .
42.7. Резюме Мы видели, что для создания фильтрующего адаптера итератора необходимо задавать пару итераторов, определяющих диапазон. Это немного неудобно поль зователю, но другого решения не существует. Мы видели также, что за счет харак теристического класса adapted_iterator_traits можно получить простое оп ределение достаточно сложной адаптации итератора.
42.8. На компакт"диске На компактдиске имеются предварительные соображения по поводу того, как можно проще реализовать фильтрацию с помощью понятия диапазонов, но подробно эта тема будет рассматриваться во втором томе.
Глава 43. Составные адаптеры итераторов Покажите мне блок%схемы, не показывая таб% лиц, и я останусь в заблуждении. Покажите мне ваши таблицы, и блок%схемы, скорей все% го, не понадобятся: они будут очевидны. – Фредерик П. Брукс Но если меньше – это больше, то насколько же больше должно быть больше! – Др Фразье Крейн, сериал «Фразье»
43.1. Введение Вы, наверное, уже размышляете над тем, можно ли использовать несколько адаптаций итераторов совместно, и, если да, то как. У меня для вас хорошая но вость – можно. И совсем отличная – вам даже не придется для этого писать новый адаптер итератора!
43.2. Трансформация фильтрующего итератора Допустим, что имеется множество целых чисел (int), и мы хотим отфильтро вать нечетные, а те, что останутся, преобразовать к типу double и извлечь из них квадратные корни. Предположим, что есть два функторных класса is_even() и sqroot(): struct is_even : std::unary_function { bool operator ()(int i) const { return 0 == (i % 2) /* && 0 != i */; } }; struct sqroot : std::unary_function {
602
Итераторы
double operator ()(int i) const { return ::sqrt(static_cast<double>(i)); } };
С помощью порождающих функций transformer() (раздел 36.3.1) и filter() (глава 42) мы можем произвести фильтрацию и трансформацию в од ном предложении: std::list ints = . . . std::copy(stlsoft::transformer(stlsoft::filter(ints.begin(), ints.end() , is_even()) , sqroot()) , stlsoft::transformer(stlsoft::filter(ints.end(), ints.end() , is_even()) , sqroot()) , std::ostream_iterator<double>(std::cout, "\n"));
Эффективно, но чересчур громоздко, да еще имеется то же не слишком зло стное нарушение принципа DRY SPOT (глава 5), с которым мы встречались в раз деле 36.3. В общем, не очень красиво.
43.2.1. Порождающая функция Мы можем упростить это решение, по крайней мере синтаксически, опреде лив комбинированную порождающую функцию transform_filter(): template< typename I // Òèï àäàïòèðóåìîãî èòåðàòîðà , typename TF // Ôóíêöèÿ òðàíñôîðìàöèè , typename FP // Ôèëüòðóþùèé ïðåäèêàò > transform_iterator transform_filter(I from, I to, TF fn, FP pr) { return transformer(filter(from, to, pr), fn); }
Игра в названия (глава 37) становится еще увлекательнее, когда дело доходит до комбинированных адаптеров. (Возможно, это и есть самая сложная часть при написании таких функций!) Как назвать: filter_transformer(), transforming_ filter(), transformer_filter() или …? Я выбрал имя, наиболее благозвучное для человека, говорящего поанглийски, – transform_filter(). Включив эту функцию в пример, мы немного поправим дело, как видно из следующего фраг мента. (Разумеется, есть и предсказуемое эквивалентное длинное имя: make_ transform_filter_iterator()). std::copy(stlsoft::transform_filter(ints.begin(), ints.end(), sqroot() , is_even()) , stlsoft::transform_filter(ints.end(), ints.end(), sqroot() , is_even()) , std::ostream_iterator<double>(std::cout, "\n"));
Составные адаптеры итераторов
603
Обратите внимание, что функция трансформации предшествует фильтрую щему предикату, и точно так же расположены части имени порождающей функ ции. Это ни в коем случае не означает, что именно такое соглашение лучше любо го другого. С равным успехом можно считать, что имена объектовфункций должны следовать в том порядке, в котором применяются при адаптации. Я добавил небольшое ограничение на этапе компиляции и соответствующий комментарий в реализации transform_filter(), чтобы отловить ошибки в по рядке задания аргументов объектовфункций: template transform_iterator transform_filter(I from, I to, TF fn, FP pr) { typedef typename FP::result_type pred_res_t; // Åñëè ýòî óòâåðæäåíèå ñðàáîòàåò, çíà÷èò, ëèáî âû çàäàëè òðàíñôîðìèðóþùóþ // ôóíêöèþ è ôèëüòðóþùèé ïðåäèêàò íå â òîì ïîðÿäêå, ëèáî ïðåäèêàò // âîçâðàùàåò çíà÷åíèå íå-èíòåãðàëüíîãî òèïà (÷òî âåñüìà ñòðàííî). STLSOFT_STATIC_ASSERT(0 != is_integral_type<pred_res_t>::value); return transformer(filter(from, to, pr), fn); }
43.3. Фильтрация трансформирующего итератора Возникает очевидный вопрос – можно ли подвергнуть фильтрации результат работы трансформирующего итератора? Разумеется, можно. Достаточно поме нять местами адаптеры в порождающей функции: template< typename I // Òèï àäàïòèðóåìîãî èòåðàòîðà , typename TF // Ôóíêöèÿ òðàíñôîðìàöèè , typename FP // Ôèëüòðóþùèé ïðåäèêàò > filter_iterator filter_transformer(I from, I to, FP pr, TF fn) { typedef typename FP::result_type pred_res_t; // Åñëè ýòî óòâåðæäåíèå ñðàáîòàåò, çíà÷èò, ëèáî âû çàäàëè òðàíñôîðìèðóþùóþ // ôóíêöèþ è ôèëüòðóþùèé ïðåäèêàò íå â òîì ïîðÿäêå, ëèáî ïðåäèêàò // âîçâðàùàåò çíà÷åíèå íå-èíòåãðàëüíîãî òèïà (÷òî âåñüìà ñòðàííî). STLSOFT_STATIC_ASSERT(0 != is_integral_type<pred_res_t>::value); filter(transformer(from, fn), transformer(to, fn), pr); }
Помимо типа возвращаемого значения, единственное существенное отличие состоит в том, что порождающая функция transformer() вызывается дважды: по разу для аргументов from и to, чтобы у адаптера filter_iterator было два необ ходимых ему итератора (глава 42). Менее важный вопрос связан с непоследовательностью в порядке задания па раметровв шаблона. Когда я реализовывал эту функцию, то естественно скопиро вал transform_filter(). Поэтому в обеих функциях параметры задаются в по
604
Итераторы
рядке I, TF, FP, хотя во втором случае следовало бы задавать их в порядке I, FP, TF. Но я придираюсь в стремлении к перфекционизму, на самом деле это совершенно несущественно. Компилятору в подобных случаях абсолютно наплевать на поря док параметров шаблона, поскольку типы параметров однозначно выводятся из аргументов функции. И это хорошо.
43.4. И нашим, и вашим Эти функции находятся в файлах <stlsoft/iterators/transform_filter_ iterator.hpp> и <stlsoft/iterators/filter_transform_iterator.hpp> со ответственно. Имена файлов говорят пользователю о том, что в них определены обычные шаблонные классы transform_filter_iterator и filter_transform_ iterator, экземпляры которых возвращают порождающие функции. Я сделал это намеренно по очень простой причине. Компиляторы поразному проявляют себя, когда дело доходит до оптимизации шаблонов, а уж адаптированных шабло нов в особенности. Следуя принятым в STLSoft соглашениям об именовании файлов и определив порождающие функции с именами, которые наводят на мысль о существовании одного составного адаптера итератора, мы оставляем себе возможность в будущем изменить реализацию, не затрагивая уже написанный клиентский код.
43.5. Резюме В этой главе показано, как можно сравнительно просто объединить нетриви альные адаптеры итераторов, определив композицию порождающих функции. Это пример выразительной мощи расширений STL, сочетающейся со значитель ной экономией усилий.
Эпилог Плохие времена. Дети больше не слушают родителей, и все пишут книги. – Цицерон Я не имею ни малейшего представления, о чем буду писать, пока не сяду за машинку. – Лари МакМэртри Надеюсь, вы получили удовольствие от первой части нашего путешествия в мир расширений STL. Ято получил. Ну, почти – ведь на эту книгу, как и на предыду щую, ушло примерно вчетверо больше времени, чем я рассчитывал. И всетаки я настолько глуп, что попробую еще, да не один, а целых два раза! Так что если моя любимая жена не задушит меня (по заслугам) за то, что я снова рискую впасть в нищету, то одно из следующих предзнаменований исполнится.
Мэтью Уилсон возвращается с книгой Breaking Up the Monolith в которой концепция прокладок, паттерны Type Tunneling и Handle::Ref+Wrapper, а также принципы пересекающегося соответствия и неисправимости объединят уси лия для установления баланса между сцепленностью, выразительностью, гибкостью, производительностью и связанностью.
Или:
Мэтью Уилсон возвращается с книгой Extended STL, Volume 2 в которой мы продолжим путешествие в мир расширений STL, посетив островки, где обитают объектыфункции, алгоритмы, адаптеры, распределители, и многие соседние земли.
TTFN, ждите.
Библиография Я твердо верю, что самообразование – един% ственный вид образования. – Айзек Азимов Остерегайтесь людей, имеющих всего одну книгу. – Билли Конноли При работе над книгой «Расширение STL, том 1» использовались следующие ма териалы.
Публикации по STL Effective STL, Scott Meyers (Boston, MA: AddisonWesley, 2001) Мэтью Г. Остерн Обобщенное программирование и STL (Невский диалект, 2004) STL Tutorial and Reference Guide, Second Edition, David R. Musser, Gillmer J. Derge, and Atul Saini (Boston, MA: AddisonWesley, 2001) The C++ Standard, Second Edition, ISO/EIC 14882 (New York: American National Standards Institute, 2003)
Книги на другие темы Advanced Programming in the UNIX Environment, W. Richard Stevens (Reading, MA: AddisonWesley, 1993) Advanced Windows Programming, Third Edition, Jeffrey Richter (Redmond, WA: Microsoft Press, 1997) Эрик С. Реймонд Искусство программирования для UNIX (Вильямс, 2005) Beyond the C++ Standard Library: An Introduction to Boost, Bjorn Karlsson (Boston, MA: AddisonWesley, 2005) The Cathedral and the Bazaar, Eric Raymond (Sebastopol, CA: O’Reilly, 2001) Стивен Дьюхерст C++. Священные знания (Символплюс, 2007) Бьярн Страуструп Язык программирования C++, 3е издание, (Бином, Не вский диалект, 2006) Бьярн Страуструп Дизайн и эволюция C++ (ДМКПресс, Питер, 2006)
Библиография
607
Don’t Make Me Think, Steve Krug (Indianapolis, IN: New Riders, 2000) Скотт Мейерс Эффективное использование C++, 3е издание, (ДМК Пресс, 2006) Роберт Гласс Факты и заблуждения профессионального программирования, (Символплюс, 2007) Мэтью Уилсон C++. Практический подход к решению проблем программи% рования (Кудицобраз, 2006) Джоэл Спольски Джоэл о программировании (Символплюс, 2006) Скотт Мейерс Наиболее эффективное использование C++ (ДМК, 2000) Бертран Мейер Объектно%ориентированное конструирование программ% ных систем (Русская редакция, 2005) Э.Хант, Д.Томас Программист%прагматик (Лори, Питер Пресс, 2007) Small Things Considered, Henry Petroski (New York: Knopf, 2003) У.Р. Стивенс UNIX. Разработка сетевых приложений (Питер, 2003)
Книги издательства «ДМК Пресс» можно заказать в торговоиздательском холдинге «АЛЬЯНСКНИГА» наложенным платежом, выслав открытку или письмо по почтовому адресу: 123242, Москва, а/я 20 или по электронному адресу: orders@alianskniga.ru. При оформлении заказа следует указать адрес (полностью), по которому должны быть высланы книги; фамилию, имя и отчество получателя. Желательно также указать свой телефон и электронный адрес. Эти книги вы можете заказать и в Internetмагазине: www.alianskniga.ru. Оптовые закупки: тел. (495) 2589194, 2589195; электронный адрес books@alians kniga.ru.
Мэтью Уилсон
Расширение библиотеки STL для С++ Наборы и итераторы Главный редактор
Мовчан Д. А.
dm@dmk)press.ru
Перевод Верстка Дизайн обложки
Слинкин А. А. Чаннова А. А. Мовчан А. Г.
Совместный проект издательства «ДМК Пресс» и издательства «БХВПетербург» Подписано в печать 18.04.2008. Формат 70×100 1/16 . Гарнитура «Петербург». Печать офсетная. Усл. печ. л. 49,02. Тираж 1000 экз. № Издательство ДМК Пресс Webсайт издательства: www.dmkpress.ru Internetмагазин: www.alianskniga.ru Санитарно эпидемиологическое заключение на продукцию № 77.99.60.953.Д.002108.02.07 от 28.02.2007 г. выдано Федеральной службой по надзору в сфере защиты прав потребителей и благополучия человека. Отпечатано с готовых диапозитивов в ГУП «Типография «Наука» 199034, Санкт Петербург, 9 линия, 12