Text
                    C++ Templates
The Complete Guide
David Vandevoorde
Nicolai M. Josuttis
▲
TT
ADDISON-WESLEY
Boston • San Francisco • New York • Toronto • Montreal
London • Munich • Paris • Madrid • Capetown • Sydney
Tokyo • Singapore • Mexico City


Шаблоны C++ Справочник разработчика Дэвид Вандевурд Николаи М. Джосаттис Москва • Санкт-Петербург • Киев 2003
Оцифровка: ББК 32.973.26-018.2.75 Дмитрий NightWind Шестеркин В17 dfb@yandex.ru УДК 681.3.07 Издательский дом "Вильяме" Зав. редакцией С.#. Тригуб Перевод с английского В.И. Кочешкова, канд. техн. наук И.В. Красикова, Л.И.Мезенко, А. Наумовца, В.В. Новикова, В.Н. Романова, Под редакцией канд. техн. наук И.В. Красикова По общим вопросам обращайтесь в Издательский дом "Вильяме" по адресу: info@williamspublishing.com, http://www.williamspublishing.com Вандевурд, Дэвид, Джосаттис, Николаи, М. В17 Шаблоны C++: справочник разработчика. : Пер. с англ. — М. : Издательский дом "Вильяме", 2003. — 544 с.: ил. — Парал. тит. англ. ISBN 5-8459-0513-3 (рус.) Шаблоны C++ представляют собой активно развивающуюся часть языка программирования, предоставляющую программисту новые возможности быстрой разработки эффективных и надежных программ и повторного использования кода. Данная книга, написанная в соавторстве теоретиком C++ и программистом-практиком с большим опытом, удачно сочетает строгость изложения и полноту освещения темы с вопросами практического использования шаблонов. В книге содержится масса разнообразного материала, относящегося к программированию с использованием шаблонов, в том числе материал, который даст опытным программистам возможность преодолеть современные ограничения в этой области. Книга предполагает наличие у читателя достаточно глубоких знаний языка C++; тем не менее стиль изложения обеспечивает доступность материала как для квалифицированных специалистов, так и для программистов среднего уровня. ББК 32.973.26-018.2.75 Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм. Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства Addison-Wesley Publishing Company, Inc. Authorized translation from the English language edition published by Addison-Wesley Publishing Company, Inc., Copyright © 2003 by Pearson Education, Inc. All rights reserved. No part of this book may be reproduced, stored in retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise without either the prior written permission о the Publisher. Russian language edition published by Williams Publishing House according to the Agreement with R&I Enterprises International, Copyright © 2003 ISBN 5-8459-0513-3 (рус.) ISBN 0-201-73484-2 (англ.) © Издательский дом "Вильяме", 2003 © Pearson Education, Inc., 2003
Оглавление Предисловие 17 Благодарности 18 Глава 1. Об этой книге 21 Часть I. Основы 29 Глава 2. Шаблоны функций 31 Глава 3. Шаблоны классов 43 Глава 4. Параметры шаблонов, не являющиеся типами 57 Глава 5. Основы работы с шаблонами 65 Глава 6. Применение шаблонов на практике 83 Глава 7. Основные термины в области шаблонов 111 Часть II. Углубленное изучение шаблонов 117 Глава 8. Вглубь шаблонов 119 Глава 9. Имена в шаблонах 143 Глава 10. Инстанцирование 165 Глава 11. Вывод аргументов шаблонов 193 Глава 12. Специализация и перегрузка 205 Глава 13. Направления дальнейшего развития 231 Часть III. Шаблоны и конструирование 255 Глава 14. Полиморфные возможности шаблонов 257 Глава 15. Классы свойств и стратегий 273 Глава 16. Шаблоны и наследование 311 Глава 17. Метапрограммы 325 Глава 18. Шаблоны выражений 347
6 Оглавление Часть IV. Нетрадиционное использование шаблонов 369 Глава 19. Классификация типов 371 Глава 20. Интеллектуальные указатели 387 Глава 21. Кортежи 417 Глава 22. Объекты-функции и обратные вызовы 437 Приложение А. Правило одного определения 493 Приложение Б. Разрешение перегрузки / 505 Библиография 517 Глоссарий 521 Предметный указатель 532
Содержание Предисловие 17 Благодарности 18 Глава 1. Об этой книге 21 1.1. Что необходимо знать, приступая к чтению этой книги 22 1.2. Структура книги в целом 22 1.3. Как читать эту книгу 23 1.4. Некоторые замечания о стиле программирования 24 1.5. Стандарт и практика 26 1.6. Примеры кода и дополнительная информация 26 1.7. Обратная связь с авторами 26 Часть I. Основы 29 Зачем нужны шаблоны 29 Глава 2. Шаблоны функций 31 2.1. Первое знакомство с шаблонами функций 31 2.1.1. Определение шаблона 31 2.1.2. Использование шаблонов 32 2.2. Вывод аргументов 34 2.3. Параметры шаблонов 35 2.4. Перегрузка шаблонов функций 37 2.5. Резюме 42 Глава 3. Шаблоны классов 43 3.1. Реализация шаблона класса Stack 43 3.1.1. Объявление шаблонов классов 44 3.1.2. Реализация функций-членов 45 3.2. Использование шаблона класса Stack 47 3.3. Специализации шаблонов класса 49 3.4. Частичная специализация 51 3.5. Аргументы шаблона, задаваемые по умолчанию 52 3.6. Резюме 54
8 Содержание Глава 4. Параметры шаблонов, не являющиеся типами 57 4.1. Параметры шаблонов классов, не являющиеся типами 57 4.2. Параметры шаблонов функций, не являющиеся типами 61 4.3. Ограничения на параметры шаблонов, не являющиеся типами 62 4.4. Резюме 63 Глава 5. Основы работы с шаблонами 65 5.1. Ключевое слово typename 65 5.2. Использование this-> 67 5.3. Шаблоны-члены классов 68 5.4. Шаблонные параметры шаблонов 72 5.5. Инициализация нулем / 78 5.6. Использование строковых литералов в качестве аргументов шаблонов функций 79 5.7. Резюме 82 Глава 6. Применение шаблонов на практике 83 6.1. Модель включения 83 6.1.1. Ошибки при компоновке 83 6.1.2. Шаблоны в заголовочных файлах 85 6.2. Явное инстанцирование 87 6.2.1. Пример явного инстанцирования шаблона 87 6.2.2. Сочетание модели включения и явного инстанцирования 88 6.3. Модель разделения 89 6.3.1. Ключевое слово export 90 6.3.2. Ограничения модели разделения 92 6.3.3. Составление программы для модели разделения 93 6.4. Шаблоны и inline 94 6.5. Предварительно откомпилированные заголовочные файлы 95 6.6. Отладка шаблонов 98 6.6.1. Дешифровка ошибок-романов 98 6.6.2. Мелкое инстанцирование 100 6.6.3. Длинные имена 103 6.6.4. Трассировщики 103 6.6.5. Интеллектуальные трассировщики 107 6.6.6. Прототипы 108 6.7. Заключение 108 6.8. Резюме 109 Глава 7. Основные термины в области шаблонов 111 7.1. "Шаблон класса" или "шаблонный класс" 111 7.2. Инстанцирование и специализация 112 7.3. Объявления и определения 113
Содержание 9 7.4. Правило одного определения 114 7.5. Аргументы и параметры шаблонов 114 Часть II. Углубленное изучение шаблонов 117 Глава 8. Вглубь шаблонов 119 8.1. Параметризованные объявления 119 8.1.1. Виртуальные функции-члены 122 8.1.2. Связывание шаблонов 122 8.1.3; Первичные шаблоны 124 8.2. Параметры шаблонов 124 8.2.1. Параметры типа 125 8.2.2. Параметры, не являющиеся типами 125 8.2.3. Шаблонные параметры шаблона 126 8.2.4. Аргументы шаблона, задаваемые по умолчанию 127 8.3. Аргументы шаблонов 128 8.3.1. Аргументы шаблонов функций 129 8.3.2. Аргументы типов 132 8.3.3. Аргументы, не являющиеся типами 133 8.3.4. Шаблонные аргументы шаблонов 135 8.3.5. Эквивалентность 137 8.4. Друзья 138 8.4.1. Дружественные функции 138 8.4.2. Дружественные шаблоны 141 8.5. Заключение 142 Глава 9. Имена в шаблонах 143 9.1. Систематизация имен 143 9.2. Поиск имен 145 9.2.1. Поиск, зависящий от аргументов 147 9.2.2. Внесение дружественных имен 149 9.2.3. Внесение имен классов 150 9.3. Синтаксический анализ шаблонов 151 9.3.1. Зависимость от контекста в нешаблонных конструкциях 152 9.3.2. Зависимые имена типов 154 9.3.3. Зависимые имена шаблонов 156 9.3.4. Зависимые имена в объявлениях using 158 9.3.5. ADL и явные аргументы шаблонов 159 9.4. Наследование и шаблоны классов 160 9.4.1. Независимые базовые классы 160 9.4.2. Зависимые базовые классы 161 9.5. Заключение 164
10 Содержание Глава 10. Инстанцирование 165 10.1. Инстанцирование по требованию 165 10.2. Отложенное инстанцирование 167 10.3. Модель инстанцирования C++ 170 10.3.1. Двухфазный поиск 170 10.3.2. Точки инстанцирования 171 10.3.3. Модели включения и разделения ' 174 10.3.4. Поиск в единицах трансляции 175 10.3.5. Примеры X 176 1Q.4. Схемы реализации 178 10.4.1. "Жадное" инстанцирование 180 10.4.2. Инстанцирование по запросу 181 10.4.3. Итеративное инстанцирование 183 10.5. Явное инстанцирование 186 10.6. Заключение 190 Глава 11. Вывод аргументов шаблонов 193 11.1. Процесс вывода 193 11.2. Выводимый контекст 196 11.3. Особые ситуации вывода 198 11.4. Допустимые преобразования аргументов 199 11.5. Параметры шаблона класса 199 11.6. Аргументы функции по умолчанию 200 11.7. Метод Бартона-Нэкмана 201 11.8. Заключение 203 Глава 12. Специализация и перегрузка 205 12.1. Когда обобщенный код не совсем хорош 205 12.1.1. Прозрачная настройка 206 12.1.2. Семантическая прозрачность 207 12.2. Перегрузка шаблонов функций 208 12.2.1. Сигнатуры 210 12.2.2. Частичное упорядочение перегруженных шаблонов функций 212 12.2.3. Правила формального упорядочения 213 12.2.4. Шаблоны и нешаблоны 215 12.3. Явная специализация * 215 12.3.1. Полная специализация шаблона класса 216 12.3.2. Полная специализация шаблона функции 220 12.3.3. Полная специализация члена 222 12.4. Частичная специализация шаблона класса 225 12.5. Заключение 229
Содержание 11 Глава 13. Направления дальнейшего развития 231 13.1. Коррекция угловых скобок 231 13.2. Менее строгие правила использования ключевого слова typename 232 13.3. Аргументы шаблонов функций по умолчанию 233 13.4. Строковые литералы и выражения с плавающей точкой в качестве аргументов шаблонов 235 13.5. Менее строгие правила соответствия для шаблонных параметров шаблона 237 13.6. typedef-шаблоны 238 13.7. Частичная специализация шаблонов функций 239 13.8. Оператор typeof 241 13.9. Именованные аргументы шаблонов 242 13.10. Статические свойства 243 13.11. Пользовательская диагностика инстанцирования 244 13.12. Перегруженные шаблоны классов 247 13.13. Параметры-списки 248 13.14. Управление размещением данных 250 13.15. Вывод на основе инициализатора 251 13.16. Функциональные выражения 252 13.17. Заключение 254 Часть III. Шаблоны и конструирование 255 Глава 14. Полиморфные возможности шаблонов 257 14.1. Динамический полиморфизм 257 14.2. Статический полиморфизм 260 14.3. Сравнение динамического и статического полиморфизма 263 14.4. Новые виды шаблонов проектирования 265 14.5. Обобщенное программирование 266 14.6. Заключение 269 Глава 15. Классы свойств и стратегий 273 15.1. Пример: суммирование последовательности 273 15.1.1. Фиксированные классы свойств 274 15.1.2. Свойства-значения 277 15.1.3. Параметризованные свойства 281 15.1.4. Стратегии и классы стратегий 283 15.1.5. Различие между свойствами и стратегиями 285 15.1.6. Шаблоны членов и шаблонные параметры шаблонов 287 15.1.7. Комбинирование нескольких стратегий и/или свойств 289 15.1.8. Накопление с обобщенными итераторами 289 15.2. Функции типа 290
12 Содержание i 15.2.1. Определение типа элемента 291 15.2.2. Определение типов классов 293 15.2.3. Ссылки и квалификаторы 295 15.2.4. Свойства продвижения 298 15.3. Свойства стратегий 301 15.3.1. Типы параметров только для чтения 302 15.3.2. Копирование, обмен и перемещение 305 15.4. Заключение 310 Глава 16. Шаблоны и наследование 311 16.1. Именованные аргументы шаблона 311 16.2. Оптимизация пустого базового класса 315 16.2.1. Принципы размещения 315 16.2.2. Члены как базовые классы 318 16.3. Модель необычного рекуррентного шаблона 320 16.4. Параметризованная виртуальность 323 16.5. Заключение 324 j Глава 17. Метапрограммы 325 17.1. Первый пример метапрограммы 325 i 17.2. Значения перечислимого типа и статические константы 327 ' 17.3. Второй пример: вычисление квадратного корня 329 17.4. Применение переменных индукции 333 17.5. Полнота вычислений 336 17.6. Рекурсивное инстанцирование и рекурсивные аргументы шаблона 337 17.7. Метапрограммы для развертывания циклов 338 17.8. Заключение 342 Глава 18. Шаблоны выражений 347 18.1. Временные объекты и раздельные циклы 348 18.2. Программирование выражений в аргументах шаблонов 353 18.2.1. Операнды шаблонов выражений 354 18.2.2. Тип Array 357 18.2.3. Операторы 359 18.2.4. Подведем итог 361 18.2.5. Присвоение шаблонов выражений 363 18.3. Производительность и ограничения шаблонов выражений 364 18.4. Заключение 365 Часть IV. Нетрадиционное использование шаблонов 369 Глава 19. Классификация типов 371 19.1. Определение фундаментальных типов 371
Содержание 13 19.2. Определение составных типов 373 19.3. Определение типов функций 376 19.4. Классификация перечислений с помощью разрешения перегрузки 380 19.5. Определение типов классов 382 19.6. Окончательное решение 383 19.7. Заключение 386 Глава 20. Интеллектуальные указатели 387 20.1. Holder и Trule 387 20.1.1. Защита от исключений 388 20.1.2. Holder 390 20.1.3. Holder в качестве члена класса 392 20.1.4. Захват ресурса есть инициализация 394 20.1.5. Ограничения Holder 394 20.1.6. Копирование Holder 396 20.1.7. Копирование Holder при вызовах функций 397 20.1.8. Trule 397 20.2. Счетчики ссылок 400 20.2.1. Где находится счетчик 401 20.2.2. Параллельный доступ к счетчику 402 20.2.3. Деструкция и освобождение памяти 403 20.2.4. Шаблон CountingPtr 404 20.2.5. Простой незахватывающий счетчик 407 20.2.6. Шаблон простого захватывающего счетчика 409 20.2.7. Константность 410 20.2.8. Неявные преобразования типов 411 20.2.9. Сравнения 414 20.3. Заключение 415 Глава 21. Кортежи 417 21.1. Класс Duo 417 21.2. Рекурсивное вложение объектов класса Duo 422 21.2.1. Количество полей 423 21.2.2. Типы полей 424 21.2.3. Значения полей 425 21.3. Создание класса Tuple 430 21.4. Заключение 435 Глава 22. Объекты-функции и обратные вызовы 437 22.1. Прямые, непрямые и встраиваемые вызовы 438 22.2. Указатели и ссылки на функции 441 22.3. Указатели на функции-члены 444 22.4. Функторы-классы 447
14 Содержание 22.4.1. Первый пример функторов-классов 447 22.4.2. Типы функторов-классов 448 22.5. Определение функторов 450 22.5.1. Функторы в роли аргументов типа шаблонов 450 22.5.2. Функторы в роли аргументовфункций 451 22.5.3. Сочетание параметров функции и параметров типа шаблона 452 22.5.4. Функторы в роли не являющихся типами аргументов шаблонов 453 22.5.5. Инкапсуляция указателей на функции 454 j 22.6. Самотестирование 457 22.6.1. Анализ типа функтора 457; 22.6.2. Доступ к типам параметров 458 22.6.3. Инкапсуляция указателей на функции 460 ' 22.7. Композиции объектов-функций 465 ; 22.7.1. Простая композиция 466 '• 22.7.2. Композиция разных типов 470 22.7.3. Функторы с несколькими параметрами 473 22.8. Связывание значений 476 22.8.1. Выбор параметров связывания * 477 22.8.2. Сигнатура связывания 479 22.8.3. Выбор аргументов 480 22.8.4. Вспомогательные функции 486 22.9. Операции с функторами: полная реализация 489 22.10. Заключение 491 Приложение А. Правило одного определения 493 А. 1. Единицы трансляции 493 А.2. Объявления и определения 494 А.З. Детали правила одного определения 495 А.3.1. Ограничения "одно на программу" 495 А.3.2. Ограничения "одно на единицу трансляции" 498 А.3.3. Ограничения эквивалентности единиц перекрестной трансляции 499 Приложение Б. Разрешение перегрузки 505 Б. 1. Когда используется разрешение перегрузки 506 Б.2. Упрощенное разрешение перегрузки 506 Б.2.1. Неявный аргумент для функций-членов 508 Б.2.2. Улучшение точного соответствия 510 Б.З. Детали перегрузки 511 Б.3.1. Предпочтение нешаблонных функций 511 Б.З.2. Последовательности преобразований 512 Б.3.3. Преобразования указателей 513 Б.3.4. Функторы и функции-суррогаты 514 Б. 3.5. Другие контексты перегрузки 515
Содержание 15 Библиография 517 Группы новостей 517 Книги и Web-узлы 517 Глоссарий 521 Предметный указатель 532
/'
Предисловие Концепция шаблонов в C++ имеет довольно почтенный возраст — свыше десятка лет. Еще в 1990 году шаблоны C++ были документированы в аннотированном справочнике по C++ (Annotated C++ Reference Manual — ARM) [15], но и до того они встречались в более специализированных работах. Однако и сегодня, спустя более десятилетия, ощущается нехватка литературы, посвященной фундаментальным концепциям и технологиям, обеспечивающим это мощное, сложное и богатое по своим возможностям средство C++. Потому наше желание (хотя, возможно, и несколько самонадеянное) взяться за решение данной проблемы и написать эту книгу о шаблонах вполне естественно. Однако побудительные причины и намерения у каждого из авторов данной книги были несколько различны. Дэвид, опытный разработчик компиляторов и член C++ Standard Committee Core Language Working Group (рабочая группа Комитета по базовым стандартам языка C++), больше склонялся к точному и подробному описанию всех возможностей шаблонов (а также связанных с ними проблем). А Нико, специализирующегося на "обычных" приложениях программиста и члена C++ Standard Committee Library Working Group (рабочая группа Комитета по стандартам библиотек C++), интересовали приемы работы с шаблонами исходя из того, как их может использовать рядовой программист и какие преимущества для него может обеспечить данное средство. Кроме того, обоих Авторов объединяло желание сделать эти знания доступными как для вас, наших читателей, так и для сообщества программистов на C++ в целом. Мы надеялись, что наша работа сможет положить конец недопониманию, путанице и пессимистическим прогнозам в этой области. Что же в результате мы представляем на суд читателей? В книге не только рассматриваются базовые концепции шаблонов, подкрепленные практическими примерами, но и подробно описываются грамотные приемы работы с ними. На пути от основных принципов шаблонов к "искусству программирования шаблонов" читателям предстоит открыть для себя (или вспомнить) такие технические приемы, как статический полиморфизм, классы стратегий, метапрограммирование и шаблоны выражений. Кроме того, книга поможет читателю более глубоко разобраться в стандартной библиотеке C++, поскольку почти весь ее код опирается на использование шаблонов. Работа над книгой принесла нам немало удовольствия, существенно пополнив к тому же наши собственные знания о шаблонах C++. Надеемся, что при изучении изложенного материала читатели получат такое же удовлетворение. Желаем приятного чтения!
Благодарности Эта книга вобрала в себя мысли, концепции, решения и примеры из множества различных источников, и теперь мы хотели бы выразить свою признательность всем, кто оказывал нам помощь и поддержку на протяжении последних нескольких лет. Прежде всего огромное спасибо нашим рецензентам, а также тем, кто высказывал свое мнение по самым первым вариантам рукописи. Без их участия нам не удалось бы довести книгу до такого уровня, какой она имеет сегодня. Нашими рецензентами были Кайл Блейни (Kyle Blaney), Томас Гшвинд (Thomas Gschwind), Деннис Менкл (Dennis Mancl), Патрик Мак-Киллен (Patrick McKillen), Ян Христиан ван Винкель (Jan Christian vanWinkel). Особая благодарность Дитмару Кюлю (Dietmar Kuhl), который тщательно прорецензировал и отредактировал всю книгу. Обратная связь, которую обеспечил Дит- мар, помогла значительно повысить уровень книги. Хотелось бы также выразить признательность всем людям и организациям, которые предоставили нам возможность протестировать вошедшие в книгу примеры на разных платформах с помощью различных компиляторов. Большое спасибо Edison Design Group за замечательный компилятор и его поддержку. Сотрудники этой группы помогали нам не только создавать эту книгу, но и приводить ее в соответствие со стандартами. Большее спасибо всем разработчикам свободно распространяемых компиляторов GNU и egcs (особая благодарность Джесону Меррилу (Jason Merril) за отзывчивость), а также компании Microsoft за бета-версию Visual C++ (здесь мы контактировали с Джонатаном Кейв- сом (Jonathan Caves), Гербом Саттером (Herb Sutter) и Джесоном Ширком (Jason Shirk)). То, что сегодня составляет "ноосферу C++", — плод коллективного творчества сетевого сообщества C++. Львиную долю этих знаний обеспечивают модерируемые конференции Usenet— сотр.lang.C++.moderated и сотр.std.C++. Поэтому особое спасибо активным модераторам этих групп, которые смогли сделать обсуждение полезным и конструктивным. Мы хотим также поблагодарить каждого из тех, кто не один год подряд выкраивал время для описания и объяснения своих идей, желая сделать их нашим общим достоянием. Трудно переоценить тот вклад, который внесли в работу над книгой сотрудники издательства Addison-Wesley. Мы выражаем особую признательность нашему редактору Дебби Лафферти (Debbie Lafferty) за ее деликатные "пинки", дельные советы и добросовестную упорную работу над книгой. Спасибо также другим сотрудникам издательства — Тайрелль Олбах (Tyrell Albaugh), Банни Эймс (Bunny Ames), Мелани Бак (Melanie Buck), Жаклин Дюсетт (Jacquelyn Doucette), Чанда Лири-Коту (Chanda Leary-Coutu), Кэтрин Охала (Catherine Ohala) и Марти Рабинович (Marty Rabinowitz). Искренне благодарим Марину Ланг (Marina Lang), которая способствовала изданию этой книги в Addison-
Благодарности 19 Wesley, а также Сюзан Винер (Susan Winer), выполнившую первое редактирование, которое помогло очертить контуры будущей книги. Благодарности Нико Прежде всего мне хотелось бы передать личную благодарность и бесчисленное количество поцелуев своей семье: Улли (Ulli), Лукасу (Lucas), Анике (Anica) и Фредерику (Frederic) — за их заботу, предупредительность и поддержку, которые так помогали мне во время работы над книгой. Кроме того, я хотел бы сказать спасибо Дэвиду. Его знания и опыт огромны, но терпение оказалось поистине безграничным (временами я задавал ему на редкость глупые вопросы). Работать с ним очень интересно. Благодарности Дэвида Тем, что мне удалось завершить работу над этой книгой, я обязан своей жене Карине (Karina). Я чрезвычайно благодарен ей за ту роль, которую она играет в моей жизни. Когда в твоем ежедневном расписании множество одинаково первоочередных дел, написание книги "в свободное время" быстро превращается в утопию. Именно Карина помогала мне справляться с этим расписанием, учила меня говорить "нет", чтобы выкроить время для работы и обеспечить постоянное продвижение вперед. А самое главное — она была потрясающей движущей силой этого проекта. Я каждый день благодарю Бога за ее дружбу и любовь. И еще: я очень рад, что мне пришлось работать с Нико. Его вклад в создание книги измеряется не только непосредственно написанным текстом. Именно опыт и дисциплинированность Нико позволили нам перейти от моих графоманских попыток к хорошо организованному изданию. "Мистер Шаблон" Джон Спайсер (John Spicer) и "мистер Перегрузка" Стив Адамчик (Steve Adamczyk) — замечательные друзья и коллеги. А также, по моему мнению, этот дуэт является последней инстанцией во всем, что касается основ языка C++. Именно они внесли ясность во многие сложные вопросы, освещенные в данной книге, и если вам случится найти ошибку в описании какого-то элемента языка C++, значит, по этому вопросу я не смог проконсультироваться с ними. И наконец, я хотел бы выразить признательность всем тем, кто в разной степени помогал нам в работе над этим проектом. Многие из них не внесли непосредственный вклад в этот проект, но их участие и поддержка послужили для него огромной движущей силой. Это прежде всего мои родители; их любовь и ободрение были для меня чрезвычайно важны. Источником вдохновения были для меня и мои друзья, которые постоянно интересовались, как продвигается работа над книгой. Спасибо вам всем: Майкл Бэкманн (Michael Beckmann), Бретт и Джули Бин (Brett and Julie Beene), Джарран Карр (Jarran Carr), Симон Чанг (Simon Chang), Xo и Сара Чо (Но and Sarah Cho), Кристоф Де Динечин
20 Благодарности (Christophe De Dinechin), Ева Дилман (Eva Deelman), Нейл Эберли (Neil Eberle), Сэссан Хазеги (Sassan Hazeghi), Викрам Кумар (Vikram Kumar), Джим и Линдсей Лонг (Jim and Lindsay Long), Р.Дж. Морган (R.J. Morgan), Майк Пуритано (Mike Puritano), Рагу Рага- вендра (Ragu Raghavendra), Джим и Фуонг Шарп (Jim and Phuong Sharp), Грег Вогн (Gregg Vaughn) и Джон Вигли (John Wieglley).
Глава 1 Об этой книге Несмотря на то что шаблоны входят в C++ уже добрый десяток лет (и практически столько же времени доступны в том или ином виде в различных компиляторах), их использование до сих пор сопряжено с непониманием, неправильным применением или противоречиями. В то же время значение шаблонов как мощного инструмента для создания эффективного, быстрого и гибкого программного обеспечения с каждым днем возрастает. Сегодня концепция шаблонов уже стала краеугольным камнем ряда новых парадигм программирования на C++. Авторам пришлось столкнуться с тем, что в большинстве существующих книг и статей трактовка теоретических положений и применения шаблонов C++ является в лучшем случае поверхностной. И даже в тех немногих публикациях, где дается квалифицированный обзор различных технологий программирования, основанных на шаблонах, описание их поддержки средствами языка оставляет желать лучшего. В результате как начинающие, так и опытные программисты на C++ при работе с шаблонами сталкиваются с трудностями, пытаясь понять, почему их код работает не так, как ожидалось. Именно эти соображения и послужили толчком к созданию данной книги. Однако оба автора пришли к этой идее независимо друг от друга, и их позиции несколько различаются. • Целью Дэвида было создание самодостаточного справочника, включающего подробное описание механизма языка шаблонов и основных современных приемов программирования на базе шаблонов. Самое важное, с точки зрения Дэвида, — полнота и точность изложения материала. • Нико ставил перед собой задачу написать книгу, которая помогла бы ему и другим программистам использовать шаблоны на практике. Его кредо можно было бы выразить так: интуитивно понятное изложение материала и акцент на практических аспектах применения шаблонов. В какой-то степени у нас получился альянс ученого и инженера: мы оба имеем дело с одной и той же областью знаний, но сферы наших интересов несколько различны (хотя, разумеется, и имеют много общего). Начало нашему сотрудничеству положило издательство Addison-Wesley, и сейчас перед вами результат этого сотрудничества. Нам хочется верить, что у нас получи-
22 Глава 1. Об этой книге лась основательная работа, сочетающая в себе тщательно проработанное учебное пособие по шаблонам и детальный справочник. В качестве учебного пособия она охватывает не только введение в элементы языка, но и развитие понимания методов конструирования, лежащих в основе практических решений. Точно так же эта книга является не только детальным справочником по синтаксису и семантике шаблонов C++, но и кратким руководством по идиомам и технологиям языка — как широко известным, так и малознакомым. 1.1. Что необходимо знать, приступая к чтению этой книги Чтобы получить максимальную пользу от работы с книгой, читатель должен быть знаком с C++. В данной книге дается детальное описание конкретного средства языка программирования, но не основ самого языка. Предполагается знакомство читателя с концепцией классов и наследования, а также умение писать программы на C++ с использованием таких компонентов, как потоки ввода-вывода и контейнеры из стандартной библиотеки C++. Кроме того, при необходимости в данной книге рассматриваются различные тонкие вопросы, которые могут не иметь прямого отношения к шаблонам. Таким образом обеспечивается доступность изложенного здесь материала как для квалифицированных специалистов, так и для программистов среднего уровня. Изложение материала базируется на языке C++, соответствующем стандарту 1998 года [31], с учетом уточнений, приведенных в первом списке технических опечаток Комитета по стандартизации C++ (C++ Standardization Committee) [32]. Если вы чувствуете, что ваши знания основ C++ несколько устарели или отстали от современного уровня и их необходимо освежить, рекомендуем обратиться к дополнительным источникам информации, в частности [17, 18, 33]. В этих книгах содержится отличное введение в современный язык программирования C++ и его стандартную библиотеку. Кроме того, можно использовать публикации, перечисленные в библиографии. 1.2. Структура книги в целом Цель данной книги — предоставить читателю базовые знания, необходимые для работы с шаблонами и использования в полной мере их преимуществ; кроме того, книга призвана обеспечить читателей информацией, которая даст опытным программистам возможность преодолеть современные ограничения в этой области. Исходя из этого, мы разбили материал книги на четыре части. • Часть I, "Основы". Здесь описаны основные концепции, положенные в основу шаблонов. Эта часть написана в стиле учебника.
1.3. Как читать эту книгу 23 • Часть II, "Углубленное изучение шаблонов". Здесь представлены детальные сведения о языке. Эта часть является неплохим справочником по конструкциям, связанным с шаблонами. • Часть III, "Шаблоны и конструирование". В этой части рассматриваются фундаментальные приемы конструирования, поддерживаемые шаблонами C++, простирающиеся от тривиальных идей до сложных идиом (возможно, нигде до этого не опубликованных). • Часть IV, "Нетрадиционное использование шаблонов". Основана на предшествующих ей двук частях и рассматривает различные распространенные применения шаблонов. Каждая из перечисленных частей книги состоит из нескольких глав. Книга также включает несколько приложений, которые охватывают материал, относящийся не только к шаблонам (например, вопросы перегрузки в C++). Главы, входящие в состав первой части книги, требуют последовательного изучения. Например, глава 3 основана на материале, рассмотренном в главе 2. Однако в других частях книги связь между главами выражена не столь явно. Например, можно сначала проработать главу, посвященную функциям (глава 22, "Объекты-функции и обратные вызовы"), а уже потом приступать к чтению главы, посвященной интеллектуальным указателям (глава 20, "Интеллектуальные указатели"). 1.3. Как читать эту книгу Если вы являетесь программистом на C++ и хотите получить общее представление о концепции шаблонов и поближе познакомиться с ней, то вам следует тщательно изучить часть I, "Основы". С этим материалом имеет смысл хотя бы бегло ознакомиться даже тем, кто с шаблонами уже "на ты", чтобы прочувствовать стиль и освоиться с используемой в книге терминологией. Эта часть также охватывает некоторые "материально-технические" аспекты, касающиеся организации исходного кода, содержащего шаблоны. В зависимости от того, какой метод изучения материала вы предпочитаете, можно либо основательно изучить детальную информацию о шаблонах из части I, либо познакомиться с приемами практического программирования в части III (обращаясь к части II, если возникнут какие-либо вопросы). Последнее представляется особенно целесообразным в случае, если вы приобрели эту книгу для конкретных практических целей. Часть IV во многом подобна части III, но акцент в ней делается не на приемах конструирования, а на понимании роли шаблонов в конкретных приложениях. Следовательно, прежде чем приступать к изучению части IV, имеет смысл сначала ознакомиться с материалом, изложенным в части III. Приложения содержат большое количество полезной информации, на которую сделано много ссылок в основной части книги. Кроме того, мы старались сделать их интересными и в качестве самостоятельного материала. Опыт подсказывает, что лучше всего новые знания усваиваются на примерах. Поэтому в книге вы найдете большое количество примеров. Иногда это всего лишь несколько
24 Глава 1. Об этой книге строк кода, иллюстрирующих теоретическое положение, иногда— полноценные программы, реализующие конкретное применение материала. В последнем случае примеры снабжены комментариями C++ с описанием пути к файлу, в котором содержится код программы. Все эти файлы можно найти на Web-узле данной книги по адресу: http://www.josuttis.com/tmplbook/. i I 1.4. Некоторые замечания о стиле программирования У каждого программиста на C++ свой стиль программирования, и авторы данной книги также не составляют исключения. Понятие стиля включает обычные вопросы: где помещать пробелы, разделители (скобки, фигурные скобки) и т.п. В целом мы старались придерживаться единого стиля, хотя иногда по ходу изложения приходилось делать исключения. Например, чтобы придать коду больше наглядности, в разделах руководства были широко использованы пробелы и осмысленные имена, в то время как при рассмотрении более сложных вопросов предпочтение отдавалось компактности. Хотелось бы обратить внимание читателя на то, что в данной книге применяется несколько необычный подход к записи объявлений типов, параметров и переменных. Очевидно, что при объявлении возможно использование нескольких стилей: void foo(const int &x) ; void foo(const int& x) ; void foo(int const &x) ; void foo(int const& x); Для обозначения целочисленной константы мы решили применять несколько непривычный порядок записи — int const вместо const int. Сделано это было по двум причинам. Во-первых, такой порядок обеспечивает более очевидный ответ на вопрос: "Что именно является константой?". "Что" — это всегда то, что находится перед модификатором const. Однако для выражения int* const bookmark; // Указатель не может изменяться/ // однако может изменяться // значение, на которое он указывает не существует эквивалентной формы, в которой модификатор const стоял бы перед оператором указателя *, хотя const int N = 100; эквивалентно int const N = 100; В этом примере константой является сам указатель, а не целочисленное значение, на которое он указывает.
1.4. Некоторые замечания о стиле программирования 25 Вторая причина связана с синтаксической подстановкой, часто встречающейся при работе с шаблонами. Рассмотрим два следующие определения типов : typedef char* CHARS; typedef CHARS const CPTR; // Константный указатель на char Если вместо CHARS буквально подставить его значение, то смысл второго объявления не изменится: typedef char* const CPTR; // Константный указатель на char Однако при обратном порядке записи, т.е. если const стоит перед именем определяемого типа, этот принцип неприменим. Рассмотрим вариант определений, альтернативный представленным выше определениям типа: typedef char* CHARS; typedef const CHARS CPTR; //Константный указатель на char В результате буквальной подстановки значения CHARS получается другой тип: typedef const char* CPTR; // Указатель на константу char Очевидно, что сказанное выше справедливо и для спецификатора volatile. Что касается расстановки пробелов, то мы решили помещать пробел между ампер- сандом и именем параметра: void foo(int const& x); Это сделано для того, чтобы подчеркнуть разделение между типом параметра и именем параметра. Однако при такой записи еще более запутанными становятся объявления наподобие char* a, b; Здесь согласно правилам, унаследованным из С, а является указателем, a b — обычной символьной переменной. Чтобы исключить такого рода путаницу, мы просто стараемся избегать объявления нескольких переменных приведенным образом. Данная книга не посвящена стандартной библиотеке C++, однако в ряде приведенных в ней примеров эта библиотека задействована. В общем случае мы используем заголовочные файлы C++ (например, <iostream>, а не <stdio .h>). Исключение составляет <stddef .h>. Мы применяем его вместо <cstddef > и, следовательно, не определяем size_t и ptrdif f_t с помощью префикса std: :, поскольку таким образом обеспечивается большая переносимость, а применение std: : size_t вместо size_t не дает никаких преимуществ. Заметим, что в C++ синоним типа определяет псевдоним, а не новый тип, например: typedef int Length; //Length определяется как псевдоним int int i = 42; Length 1 = 88; i = 1; // Корректно 1 = i; // Корректно
26 Глава 1. Об этой книге 1.5. Стандарт и практика Несмотря на то что стандарт C++ доступен с конца 1998 года, вплоть до 2002 года не существовало широкодоступного компилятора, о котором можно было бы сказать, что он "обеспечивает полное соответствие стандарту". Таким образом, сегодня поддержка языка в компиляторах реализована по-разному. Есть среди них такие, которые способны компилировать большую часть кода, приведенного в этой книге, однако многие достаточно популярные компиляторы с большинством наших примеров могут и не справиться. Для таких нестандартных реализаций C++, как правило, приводятся альтернативные приемы, которые должны обеспечить полное или частичное решение проблем, но некоторые из описанных в книге методик для таких компиляторов сегодня недоступны. Тем не менее авторы надеются, что эта проблема будет решена в широком масштабе, поскольку программисты требуют от производителей компиляторов строгого соответствия стандарту. Однако даже с учетом упомянутых трудностей язык программирования C++ находится в постоянном развитии. Эксперты из сообщества C++ (независимо от того, входят они или нет в состав Комитета по стандартизации C++) уже обсуждают различные пути улучшения языка; при этом некоторые из потенциальных усовершенствований затрагивают шаблоны. Некоторые тенденции в этой области рассмотрены в главе 13, "Направления дальнейшего развития". 1.6. Примеры кода и дополнительная информация Получить доступ к демонстрационным программам и найти дополнительную информацию об этой книге можно на ее Web-узле по адресу: http: / /www. josuttis. com/ tmplbook/. Кроме того, большое количество дополнительной информации по рассматриваемой теме вы сможете получить на Web-узле Дэвида Вандевурда (http: / /www. vandevoorde. com/ Templates) и в Web в целом. Начать советуем с просмотра списка литературы к данной книге. 1.7. Обратная связь с авторами Мы приветствуем любые конструктивные отклики читателей — как отрицательные, так и положительные. Нам обоим пришлось основательно потрудиться, чтобы создать для вас эту книгу, которую, надеемся, вы оцените как отличную. Однако в определенный момент мы просто вынуждены были прервать работу над ней, поскольку подошел срок "выпуска продукта". Следовательно, ни один из наших читателей не застрахован от того, что при изучении материала ему придется столкнуться с ошибками или несогласованностью, а также с отдельными моментами, которые
1.7. Обратная связь с авторами 27 нуждаются в доработке или с тем, что отдельные темы в книге не освещены вообще. Ваши отклики дают нам возможность проинформировать всех читателей через Web- узел данной книги о найденных вами "узких местах" и улучшить таким образом ее последующие издания. Связываться с нами лучше всего по электронной почте (tinplbook@josuttis.com); однако, прежде чем посылать сообщение, удостоверьтесь, пожалуйста, что найденная вами неточность отсутствует в списке опечаток на нашем Web-узле. Заранее благодарим вас за сотрудничество.
\ /
Часть I Основы Эта часть книги знакомит читателя с общими концепциями и языковыми средствами шаблонов C++. Она начинается с обсуждения основных задач и концепций на примерах шаблонов функций и шаблонов классов. В последующих главах рассматриваются некоторые дополнительные фундаментальные приемы работы с шаблонами, в частности параметры шаблонов, не являющиеся типами, ключевое слово typename и шаблоны- члены. В завершение приведены некоторые распространенные приемы применения шаблонов на практике. Данное введение в шаблоны частично использовано Николаи Джосаттисом (Nicolai М. Josuttis) в его книге Object-Oriented Programming in C++, опубликованной издательством John Wiley and Sons Ltd, ISBN 0-470-84399-3. Эта книга представляет собой учебное пособие, в котором дается описание всех возможностей языка C++ и его стандартной библиотеки, а также их практического применения. Зачем нужны шаблоны В C++ можно объявлять переменные, функции и большинство других видов объектов, используя конкретные типы. Однако в основном код для обработки объектов различных типов выглядит практически одинаково. Это особенно справедливо, если для разных типов данных требуется реализовать алгоритмы наподобие быстрой сортировки либо способы обработки таких структур данных, как связанный список или двоичное дерево. В таких случаях код одинаков для всех используемых типов объектов. В общем случае (если язык программирования не поддерживает специальных средств для решения подобных задач) у программиста имеются только следующие альтернативы: 1. Можно вновь и вновь реализовывать один и тот же алгоритм для каждого типа данных. 2. Можно написать общий код для обобщенного базового типа, такого, как Object или void*. 3. Можно использовать специальные препроцессоры.
30 Часть I. Основы Если говорить о конкретных языках (таких, как С, Java или подобных им), то читателю, возможно, уже приходилось проделывать подобные действия. Однако каждый из описанных выше подходов имеет свои недостатки. 1. Каждый раз заново реализуя один и тот же алгоритм, мы, по сути, снова и снова изобретаем велосипед. Мы делаем одни и те же ошибки и, чтобы не наделать их еще больше, стараемся избегать более сложных, но зато и более эффективных алгоритмов. 2. Если мы пишем обобщенный код для общего базового класса, то теряем при этом преимущество проверки типов. Кроме того, в разных ситуациях может потребоваться порождение от различных базовых классов, что еще более затрудняет поддержку кода. 3. При использовании специального препроцессора, например препроцессора C/C++, теряется преимущество форматирования исходного кода. Код заменяется некоторым "тупым механизмом замены текста", который не имеет представления ни об области видимости, ни о типах. Шаблоны обеспечивают решение данной проблемы, лишенное недостатков, присущих рассмотренным способам. Шаблон представляет собой функцию или класс, реализованные для одного или нескольких типов данных, которые не известны в момент написания кода. При использовании шаблона в качестве аргументов ему явно или неявно передаются конкретные типы данных. Поскольку шаблоны являются средствами языка, для них обеспечивается полная поддержка проверки типов и областей видимости. Шаблоны получили широкое применение в современном программировании. Например, практически весь код в стандартной библиотеке C++ состоит из шаблонов. Библиотека обеспечивает алгоритмы для сортировки объектов и значений определенного типа, структуры данных (так называемые классы контейнеров) для управления элементами конкретного типа, строки, для которых тип символа является параметризованным, и т.п. Однако это еще не все: шаблоны позволяют также параметризовать способы обработки данных, оптимизировать код и параметризовать информацию. Как это делается, описано в последующих главах. А пока начнем с самых простых шаблонов.
Глава 2 Шаблоны функций Данная глава знакомит читателя с шаблонами функций. Шаблоны функций — это параметризованные функции; таким образом, шаблон функции представляет целое семейство функций. 2.1. Первое знакомство с шаблонами функций Шаблон функции — это обобщенное описание поведения функций, которые могут вызываться для объектов разных типов; другими словами, шаблон функции представляет семейство функций. Шаблон очень похож на обычную функцию, разница только в том, что некоторые элементы этой функции не определены и являются параметризованными. Чтобы проиллюстрировать сказанное выше, рассмотрим небольшой пример. 2.1.1. Определение шаблона Ниже приведен шаблон функции, возвращающей большее из двух значений. // basics/max.hpp template <typename T> inline T constfc max(T constfc a, T const& b) { // Если а < b, возвращаем b, иначе а return a < b ? b : a; } Определение шаблона задает семейство функций, возвращающих большее из двух значений; эти значения передаются функции как ее параметры а и Ь. Тип этих параметров оставлен не определенным и задается как параметр шаблона Т. Как можно видеть из приведенного примера, параметры шаблонов необходимо объявлять, используя следующий синтаксис: template < разделеиный___запятыми_список__параметров > В нашем примере список параметров задан как typename Т. Обратите внимание, что в качестве скобок используются символы "меньше" и "больше". Ключевое слово
32 Глава 2. Шаблоны функций typename задает так называемый параметр типа. Это наиболее распространенный вид параметров шаблонов в программах на C++, хотя возможны и другие параметры, которые рассматриваются в книге несколько позже (см. главу 4, "Параметры шаблонов, не являющиеся типами"). В данном примере параметр типа обозначен как Т. В качестве имени параметра можно использовать любой идентификатор, но обычно по соглашению используется именно Т. Параметр типа представляет произвольный тип, который определяется при вызове функции. Можно использовать любой тип (это может быть один из базовых типов, класс и т.п.), который допускает применение операций, задействованных в шаблоне. В нашем случае тип Т должен поддерживать оператор <, поскольку он используется в теле функции для сравнения а и Ь. В силу исторических причин для определения параметра типа разрешается применение вместо typename ключевого слова class. Ключевое слово typename в ходе эволюции языка C++ появилось относительно недавно, а до этого единственным способом задания параметра типа было ключевое слово class. Применение class для определения параметра типа корректно и сегодня. Поэтому эквивалентным способом определения шаблона max () является следующий: // basics/max.hpp template <class T> inline T const& max(T constfc a, T const& b) { // Если a < b, возвращаем b, иначе а return a < b ? b : a; } Семантически в данном контексте между этими двумя способами записи нет никакой разницы. Даже в случае применения ключевого слова class для аргументов шаблона может быть использован любой тип. Однако, поскольку ключевое слово class может ввести в заблуждение (вместо Т можно подставлять не только тип, являющийся классом), в данном контексте следует отдавать предпочтение использованию ключевого слова typename. Отметим, что в отличие от объявлений типа класса, ключевое слово struct при объявлении параметров типа вместо typename использовать нельзя. 2.1.2. Использование шаблонов В приведенном ниже фрагменте кода иллюстрируется применение шаблона функции max (). // basics/max.cpp #include <iostream> #include <string> #include <max.hpp> int main()
2.1. Первое знакомство с шаблонами функций 33 { int i = 42; std::cout « "max(7,i): " « ::max(7,i) « std::endl; double fl = 3.4; double f2 = -6.7; std::cout « "max(f 1, f2) : " « ::max(fl,f2) « std::endl; std::string si = "mathematics"; std::stfing s2 = "math" ; std::cout « "max(sl,s2): " « ::max(sl,s2) « std::endl; } В этой программе max () вызывается трижды: для двух значений типа int, для двух double и для двух std: :string. Каждый раз вычисляется большее значение. В результате программа выводит следующую информацию: max(7,i): 42 max(fl,f2) : 3.4 max(sl/s2): mathematics Вы обратили внимание на то, что в примере каждый вызов шаблона max () предваряется двумя двоеточиями — : : ? Делается это вовсе не потому, что max () находится в глобальном пространстве имен. Причина здесь другая: в стандартной библиотеке тоже есть шаблон std: :max (), который может быть вызван при определенных обстоятельствах или способен привести к неоднозначности1. Обычно шаблоны не компилируются в какой-то один объект, способный обрабатывать любой тип данных. Вместо этого из шаблона генерируются различные объекты для / 2 каждого типа, для которого применяется шаблон . Таким образом, max () компилируется отдельно для каждого из упомянутых типов. Например, для первого вызова max () int i = 42; ... max(7, i) ... используется шаблон функции, в котором в качестве параметра шаблона Т указан тип int. Таким образом, он имеет семантику вызова следующего кода: inline int const& max(int const& a, int const& b) { // Если а < b, то возвращаем b, иначе а Например, если один тип аргумента определен в пространстве имен std (например, string), тогда в соответствии с правилами поиска имен C++ будут найдены оба шаблона— как глобальный, так и s td: : max (). 2 ' Альтернативный способ — "один объект на все случаи жизни" — также имеет право на существование, но на практике встречается крайне редко. Все правила языка основываются на предположении, что генерируются различные объекты.
34 Глава 2. Шаблоны функций return a < b ? b : a; } Процесс подстановки конкретных типов вместо параметров шаблона называется ин- стащированием шаблона (instantiation). Его результатом является экземпляр шаблона. К сожалению, термины инстанцирование (instantiation) и экземпляр (instance) в объектно- ориентированном программировании применяются и в другом контексте, а именно для конкретного объекта класса. Однако, поскольку наша книга посвящена шаблонам, этот термин будет использоваться применительно к шаблонам, если специально не оговорено другое. Отметим, что для запуска процесса инстанцирования достаточно просто использовать шаблон функции. Специально требовать от компилятора инстанцировзния шаблона не нужно. Аналогично, другие вызовы max О йнстанцируют шаблон max для double и std: : string точно так же, как они создавались бы в случае отдельного объявления и применения: const doubled max(double const&, double constfc); const std::string& max(std::string const&, std:-.string const&) ; Попытка инстанцировать шаблон для типа, который не поддерживает все используемые в шаблоне операции, приведет к ошибке компиляции, например: std::complex<float> cl,c2; // complex не поддерживает // оператор < max(01,02); " // ОШИБКА компиляции Таким образом, шаблоны компилируются дважды. 1. Без инстанцирования; код самого шаблона проверяется на правильность синтаксиса. Выявляются синтаксические ошибки, например пропущенные точки с запятой. 2. Во время инстанцирования код шаблона проверяется на корректность всех вызовов. Выявляются некорректные вызовы, в частности неподдерживаемые вызовы функций. Здесь проявляется важная проблема, связанная с обработкой шаблонов: если применение шаблона функции предполагает инстанцирование, то компилятору в определенный момент потребуется полное определение этого шаблона. Это отличается от обычных функций, когда для компиляции достаточно их объявления. Методы решения этой проблемы обсуждаются в главе 6, "Применение шаблонов на практике". А пока возьмем на вооружение простейший способ: реализуем каждый шаблон в заголовочном файле с использованием встраиваемых функций. 2.2. Вывод аргументов При вызове шаблона функции (например, max ()) с какими-либо аргументами параметры шаблона определяются передаваемыми в функцию аргументами. Если в качестве параметров
2.3. Параметры шаблонов 35 типа Т constfc передается два значения int, компилятор делает вывод, что вместо Т следует подставить int. Заметим, что автоматическое преобразование типов в шаблонах не допускается. Должно быть точное соответствие для каждого параметра типа, например: template <typename T> inline T const& max(T const& a, T const& b); max(4/7); // ВЕРНО: Т - int для обоих аргументов max(4,4.2); // ОШИБКА: первый Т - int, второй - double Существует несколько способов исправить эту ошибку. 1. Привести оба аргумента к одному типу: max(static__cast<double>(4) ,4.2) ; //ВЕРНО 2. Указать тип Т явно: max<double>(4,4.2); //ВЕРНО 3. Указать, что параметры могут иметь различные типы. Эти вопросы рассматривается в следующем разделе более подробно. 2.3. Параметры шаблонов Существуют два вида параметров шаблонов функций. 1. Параметры шаблона, которые объявляются в угловых скобках перед именем шаблона функции: template <typename T> // Т является параметром шаблона 2. Параметры вызова, которые объявляются в круглых скобках после имени шаблона функции: max(T const& a, T const& b); // а и b - параметры вызова Количество задаваемых параметров неограниченно. Однако в шаблонах функций (в отличие от шаблонов классов) нельзя использовать аргументы шаблона по умолчанию . Например, можно определить шаблон max () для двух различных типов данных. template <typename Tl, typename T2> inline Tl max (Tl constfc a, T2 const& b) { return a < b ? b : a; } max(4,4.2) // ВЕРНО, однако тип возвращаемого Это ограничение является главным образом результатом проблем исторического характера в развитии шаблонов функций. Для реализации такой возможности в современных компиляторах C++ технических препятствий не существует, и в будущем задание параметров шаблона по умолчанию, вполне вероятно, станет возможным (см. раздел 13.3).
36 Глава 2. Шаблоны функций // значения определяется типом первого // аргумента Казалось бы, неплохо иметь возможность передавать шаблону шах () два параметра вызова различных типов, но этот способ имеет свои недостатки. Проблема заключается в том, что мы должны объявить тип возвращаемого значения. Если для этого использовать один из типов параметров, аргумент для другого параметра должен конвертироваться в этот же тип, независимо от того, что именно хотел бы получить вызвавший этот шаблон программист. В C++ нет возможности задать выбор "наиболее мощного типа" (хотя такую возможность можно обеспечить с помощью определенных трюков при программировании шаблонов — см. раздел 15.2.4, стр. 298). Таким образом, в зависимости от порядка аргументов при вызове можно получить наибольшее из значений 42ибб.бби как double 66. 66, и как int 66. Еще один недостаток заключается в том, что при конвертировании типа второго параметра в тип возвращаемого значения создается новый локальный временный объект, а это означает, что возврат результата по ссылке невозможен . Поэтому в нашем примере тип возвращаемого значения должен быть Т1, а не Tl const&. Поскольку типы параметров вызова конструируются из параметров шаблона, параметры шаблона и параметры вызова обычно взаимосвязаны. Эта концепция называется выводом аргументов шаблона функции (function template argument deduction) и обеспечивает возможность вызывать шаблонную функцию так же, как и обычную. Однако, как уже упоминалось ранее, можно явно инстанцировать шаблон для конкретных типов. template <typename T> inline T const& max(T const& a, T const& b); max<double>(4/4.2); // Инстанцирование для Т, // представляющего собой double В тех случаях, когда связь между параметрами шаблона и параметрами вызова отсутствует или когда невозможно определить параметры шаблона, аргумент шаблона в его вызове следует задавать явно. Например, можно ввести третий тип аргумента шаблона, который задает тип значения, возвращаемого функцией. template <typename Tl, typename T2, typename RT> inline RT max(Tl const& a, T2 const& b) ; Однако вывод аргументов шаблона не работает с возвращаемыми типами5, a RT среди типов параметров вызова функции отсутствует. Следовательно, для определения RT Нельзя возвращать значения по ссылке, если они являются локальными для функции, поскольку при этом возвращается нечто, уже не существующее (после того как программа покинет область видимости данной функции). Вывод можно рассматривать как часть распознавания имени функции по типам ее параметров — процесс, который не использует тип возвращаемого значения. Единственным исключением является тип возвращаемого значения оператора-члена преобразования типов.
2.4. Перегрузка шаблонов функций 37 обычный вывод применить нельзя, а значит, список аргументов шаблона нужно задавать явно, например: template <typename Tl, typename T2, typename RT> inline RT max(Tl const& a, T2 const& b); max<int,double,double>(4,4.2) // ВЕРНО, но утомительно До сих пор рассматривались случаи, когда все аргументы шаблона функции либо задавались явно, либо явно не задавался ни один из них. Существует еще один подход: явно задается только первый аргумент, а остальные определяются при помощи вывода. Общее правило можно сформулировать так: следует явно задавать все типы аргументов, которые нельзя определить неявно. Таким образом, если в нашем примере изменить порядок следования параметров шаблона, то при вызове потребуется указать только тип возвращаемого значения. template <typename RT, typename Tl, typename T2> inline RT max(Tl const& a, T2 const& b); max<double>(4,4.2) // ВЕРНО: тип возвращаемого -// значения — double В данном примере при вызове max<double> значение RT явно задается как double, a типы параметров Т1 и Т2 определяются путем вывода из переданных аргументов как int и double. Заметим, что все рассмотренные выше модифицированные версии max () не обеспечивают сколько-нибудь значительных преимуществ — ведь ничто не мешает для версии с одним параметром явно указать тип параметра (и тип возвращаемого значения) для случая передачи аргументов различных типов. Поэтому лучше не усложнять себе жизнь и остановиться на версии max () с одним параметром (именно так мы и будем поступать в следующих разделах при обсуждении других вопросов, касающихся шаблонов). Процесс вывода более подробно описан в главе 11, "Вывод аргументов шаблонов". 2.4. Перегрузка шаблонов функций Шаблоны могут быть перегружены точно так же, как и обычные функции. Другими словами, могут иметься различные определения функций с одним и тем же именем, и при вызове функции с этим именем компилятор C++ примет решение о том, какую из функций-кандидаток следует вызвать. Правила принятия такого решения достаточно сложны даже без использования шаблонов. В этом разделе рассматривается перегрузка при участии шаблонов. Читателям, не знакомым с основными правилами перегрузки без шаблонов, рекомендуем обратиться к приложению Б, "Разрешение перегрузки", где дается достаточно подробный обзор правил перегрузки функций. Приведенная ниже небольшая программа иллюстрирует перегрузку шаблона функции.
38 Глава 2. Шаблоны функций // basics/max2.cpp // Большее из двух целочисленных значений inline int const& max(int constfc a, int const& b) { return a < b ? b : a; } // Большее из двух значений произвольного типа template <typename T> inline T const& max(T const& a, T const& b) { return a < b ? b : a; } / // Большее из трех значений произвольного типа template <typename T> inline T const& max(T const& a, T const& b, T const& c) { } return max(max(a,b),c); int main () ; { :max(7, 42, 68) :max(7.0, 42.0) :max('a' , 'b' :тах(7, 42); ) :тах<>(7, 42); :max<double>(7, 42) :тах('а\ 42.7); } трех аргументов (вывод // Вызов шаблона для // Вызов max<double> // аргументов) // Вызов max<char> (вывод аргументов) // Вызов функций, не являющейся // шаблоном, для двух целочисленных // аргументов // Вызов max<int> (вывод аргументов) ;// Вызов max<double> (без вывода // аргументов) // Вызов функции, не являющейся // шаблоном, для двух целых значений Как видно из данного примера, нешаблонная функция может вполне мирно сосуществовать с одноименным шаблоном функции, который может быть инстанцирован с тем же типом. При прочих равных условиях процесс распознавания имени функции по типам ее параметров обычно отдает предпочтение нешаблонным версиям, а не тем, которые генерируются на основе шаблонов. В соответствии с этим правилом в четвертом вызове max () инстанцирование шаблона не состоится. max(7,42); // При наличии двух int будет вызвана // функция, не являющаяся шаблоном
2.4. Перегрузка шаблонов функций 39 Но если на базе шаблона возможно сгенерировать функцию, которая для данного вызова подходит лучше, то выбор будет сделан в пользу шаблона. Это можно продемонстрировать на примерах второго и третьего вызовов max (). max(7.0,42.6); // Вызов max<double> (вывод // аргументов) max('a', 'b'); // Вызов max<char> (вывод // аргументов) Можно указать пустой список аргументов шаблона. Такой синтаксис определяет, что вызов можно выполнить только при помощи шаблона, но все параметры шаблона должны определяться на основе аргументов вызова. тах<>(7,42); // Вызов max<int> (вывод аргументов) Поскольку автоматическое преобразование типов для шаблонов невозможно, но вполне применимо для обычных функций, для последнего вызова используется не являющаяся шаблоном функция (при этом и ' а', и 42 .7 конвертируются в int). max('a', 42.7); // Различные типы аргументов допустимы // только в функции, не являющейся // шаблоном Приведем еще более полезный пример: перегрузка шаблона функции, вычисляющей наибольшее значение для указателей и обычных строк в С-стиле. // basics/тахЗ. срр #include <iostream> #include <cstring> #include <string> // Наибольшее из двух значений произвольных типов template <typename T> inline T const& max(T constfc a, T const& b) { return a < b ? b : a; } // Наибольший из двух указателей template <typename T> > inline T* const& max(T* constfc £, T* const& b) { return *a < *b ? b : a; } // Наибольшая из двух С-строк inline char const* const& max(char const* const& a, char const* const& b) { return std: :strcmp(a,b) < 0 ? b : a; } int main()
40 Глава 2. Шаблоны функций { int a = 7; int b = 42; ::max(a,b); // max() для двух значений int s.td: istring s="hey"; std::string t="you"; ::max(s,t); // max() для двух значений std:istring int* pi = &b; int* p2 = &a; ::max(pl,p2); // max() для двух указателей char const* si = "David"; char const* s2 = "Nico"; ::max(sl,s2); // max() для двух С-строк } Заметим, что. аргументы для всех перегруженных реализаций передаются по ссылке. В общем случае при перегрузке шаблонов функций лучше не вносить изменений больше, чем это необходимо. Изменения следует ограничить числом параметров или числом явно задаваемых параметров шаблона, так как в противном случае возможны неожиданные эффекты. Например,, если мы перегружаем шаблон max (), которому" передаются аргументы по ссылке, для двух С-строк, передаваемых по значению, то для вычисления наибольшей из трех С-строк мы не сможем использовать версию с тремя аргументами. // basics/тахЗа.срр #include <iostream> tinclude <cstring> #include <string> // Наибольшее из двух значений произвольного типа // (передача по ссылке) template <typename T> inline T constfc max(T const& a, T const& b) { return a < b ? b : a; } // Наибольшая из двух С-строк // (передача по значению) inline char const* max(char const* a, char const* b) { return std::strcmp(a,b) < 0 ? b : a; } // Наибольшее из трех значений произвольного типа // (передача по ссылке)
2.4. Перегрузка шаблонов функций 41 template <typename T> inline T const& max(T constk a, T const& b, T const& c) { return max(max(a,b),c); // ОШИБКА, если в max(a,b) // используется передача //по значению int main () { ::max(7,42,68); // ВЕРНО const char* si = "frederic"; const char* s2 = "anica"; const char* s3 = "lucas"; ::max(sl,s2,s3)) // ОШИБКА } Проблема заключается в том, что если мы вызываем max () для трех С-строк, то инструкция return max(max(a,b),с); становится некорректной. Это происходит потому, что в max () для С-строк создается новая временная локальная переменная, которую функция возвращает по ссылке. Здесь приведен только один пример кода, который вследствие нюансов правил перегрузки функций работает иначе, чем можно было бы ожидать. Например, может иметь (а может и не иметь) значение то, что не все перегруженные функции являются видимыми в момент вызова соответствующей функции. Так, определение версии max () с тремя аргументами для int при отсутствии объявления специализированной двухаргументнои версии max () для int приводит к тому, что в трехаргументной версии используется двухаргументный шаблон. // basics/max4.cpp // Наибольшее из двух значений произвольного типа template <typename T> inline T const& max(T constfc a, T const& b) { * return a < b ? b : a; } // Наибольшее из трех значений произвольного типа template <typename T> inline T const& max(T const& a, T const& b, T const& c) { return max(max(a,b),c); // Используется шаблонная } // версия даже для значений // int, поскольку объявление функции для // двух int располагается позже данного
42 Глава 2. Шаблоны функций // Максимальное из двух целочисленных значений inline int const& max(int const& a, int const& b) { return a < b ? b : a; } Более подробно этот вопрос будет рассмотрен в разделе 9.2, стр. 145, а пока в качестве правила примем следующее: объявления всех перегруженных версий функции следует помещать перед ее вызовом. ,/' 2.5. Резюме • Шаблоны функций определяют семейство функций для разных аргументов шаблона. • При передаче аргументов шаблона происходит инстанцирование шаблонов функций для данных типов аргументов. • Параметры шаблонов можно задавать явно. • Шаблоны функций можно перегружать. • При перегрузке шаблонов функций следует ограничивать вносимые изменения явным указанием параметров шаблона. • Следует убедиться, что все перегруженные версии шаблонов функций размещены в программе до вызовов соответствующих функций.
Глава 3 Шаблоны классов Подобно функциям, классы также могут быть параметризованы одним или несколькими типами. Типичным примером такой возможности могут служить классы контейнеров, которые применяются для работы с элементами определенного типа. Такие классы контейнеров с неизвестными заранее типами элементов реализуются с помощью шаблонов классов. В этой главе в качестве примера шаблона класса рассматривается стек. 3.1. Реализация шаблона класса Stack Объявим и определим класс Stacko в заголовочном файле точно так же, как это делалось для "шаблонов функций (вопросы помещения объявлений и определений в отдельных файлах будут рассмотрены в разделе 7.3, стр. 113). // basics/stackl.hpp #include <vector> #include <stdexcept> template <typename T> class Stack { private: std::vector<T> elems; // Элементы public: void puSh(T const&); void pop(); T top() const; bool empty() const { return elems,empty(); } }; template <typename T> void Stack<T>::push(T const& elem) // Добавление элемента // Снятие элемента // Возврат элемента //с вершины стека // Возвращает true, // если стек пуст
44 Глава 3. Шаблоны классов { elems.push_back(elem) ; // Добавление в стек // копии передаваемого // элемента } template <typename T> / void Stack<T>::pop() / { if (elems.empty()) { throw std: : out_of__range (" Stack< >: : pop () : " " empty stack"); } elems .pop__back() ; // Удаление последнего элемента } template <typename T> T Stack<T>::top() const { if (elems.empty()) { throw std: : out__of_range (" Stack< >: : top () :" " empty stack"); } return elems.back(); // Возврат копии последнего элемента } Как видите, для реализации шаблона класса используется шаблон класса vector о стандартной библиотеки C++. Таким образом, отпадает необходимость заниматься реализацией управления памятью, конструктора копирования и оператора присвоения и можно сосредоточиться только на интерфейсе шаблона класса. 3.1.1. Объявление шаблонов классов Объявление шаблона класса выполняется аналогично объявлению шаблона функции: ему должна предшествовать инструкция, которая объявляет некоторый идентификатор в качестве параметра типа. Здесь также принято использовать в качестве идентификатора Т. template <typename T> class Stack { ь Как и для шаблонов функций, вместо ключевого слова typename можно применять ключевое слово class. template <class T> class Stack {
3.1. Реализация шаблона класса Stack 45 Внутри шаблона класса идентификатор Т можно использовать в объявлениях членов и функций-членов так же, как и любой другой тип. В данном примере Т используется для объявления типа элементов как вектора значений с типом Т, для объявления push () как функции- члена класса, которая получает в качестве аргумента константную ссылку на объект типа Т, и для объявления функции top (), которая возвращает элемент типа Т. template <typename T> class Stack { private: std::vector<T> elems; // Элементы ' public: StackO; // Конструктор void push(T const&); // Добавление элемента void pop(); // Снятие элемента со стека Т top() const; // Возврат элемента //с вершины стека Класс имеет тип Stack<T>, где Т является параметром шаблона. Таким образом, каждый раз, когда требуется использовать тип этого класса в объявлении, следует указывать Stack<T>. Если, например, необходимо объявить собственные конструктор копирования и оператор присвоения, это должно выглядеть так1: template <typename T> class Stack { Stack (Stack<T> const&); // Конструктор копирования Stack<T>& operator = (Stack<T> constfc); // Оператор присвоения } Однако если требуется указать имя, а не тип класса, следует использовать только Stack. Это делается при указании имени класса, его конструктора и деструктора. 3.1.2. Реализация функций-членов Для того чтобы определить функцию-член шаблона класса, нужно указать, что это шаблон функции; при этом необходимо использовать полное имя типа шаблона класса. Таким образом, реализация функции-члена push () типа Stack<T> имеет следующий вид: template <typename T> void Stack<T>::push(T const& elem) { elems\push__back(elem) ; // Добавление копии // элемента в стек } В соответствии со стандартом, из этого правила имеются некоторые исключения (см. раздел 9.2.3, стр. 150). Однако для гарантии корректности лучше использовать полный тип.
46 Глава 3. Шаблоны классов Здесь для элемента вектора вызывается функция pushJoack (), которая и добавляет его в конец вектора. Заметим, что функция pop__back () вектора удаляет последний элемент, но не возвращает его, что связано с вопросами безопасности исключений. Реализовать полностью безопасную в плане исключений функцию pop (), возвращающую удаленный элемент, невозможно (этот вопрос впервые был рассмотрен Томом Каргиллом (Tom Cargill) в [9]; кроме того, этот вопрос детально рассматривается в [36]). Однако если игнорировать небезопасность данной функции в плане исключений, то можно написать функцию pop (), возвращающую только что удаленный элемент. Здесь Т используется просто для объявления локальной переменной соответствующего типа. template <typename T> void Stack<T>::pop() { if (elems.empty()) { throw std: : out__of__range (" Stacko: : pop () : " 11 empty stack"); } T elem = elems.back(); // Сохранение копии // последнего элемента elems.pop_back(); // Его удаление return elem; // Возврат сохраненной // копии элемента } Поскольку поведение функций вектора back () (возвращающей последний элемент) и pop_back () (удаляющей последний элемент) не определено для случая, когда вектор не содержит ни одного элемента, требуется проверка, не является ли стек пустым. Если он пуст, генерируется исключение типа std: :out__of„range. Такое же исключение генерируется и в функции top (), которая возвращает (но не удаляет) элемент, находящийся на вершине стека. template <typename T> Т Stack<T>::top() const { if (elems.empty()) { throw std::out_of„range("Stacko::top():" " empty stack"); } return elems.back(); // Возврат копии // последнего элемента } Разумеется, точно так же, как и в случае любых других функций-членов, функции- члены шаблонов классов можно реализовать как встраиваемые функции, располагающиеся внутри объявления класса, например:
3.2. Использование шаблона класса Stack 47 template <typename T> class Stack{ void push (T const& elem) { elems.push_back(elem); // Добавление копии // элемента в стек ) }; 3.2. Использование шаблона класса Stack Для того чтобы использовать объект шаблона класса, необходимо явно указать аргументы шаблона. В приведенном ниже примере проиллюстрировано применение шаблона класса Stacko. // basics/stackltest.cpp #include <iostream> #include <string> #include <cstdlib> #include "stackl.hpp" int main; { try { Stack<int> intStack; // Стек элементов типа int Stack<std::string> stringStack; // Стек элементов типа string // Работа со стеком целых чисел intStack.push(7); std::cout « intStack.top() « std::endl; // Работа со стеком строк stringStack.push("hello"); std::cout « stringStack.top() « std::endl; stringStack.pop(); stringStack.popO ; } catch (std::exception const& ex) { std::cerr « "Exception: " « ex.whatO « std::endl; return EXIT__FAILURE; // Выход из программы //с указанием ошибки }
48 Глава 3. Шаблоны классов Объявление Stack<int> указывает, что внутри шаблона класса в качестве типа Т будет использоваться int. Таким образом, intStack создается как объект на базе вектора с элементами типа int, и для всех вызываемых функций-членов инстанцируется код для этого типа. Точно так же, путем объявления и использования Stack<std: :string>, создается объект на базе вектора, элементами которого являются строки, и для каждой из вызываемых функций-элементов инстанцируется код для этого типа. Заметим, что инстанцирование происходит только для вызываемых функций-членов. Для шаблонов классов экземпляры функций-членов инстанцируются только при их использовании. Очевидно, что такой подход позволяет сэкономить время и память. Дает он и еще одно преимущество — возможность инстанцирования даже для тех типов, для которых выполняются не все операции в функциях-членах, — при условии, что "проблемные" функции-члены не вызываются. В качестве примера рассмотрим класс, в котором в некоторых функциях-членах для сортировки элементов используется оператор <. Если исключить вызовы этих функций-членов, то можно инстанцировать шаблон класса для тех типов, для которых оператор < не определен. В данном примере инстанцируются конструктор^тю- умолчанию, а также функции push () и top () для значений типа int и строк. Однако функция pop () инстанцируется только для строк. Если шаблон класса имеет статические элементы, то они инстанцируются однократно для каждого типа. Тип инстанцированного шаблона класса можно использовать так же, как и любой другой тип, при условии поддержки необходимых операций. void foo(Stack<int> const& s) // Параметр s является стеком целых чисел { Stack<int> istack[10]; // istack представляет собой // массив из 10 стеков целых чисел } Используя определение типов, можно сделать применение шаблонов классов более удобным. typedef Stack<int> IntStack; void foo(IntStack const& s) // Параметр s является стеком целых чисел { IntStack istack[10]; // istack представляет собой // массив из 10 стеков целых чисел } Заметим, что в C++ при определении с помощью typedef задается псевдоним типа, а не новый тип. Таким образом, после определения типа typedef Stack<int> IntStack;
3.3. Специализации шаблонов класса 49 типы IntStack и Stack<int> представляют собой один и тот же тип; эти обозначения можно использовать одно вместо другого и присваивать друг другу переменные этого типа. Аргументы шаблона могут быть любого типа, например указателями на float или даже стеками целых чисел. Stack<float*> floatPtrStack; // Стек указателей на значения float Stack<Stack<int> > intStackStack; // Стек стеков значений int Должно выполняться только одно требование: чтобы любая вызываемая операция для данного типа была допустима. Обратите внимание, что между двумя закрывающими угловыми скобками следует помещать пробел. Если этого не делать, то две угловые скобки будут интерпретироваться как оператор », что приведет к синтаксической ошибке. Stack<Stack<int>> intStackStack; //ОШИБКА: >> не допускается 3.3. Специализации шаблонов класса Шаблон класса можно специализировать для конкретных аргументов шаблона. Так же, как и в случае перегрузки шаблонов функций (см. раздел 2.4, стр. 37), специализированные шаблоны классов позволяют оптимизировать реализации для конкретных типов или корректировать неверное поведение определенных типов для инстанцирования шаблона класса. Однако при специализации шаблона класса необходимо специализировать все его функции-члены. Хотя можно специализировать и отдельную функцию-член, после этого нельзя будет специализировать целый класс. Чтобы специализировать шаблон класса, следует объявить класс, предварив его конструкцией templateo, и указать типы, для которых специализируется шаблон класса. Типы используются в качестве аргументов шаблона и задаются непосредственно после имени класса. templateo class Stack<std::string> { } Для таких специализаций любая функция-член должна определяться как "обычная" функция-член с заменой каждого включения Т специализированным типом. void Stack<std::string>::push(std::string const& elem) { elems .push___back(elem) ; // Добавление копии элемента //в конец массива }
50 Глава 3. Шаблоны классов \ Далее приведен завершенный пример специализации Stacko для типа std:: string. // basics/stack2.hpp #include <deque> #include <string> #include <stdexcept> #include "stackl.hpp" template <> class Stack<std::string> { private: std::deque <std::string> elems; // Элементы public: void push(std::string const&); // Добавление // элемента void pop(); //Снятие элемента со стека std::string top() const; //Возврат элемента //с вершины стека bool empty() const { // Возвращает true, return elems.empty(); // если стек пуст } }; void Stack<std::string>::push(std::string const& elem) { elems.push_back(elem); // Добавление копии // элемента в стек } void Stack<std::string>::pop() { if (elems.empty()) { throw std::out_of„range("Stack<std::string>::pop():" 11 empty stack") ; } elems.pop„back(); // Удаление последнего элемента } std::string Stack<std::string>::top() const { if (elems.empty()) { throw std::out_of„range("Stack<std::string>::top():" " empty stack"); } return elems.back(); // Возврат копии // последнего элемента }
3.4. Частичная специализация 51 В данном примере для управления элементами в стеке вместо вектора используется очередь с двусторонним доступом (дек). Такая замена не дает особых преимуществ; это сделано, чтобы показать, что реализация специализации может значительно отличаться от реализации первичного шаблона2. ЗА Частичная специализация Специализация шаблонов классов может быть частичной. Можно определить реализации для определенных типов, но при этом некоторые параметры шаблона остаются задаваемыми пользователем. Например, для шаблона класса template<typename Tl, typename T2> class MyClass { } возможны следующие частичные специализации: // Частичная специализация: оба параметра шаблона // имеют один и тот же тип template<typename T> class MyClass<T,T> { }; // Частичная специализация: тип второго параметра - int template<typename T2> class MyClass<T,int> { }; // Частичная специализация: оба параметра - указатели template<typename Tl, typename T2> class MyClass<Tl*,T2*> { } ; В приведенном далее примере показано, какие шаблоны применяются при разных объявлениях. MyClass<int/float> mif; // Используется MyClass<Tl,T2> MyClass<float,float> mff; // Используется MyClass<T,T> В действительности при использовании дека вместо вектора для реализации стека определенное преимущество все-таки есть: при удалении элементов происходит освобождение памяти, кроме того, не может произойти перемещение элементов вследствие перераспределения памяти (впрочем, для строк это не такое уж значительное преимущество). По этой причине в основном шаблоне класса лучше использовать дек (как это сделано в классе std: : stacko в стандартной библиотеке C++).
52 Глава 3. Шаблоны классов MyClass<float,int> mfi; // Используется MyClass<T,int> MyClass<int*,float*> mp; // Используется MyClass<Tl*/T2*> Если для объявления одинаково хорошо подходит несколько частичных специализаций, получается неоднозначность, не разрешаемая компилятором. MyClass<int/ int > m; //ОШИБКА: соответствуют / // MyClass<T,T> и MyClass<T,int> MyClass<int*, int*> m; //ОШИБКА: соответствуют // MyClass<T,T> и MyClass<Tl*,T2*> Чтобы избежать неоднозначности во втором случае, можно использовать дополнительную частичную специализацию для указателей одного и того же типа: template<typename T> class MyClass<T*,T*> { }; Более подробно этот вопрос рассматривается в разделе 12.4, стр. 225. 3.5. Аргументы шаблона, задаваемые по умолчанию В случае использования шаблонов класса для параметров шаблона можно определять значения по умолчанию. Эти значения называются аргументами шаблона по умолчанию. Например, в классе Stacko можно использовать второй параметр шаблона, определяющий контейнер, который применяется для хранения элементов, в качестве значения по умолчанию указывая тип s td: : vectoro. // basics/stack3.hpp #include <vector> #include <stdexcept> template <typename T, typename CONT = std::vector<T> > class Stack { private: CONT elems; // Элементы public: void push(T const&); void pop(); T top() const; bool empty() const { return elems.empty(); } // Добавление элемента // Снятие со стека // Возврат элемента //с вершины стека // Возвращает true, // если стек пуст };
3.5. Аргументы шаблона, задаваемые по умолчанию 53 template<typename Т, typename CONT> void Stack<T, CONT>::push(T const& elem) { elems.push_back(elem); // Добавление элемента } template<typename T, typename CONT> void Stack<T,CONT>: :pop() { if (elems.empty()) { throw std: : out__of _range ( " Stacko: : pop () : " " empty stack"); } elems.pop_back(); // Удаление последнего элемента } template<typename T, typename CONT> T Stack<T,CONT>: :top() const { if (elems.empty()) { throw std::out_of_range("Stacko::top():" 11 empty stack") ; } return elems.back(); // Возврат копии // последнего элемента } Заметим, что, поскольку теперь у нас два параметра шаблона, каждое определение функции-члена должно иметь два параметра, template<typename T, typename CONT> void Stack<T,CONT>::push(T const& elem) { elems.push_back(elem); // Добавление элемента } Этот стек можно использовать точно так же, как и раньше. Если шаблону передается только первый аргумент, представляющий тип элементов в стеке, то для хранения элементов этого типа используется вектор. template<typename Т, typename CONT = std::vector<T> > class Stack { private: CONT elems; // Элементы }
54 Глава 3. Шаблоны классов При объявлении объекта Stack в нашей программе можно явно указать, какой контейнер должен использоваться для хранения элементов. // basics/stack3test.hpp #include <iostream> #include <deque> #include <cstdlib> ч #include "stack3.hpp" int main; { try { Stack<int> intStack; // Стек значений int // Стек значений double, в котором для хранения // элементов используется std::deque<> Stack<double,std::deque<double> > dblStack; // Работа со стеком целых чисел intStack.push(7); std::cout « intStack.top() « std::endl; // Работа со стеком чисел с плавающей точкой dblStack.push(42.42); std::cout « dblStack.top() « std::endl; dblStack.popO ; dblStack.pop(); } catch(std::exception const& ex) { std::cerr « "Exception: " « ex.whatO « std::endl; return EXIT_FAILURE; // Выход из программы //с указанием ошибки } } С помощью конструкции Stack<double,std::deque<double> > объявляется стек значений double, в котором для внутренней работы с элементами исг пользуется контейнер std: :deque<>. 3.6. Резюме • Шаблон класса — это класс, который реализован с одним или несколькими параметрами типов, остающимися открытыми.
3.6. Резюме 55 • Чтобы применить шаблон класса, нужно использовать конкретные типы в качестве аргументов шаблона. После этого шаблон класса инстанцируется (и компилируется) для указанных типов. • Для шаблонов классов инстанцируются только те функции-члены, которые реально вызываются в программе. • Можно специализировать шаблоны классов для конкретных типов. • Можно выполнять частичную специализацию шаблонов классов. • Для параметров шаблона класса можно задавать значения по умолчанию. Эти значения могут использовать предшествующие параметры шаблона.
\
Глава 4 Параметры шаблонов, не являющиеся типами В качестве параметров шаблонов классов или функций могут выступать не только типы, но и обычные величины. В этом случае, как и для шаблонов с параметрами типа, программист создает код, в котором определение отдельных деталей откладывается "на потом", т.е. до момента, когда код будет использоваться; однако эти детали представляют собой уже не типы, а величины. При использовании шаблона эти величины задаются явно, после чего выполняется инстанцирование кода шаблона. В данной главе эта возможность продемонстрирована на примере новой версии шаблона класса стека. Кроме того, здесь приведен пример параметров шаблона функции, не являющихся типами, и рассмотрены некоторые ограничения применения этой технологии. 4.1. Параметры шаблонов классов, не являющиеся типами В отличие от примеров реализаций стека из предыдущих глав, стек можно реализовать и на базе массива с фиксированным размером, в котором будут храниться элементы. Преимущество этого метода состоит в сокращении расхода ресурсов на управление памятью, независимо от того, выполняет ли это управление программист или стандартный контейнер. Однако возникает другая проблема: какой размер для такого стека будет оптимальным? Если указать размер, меньший, чем требуется, это приведет к переполнению стека. Если задать слишком большой размер, память будет расходоваться неэффективно. Напрашивается вполне резонное решение: оставить определение этого значения на усмотрение пользователя — он должен указать максимальный размер, необходимый для работы именно с его элементами. Определим для этого размер в качестве параметра шаблона. // basics/stack4.hpp ^include <stdexcept> template <typename T, int MAXSIZE>
58 Глава 4. Параметры шаблонов, не являющиеся типами // Элементы //Их текущее количество // Конструктор // Добавление элемента . // Снятие элемента // Возвращение элемента //с вершины стека // Возвращается true, // если стек пуст // Возвращается true, // если стек заполнен class Stack { private: Т elems[MAXSIZE]; int numElems; public: Stack(); void push(T const&); void pop() ; T top() const; bool empty() const { return numElems == 0; } bool full() const { return numElems == MAXSIZE; } }; // Конструктор template <typename T, int MAXSIZE> Stack<T,MAXSIZE>::Stack() : numElems(0) // В начале //в стеке нет элементов { // Больше ничего не делается } template <typename T, int MAXSIZE> void Stack<T,MAXSIZE>::push(T const& elem) { if (numElems == MAXSIZE) { throw std: : out_of_range ("Stack<>: :push() : stack" - is full") ; } elems[numElems] = elem; // Добавление элемента //в конец массива ++numElems; // Увеличение числа элементов // на 1 } template <typename T, int MAXSIZE> void Stack<T/MAXSIZE>::pop() { if (numElems <= 0) { throw std: : out__of_range ("Stacko: : pop () : empty" " stack") ;
4.1. Параметры шаблонов классов, не являющиеся типами 59 } —numElems; // Уменьшение // числа элементов на 1 template <typename T, int MAXSIZE> Т Stack<T,MAXSIZE>::top() const { if (numElems <= 0) { throw std: : out_of__range ("Stacko: : top () : empty" " stack"); } return elems[numElems-l];// Возврат последнего элемента } Новый второй параметр шаблона MAXSIZE имеет тип int. Он задает размер массива элементов стека. template <typename Т, int MAXSIZE> class Stack { private: T elems[MAXSIZE]; // Элементы }; ~ Кроме того, он задействован в функции push () для проверки заполненности стека. template <typename T, int MAXSIZE> void Stack<T/MAXSIZE>::push(T constfc elem) { if (numElems == MAXSIZE) { throw "Stacko: :push() : stack is full"; } elems [numElems] = elem; //Добавление элемента в // конец массива ++numElems // Увеличение числа элементов } Для того чтобы использовать этот шаблон класса, следует задать как тип элементов, так и максимальный размер стека. // basics/stack4test.cpp #include <iostream> #include <string>. #include <cstdlib> #include "stack4.hpp" int main()
60 Глава 4. Параметры шаблонов, не являющиеся типами try { \ Stack<int,20> int20Stack; // Стек, вмещающий до^О // целых значений Stack<int/40> int40Stack; // Стек, вмещающий до 40 // целых значений Stack<std::string,40> stringStack; // Стек, вмещающий до 40 // строк // Работа со стеком из 20 целых чисел int20Stack.push(7); std::cout « int20Stack.top() « std::endl; int20Stack.pop(); // Работа со стеком из 40 строк stringStack.push("hello"); std::cout « stringStack.top() « std::endl; stringStack.pop(); stringStack.pop(); } catch (std::exception const& ex) { std::cerr « "Exception: " « ex.whatO « std::endl; return EXIT_FAILURE; // Выход из программы //с указанием ошибки } } Заметим, что каждый экземпляр шаблона имеет свой собственный тип, т.е. int20Stack и int40Stack — это два различных типа. Преобразование этих типов один в другой — ни явное, ни неявное— не определено. Следовательно, нельзя использовать один тип вместо другого и нельзя присваивать значение одного из этих типов другому. Остается добавить, что для параметров данного шаблона можно задать значения по умолчанию. template <typename Т = int, int MAXSIZE = 100> class Stack { }; Однако в контексте грамотного дизайна это не имеет смысла. Значения по умолчанию — это значения, которые интуитивно подходят в общем случае; но нельзя сказать, что тип int или максимальный размер, равный 100, в общем случае для типа стека подходят лучше, чем другие. Следовательно, оба эти значения программист должен указывать явно.
4.2. Параметры шаблонов функций, не являющиеся типами 61 4.2. Параметры шаблонов функций, не являющиеся типами Параметры, не являющиеся типами, можно использовать и в шаблонах функций. Например, приведенный ниже шаблон функции определяет группу функций, предназначенных для добавления некоторого значения. // basics/addval.hpp template <typename Т, int VAL> T addValue (T const& x) { return x + VAL; } Функции такого типа полезны тогда, когда функции или, в общем случае, операции используются в качестве параметров. Например, при работе со стандартной библиотекой шаблонов экземпляр этого шаблона функции можно использовать для добавления определенного значения к каждому элементу коллекции. std::transform(source.begin(), // Начало и конец source.end(), // исходной коллекции dest.begin(), // Начало результирующей // коллекции 4 addValue<int,5>); // Операция Последний аргумент вызывает инстанцирование шаблона функции addValue, которая добавляет 5 к целочисленному значению. Полученная функция вызывается для каждого элемента исходной коллекции source, в процессе чего последняя преобразуется в результирующую коллекцию dest. Заметим, что в этом примере проявляется следующая проблема: addValue<int, 5> — это шаблон функции, который интерпретируется как имя семейства перегруженных функций (даже если это семейство состоит всего из одного элемента). Однако в соответствии с текущим стандартом семейства перегруженных функций нельзя использовать при выводе аргументов шаблона. Таким образом, аргумент шаблона функции необходимо привести к точному типу. std::transform(source.begin(), // Начало и конец source.end(), // исходной коллекции dest.begin(), // Начало результирующей // коллекции (int(*)(int const&))addValue<int,5>); // Операция Существует предложение изменить стандарт C++ таким образом, чтобы в данном контексте не требовалось приведение типов [11], однако сегодня для обеспечения переносимости явное приведение типов необходимо.
62 Глава 4. Параметры шаблонов, не являющиеся типами 4.3- Ограничения на параметры шаблонов, не являющиеся типами На параметры шаблонов, не являющиеся типами, накладываются некоторые ограничения. В общем сдучае такими параметрами могут быть только целочисленные константы (включая перечисления) или указатели на объекты с внешним связыванием. Использование чисел с плавающей точкой и объектов с типом класса в качестве параметров шаблона не допускается. template <double VAT> // ОШИБКА: значения с double process(double.v) // плавающей точкой нельзя { // применять в качестве return v * VAT; // параметров шаблона } template <std::string name> // ОШИБКА: объекты типа class MyClass { // класса не разрешается ... / // использовать как }; // параметры шаблона Числа с плавающей точкой (как и литеральные выражения с плавающей точкой) нельзя применять исключительно по причинам исторического характера. Поскольку для этого нет серьезных технических препятствий, в будущих версиях C++ такая возможность, вероятно, станет поддерживаться (см. раздел 13.4, стр. 235). Поскольку строковые литералы— это объекты с внутренним связыванием (два строковых литерала, которые имеют одинаковые значения, но находятся в разных модулях, являются разными объектами), их использование в качестве аргументов шаблона не допускается. template <char const* name> class MyClass { }; MyClass<"hello"> x; // ОШИБКА: строковый литерал "hello" // здесь использовать нельзя Нельзя также использовать для этой цели и глобальный указатель. template <char const* name> class MyClass { }; '"• char const* s = "hello"; MyClass<s> x; // ОШИБКА: s - указатель на объект // со внутренним связыванием
4.4. Резюме 63 Однако приведенный ниже код допустим. template <char const* name> class MyClass { ь- extern char const s[] = "hello"; MyClass<s> x; // OK Поскольку здесь глобальный массив символов s инициализируется значением "hello", он является объектом с внешним связыванием. Более подробно этот вопрос рассмотрен в разделе 8.3.3, стр. 133, а возможные изменения в данной области в будущем обсуждаются в разделе 13.4, стр. 235. 4.4. Резюме • В качестве параметров шаблонов могут выступать не только типы, но и значения. • В качестве аргументов для параметров шаблонов, не являющихся типами, нельзя использовать числа с плавающей точкой, объекты типа класса и объекты с внутренним связыванием (такие, как строковые литералы).
Глава 5 Основы работы с шаблонами В данной главе продолжается рассмотрение основных свойств шаблонов и практических приемов работы с ними. Здесь обсуждаются следующие вопросы: дополнительные возможности применения ключевого слова typename, использование шаблонов функций- членов и вложенных классов, шаблонные параметры шаблонов, инициализация нулем и некоторые детали, касающиеся использования строковых литералов в качестве аргументов шаблонов функций. Упомянутые аспекты не всегда можно отнести к числу простых и очевидных, однако программист-практик должен иметь хотя бы представление о них. 5.1. Ключевое слово typename Это ключевое слово введено в язык в процессе стандартизации C++ для указания того, что идентификатор в шаблоне является типом. Рассмотрим следующий пример: template <typename T> class MyClass { typename T::SubType * ptr; } ; В этом примере второе ключевое слово typename используется для пояснения, что SubType является типом, определенным внутри класса Т. Таким образом, ptr является указателем на Т: : SubType. Без такого указания с помощью typename идентификатор SubType интерпретировался бы как статический член класса, т.е. как конкретная переменная или объект. В результате выражение т::SubType * ptr представляло бы собой умножение статического члена класса SubType на ptr. В общем случае ключевое слово typename следует использовать всякий раз, когда имя, зависящее от параметра шаблона, представляет собой тип (более подробно этот вопрос рассматривается в разделе 9.3.2, стр. 154).
66 Глава 5. Основы работы с шаблонами Типичным применением typename является доступ к итераторам контейнеров STL в коде шаблона. // basics/printcoll.hpp #include <iostream> ^\ // Вывод элементов контейнера STL void printcoll (T const& coll) { typename T::const_iterator pos; // Итератор для // цикла по coll typename Т::const_iterator end(coll.end()); //Конечная позиция for(pos = coll.begin(); pos != end; ++pos) { std::COUt « *pos « ' ' ; } std::cout « *pos « endl; } В этом шаблоне функции параметр вызова является контейнером STL типа Т. Для цикла по всем элементам контейнера используется итератор, который объявлен внутри каждого класса контейнера как тип const__iterator. class stlcontainer { typedef ... iterator; // Итератор для доступа // для чтения/записи typedef ... const_iterator; // Итератор для доступа // только для чтения }; Таким образом, для того чтобы получить доступ к типу const_iterator шаблона типа Т, нужно выполнить уточнение типа с использованием ключевого слова typename: typename Т: :const__iterator pos; Конструкция .template Очень похожая проблема была обнаружена в C++ и после введения в язык ключевого слова typename. Рассмотрим пример, в котором используется стандартный тип bitset. template<int N> void printBitset(std::bitset<N> const& bs) { std::cout « bs.template to_string<char, char___trats<char>, allocator<char> >(); }
5.2. Использование this-> 67 В этом примере присутствует непривычная конструкция — . template. Зачем она нужна? Если не использовать здесь "лишнее" ключевое слово template, то компилятор не будет знать, что знак "<" на самом деле означает не "меньше чем", а начало списка аргументов шаблона. Заметим, что такая проблема возникает только в случаях, когда конструкция, предшествующая точке, зависит от параметра шаблона. В нашем примере параметр bs зависит от параметра шаблона N. В заключение отметим, что запись .template (и аналогичные, наподобие ->template) должны использоваться только внутри шаблонов и только в том случае, если они следуют за выражением, которое зависит от параметра шаблона. Более подробно этот вопрос рассматривается в разделе 9.3.3, стр. 156. 5.2. Использование this-> Для шаблонов классов, имеющих базовые классы, использование имени х не всегда эквивалентно this->x, даже если член х является наследуемым. template <typename T> class Base { public: void exit(); }; template <typename T> class Derived : Base<T> { public: void foo() { exit(); // Вызов внешней функции exit() // или ошибка } }; В этом примере при разрешения имени exit () в теле f оо () никогда не рассматривается функция exit () из класса Base. Следовательно, либо будет выведено сообщение об ошибке, либо будет вызвана другая функция exit () (например, стандартная функция С exit ()). Более подробно этот вопрос рассматривается в разделе 9.4.2, стр. 161. А пока рекомендуем использовать следующее правило: всегда необходимо полностью указывать любое имя, объявленное в базовом классе, который каким-либо образом зависит от параметра шаблона. Для этого можно использовать конструкции this-> или Base<T>: :. Чтобы гарантированно исключить какую бы то ни было неопределенность, можно использовать полное имя при любом обращении к членам классов (в шаблонах).
68 Глава 5. Основы работы с шаблонами 5.3. Шаблоны-члены классов Члены классов тоже могут быть шаблонами; это справедливо как для "вложенных классов, так и для функций-членов. Применение и преимущества такой возмЬжности можно еще раз продемонстрировать на примере шаблона класса Stacko. Обычно стеки можно присваивать друг другу только в том случае, если они имеют одинаковый тип, что предполагает одинаковый тип их элементов. Однако стеку невозможно присвоить стек с элементами любого другого типа, даже если для типов элементов определено неявное преобразование типов. Stack<int> intStackl, intStack2; // Стеки для // целочисленных значений Stack<float> floatStack; // Стек для значений //с плавающей точкой intStackl = intStack2; // КОРРЕКТНО: стеки имеют // одинаковый тип floatStack = intStackl; // ОШИБКА: стеки имеют // разные типы Используемый по умолчанию оператор присвоения требует, чтобы с обеих сторон оператора использовался один и тот же тип, но если типы элементов у стеков различны, то это не так. Однако если задать оператор присвоения в виде шаблона, то присвоение стеков с элементами, для которых определено соответствующее преобразование типов, станет возможным. Для этого необходимо объявить Stacko, как показано ниже. // basics/stack5decl.hpp « template <typename T> class Stack { private: std::deque<T> elems; public: void push(T const&); void pop(); T top() const; bool empty() const { return elems.empty() } // Элементы // Добавление элемента // Снятие элемента // Возвращение элемента //с вершины стека // Возвращается true, // если стек пуст }; // Присвоение стека элементов с типом Т2 template <typename T2> Stack<T>& operator= (Stack<T2> constfc);
5.3. Шаблоны-члены классов 69 Были сделаны два изменения. 1. Добавлено объявление оператора присвоения для стека с элементами другого типа Т2. 2. Теперь в качестве внутреннего контейнера для элементов стека используется очередь с двусторонним доступом. Это следствие реализации нового оператора присвоения. Реализация нового оператора присвоения показана ниже. // basics/stack5assign.hpp template <typename T> template <typename T2> Stack<T>& Stack<T>::operator = (Stack<T2> const& op2) { if ((void*)this == (void*)&op2) { // Присвоение // самому себе? return *this; } Stack<T2> tmp(op2); // Создание копии // присваиваемого стека elems.clear(); // Удаление существующих // элементов while(!tmp.empty()) { // Копирование всех элементов elems.push_front(tmp.top()); tmp.popO ; } return *this; } Прежде всего посмотрим на синтаксис определения шаблона-члена. Внутри шаблона с параметром Т определяется внутренний шаблон с параметром Т2. template <typename T> template <typename T2> Казалось бы, в теле функции-члена можно просто обращаться ко всем необходимым данным присваиваемого стека ор2. Однако этот стек имеет другой тип (при инстанцировании шаблона класса для двух разных типов данных будут получены стеки двух разных типов), поэтому использовать открытый интерфейс здесь нельзя. Отсюда следует, что единственный способ обращения к элементам стека— это вызов top (). Однако для этого каждый элемент должен находиться в вершине стека. Таким образом, сначала нужно сделать копию ор2 с тем, чтобы можно было удалять элементы при помощи вызовов pop (). Поскольку Функция top () возвращает последний элемент, помещенный в стек, необходимо использовать контейнер, который поддерживает вставку элементов в противоположный конец коллекции. Поэтому используется очередь с двусторонним доступом, у которой имеется Функция push_f ront (), помещающая элемент в начало коллекции.
70 Глава 5. Основы работы с шаблонами Имея такой шаблон-член, можно присвоить стек значений типа int стеку со значениями типа float. Stack<int> intStack; //Стек целочисленных значений Stack<float> floatStack;// Стек значений с плавающей точкой floatStack = intStack; // КОРРЕКТНО: стеки имеют разные // типы, но int конвертируется //во float Разумеется, такое присвоение не изменяет типа стека и его элементов. После присвоения тип элементов floatStack остается float и, следовательно, функция рор() будет по-прежнему возвращать значение типа float. Может показаться, что проверка типов в этой функции блокируется вообще, так что можно выполнять присвоение стеков с элементами любого типа. Но это не так. Необходимая проверка типов происходит, когда элемент (копии) исходного стека помещается в результирующий стек. elems.push_front(tmp.top()); Если, например, стек строк присвоить стеку значений с плавающей точкой, при компиляции этой строки будет выдано сообщение об ошибке, в котором говорится, что строка, возвращаемая функцией tmp.topO, не может быть передана как аргумент функции elems.push_front() (в зависимости от компилятора, сообщения могут быть различными, но смысл их именно такой). Stack<std::string> stringStack; // Стек строк Stack<float> floatStack; // Стек значений с // плавающей точкой floatStack = stringStack; // ОШИБКА: std::string не // конвертируется во float Заметим, что оператор присвоения шаблона не замещает оператор присвоения, используемый по умолчанию. Для присвоения стеков одного и того же типа по-прежнему будет вызываться стандартный оператор присвоения. Можно изменить реализацию таким образом, чтобы параметризовать тип внутреннего контейнера. // basics/stack6decl.hpp template <typename T, typename CONT = std::deque<T> > class Stack { private: CONT elems; // Элементы
5.3. Шаблоны-члены классов 71 public: void push(T const&); // Добавление элемента void pop(); // Снятие элемента Т top() const; // Возвращение элемента //с вершины стека bool empty() const { // Возвращает true, return elems.empty(); // если стек пуст } // Присвоение стека с элементами типа Т2 template <typename Т2, typename C0NT2> Stack<T,CONT>& operator= (Stack<T2,C0NT> constfc); }; Оператор присвоения шаблона будет выглядеть, как показано ниже. // basics/stack6assign.hpp template <typename Т, typename CONT> template <typename T2, typename C0NT2> Stack<T,CONT>& Stack<T,CONT>::operator = (Stack<T2,C0NT2> constfc op2) { if ((void*)this == (void*)&op2) { // Присвоение return *this; // самому себе? } Stack<T2,CONT2> tmp(op2); // Создание копии // присваиваемого стека elems.clear(); // Удаление существующих // элементов while(!tmp.emptyO) { // Копирование всех элементов elems.push_front(tmp.top()); tmp.popO ; } return *this; } Вспомним, что в случае шаблонов классов инстанцируЮтся только вызываемые функции-члены. Отсюда следует, что если исключить присвоение стеков с элементами разных типов, то в качестве внутреннего контейнера можно вполне использовать вектор. // Стек целочисленных значений, в котором в качестве // внутреннего контейнера используется вектор Stack<int,std::vector<int> > vStack; vstack.push(42); VStack.push(7); std::cout « vStack.popO « std::endl;
72 Глава 5. Основы работы с шаблонами Поскольку необходимости в операторе присвоения нет, сообщение об ошибке отсутствия функции-члена push__f ront () не выдается и программа работает корректно. Чтобы ознакомиться с полной реализацией последнего примера, рассмотрите все файлы из подкаталога basics с именами, которые начинаются со stacks1. 5.4. Шаблонные параметры шаблонов Во многих случаях было бы полезно, если бы параметр шаблона сам по себе мог быть шаблоном класса. В качестве примера мы опять воспользуемся нашим шаблоном класса стека. При использовании в стеках различных внутренних контейнеров программист вынужден указывать тип элементов стека дважды: чтобы указать тип внутреннего контейнера, необходимо указать как его тип, так и тип его элементов. Stack<int/std::vector<int> > vStack; // Стек целых чисел с использованием вектора Шаблонные параметры шаблонов обеспечивают возможность объявлять шаблон класса Stack путем задания типа контейнера без повторного задания типа его элементов. stack<int/std::vector> vStack; // Стек целых чисел с использованием вектора Для этого нужно задать второй параметр шаблона как шаблонный параметр шаблона. В принципе это будет выглядеть следующим образом : // basics/stack7decl.hpp template <typename Т, template <typename ELEM> class CONT = std::deque> class Stack { private: CONT<T> elems; // Элементы public- void push(T const&); // Добавление элемента void pop(); // Снятие элемента T top() const; ' // Возвращение элемента // с вершины стека bool empty() const { // Возвращает true, Не удивляйтесь, если ваш компилятор при компиляции этих файлов-примеров выдаст ошибку. В приведенных примерах используются практически все важные возможности шаблонов. Поэтому рекомендуем использовать компилятор, который как можно более точно соответствует стандарту. 2 У данного кода имеется одна проблема, которую мы сейчас рассмотрим. Однако, поскольку она проявляется только для значения по умолчанию std: : deque, данный пример допустимо использовать в качестве иллюстрации общих возможностей шаблонных параметров шаблонов.
5.4. Шаблонные параметры шаблонов 73 return elems.empty(); // если стек пуст } }; Отличие заключается в том, что второй параметр шаблона объявляется как шаблон класса: template <typename ELEM> class CONT Значение по умолчанию изменяется и вместо std: : deque<T> становится std: : deque. Параметр представляет собой шаблон класса, инстанцируемый для типа, передаваемого в качестве первого параметра шаблона. CONT<T> elems; То, что в приведенном выше коде первый параметр шаблона применяется для инстанци- рования второго параметра шаблона,— особенность данного примера, но отнюдь не правило. В общем случае можно сгенерировать шаблонный параметр шаблона, используя в шаблоне класса любой тип. Как обычно, вместо ключевого слова typename для параметров шаблона можно применять ключевое слово class. Однако CONT используется для определения класса и должен объявляться с помощью ключевого слова class. Таким образом, приведенный ниже код также является вполне корректным. template <typename T, template <class ELEM> class CONT = std::deque> // ВЕРНО class Stack { }; А следующий — нет. template <typename T, template<typename ELEM> typename CONT = std::deque> // ОШИБКА class Stack { }; Поскольку параметр шаблона шаблонного параметра шаблона (правда, страшно звучит? :-)) не используется, его имя можно опустить. template <typename Т, template <typename> class CONT = std::deque > class Stack { }; Соответственно нужно модифицировать и функции-члены. Так, если второй параметр шаблона задается как шаблонный параметр, это следует учитывать и в реализации функций-членов. Реализация функции-члена push (), например, будет такой, как показано далее.
74 Глава 5. Основы работы с шаблонами template <typename Т, template <typename> class CONT> void Stack<T,CONT>::push(T const& elem) { elems.push_back(elem); // Добавление элементов } ^ Для шаблонов функций шаблонные параметры не допускаются. Соответствие шаблонных аргументов шаблонов Если попытаться использовать новую версию Stack, то будет выдано сообщение об ошибке, информирующее, что значение по умолчанию std: :deque несовместимо с шаблонным параметром шаблона CONT. Проблема заключается в том, что шаблонный аргумент шаблона должен представлять собой шаблон с параметрами, точно соответствующими параметрам шаблонного параметра шаблона, который этот аргумент замещает. Значения по умолчанию для шаблонных аргументов шаблона во внимание не принимаются, так что нельзя добиться точного соответствия, просто опустив аргументы, которые имеют значения по умолчанию. Проблема в данном случае заключается в том, что на самом деле шаблон std: :deque в стандартной библиотеке имеет несколько параметров. Его второй параметр (который задает так называемый распределитель памяти (allocator)) имеет значение по умолчанию, однако при сопоставлении std: :deque с параметром CONT это значение во внимание не принимается. Как обычно, существует обходной путь. Можно переписать объявление класса так, чтобы предусмотреть для параметра CONT контейнеры с двумя шаблонными параметрами. template <typename Т, template <typename ELEM, typename ALLOC = std::allocator<ELEM> > class CONT = std::deque> class Stack { private: CONT<T> elems; // Элементы }; ALLOC в реализации также можно опустить, поскольку этот параметр нами не используется. Окончательная версия нашего шаблона Stack (включая шаблоны-члены для присвоения стеков с разными типами элементов) приведена ниже. // basics/stack8.hpp #ifndef STACK__HPP #define STACK_HPP #include <deque> #include <stdexcept>
5.4. Шаблонные параметры шаблонов 75 #include <allocator> template <typename T, template <typename ELEM, typename = std::allocator<ELEM> > class CONT = 3td::deque> class Stack { private: CONT<T> elems; // Элементы public: void push(T const&); // Добавление элемента void pop(); // Снятие элемента T top() const; // Возврат элемента //с вершины стека bool empty() const { // Возвращается true, return elems.empty(); // если стек пуст } // Присвоение стека элементов типа Т2 template <typename T2> template<typename ELEM2, typename = std::allocator<ELEM2> > class C0NT2> Stack<T,CONT>& pperator= (Stack<T2,CONT2> constfc); }; template <typename T> template <typename,typename> class CONT> void Stack<T,CONT>::push(T constfc elem) { elems.push_back(elem); // Добавление элементов } template <typename T, template <typename,typename> class CONT> void Stack<T,CONT>::pop() { if (elems.empty()) { throw std::out_of_range("Stacko::" "popO : empty stack") ; } elems.pop_back(); // Удаление последнего элемента } template <typename T, template <typename,typename> class CONT>
76 Глава 5. Основы работы с шаблонами Т Stack<T,CONT>::top() const { if (elems.empty()) { throw std: : out_of_range ("Stacko: :" 11 top () : empty stack") ; } return elems.backO; // Возвращение копии // последнего элемента } template <typename T, template <typename,typename> class CONT> template <typename T2, template <typename,typename> class C0NT2> Stack<T,Cont>& Stack<T,CONT>::operator = (Stack<T2/CONT2> const& op2) { if ((void*)this == (void*)&op2) { // Присвоение return *this; // самому себе? } Stack<T2> tmp(op2); // Создание копии // присваиваемого стека elems.clear(); // Удаление существующих // элементов while(!tmp.empty()) { // Копирование всех elems.push_front(tmp.top()); // элементов tmp.popO ; } return *this; } #endif // STACK_HPP И наконец, в приведенной ниже демонстрационной программе используются все возможности окончательной версии шаблона Stack. // basics/stack8test.hpp #include <iostream> #include <string> #include <cstdlib> #include <vector> #include "stack8.hpp" int main() {
5.4. Шаблонные параметры шаблонов 77 } try { Stack<int> intStack; // Стек целочисленных // значений Stack<float> floatStack; // Стек значений //с плавающей точкой // Работа со стеком целочисленных значений intStack.push(42); intStack.push(7); // Работа со стеком значений с пдавающей точкой floatStack.push(7.7); // Присвоение стеков с разными типами floatStack = intStack; // Вывод стека float std::cout « floatStack.top() « std::endl; floatStack.popO ; std::cout « floatStack.top() « std::endl; floatStack.popO ; std::cout « floatStack.top() « std::endl; } catch (std::exception const& ex) { std: :cerr«"Exception: " «ex.what () «std: :endl; } // Стек целочисленных значений, в котором в качестве // внутреннего контейнера используется вектор Stack<int,std::vector> vStack; vStack.push(42) ; vStack.push(7); std::cout « vStack.topO « std::endl; vStack.popO ; Программа дает следующий вывод: 7 42 Exception: Stacko: :top() : empty stack 7 Заметим, что шаблонные параметры шаблонов — одна из наиболее современных функциональных возможностей, которую должны обеспечивать компиляторы, соответствующие стандарту. Таким образом, приведенная выше программа является хорошим тестом того, насколько ваш компилятор отвечает современным требованиям в области шаблонов.
78 Глава 5. Основы работы с шаблонами Продолжение обсуждения данной темы и примеры шаблонных параметров шаблонов вы найдете в разделах 8.2.3, стр. 126, и 15.1.6, стр. 287. 5.5. Инициализация нулем -^ Для базовых типов данных, таких, как int, double или указатели, не существует стандартного конструктора, который инициализировал бы эти величины каким-либо полезным значением по умолчанию. Напротив, каждая неинициализированная локальная переменная имеет неопределенное значение. void fоо() { int x; // Значение х не определено int* ptr; // ptr указывает неизвестно на что // (но не в никуда!) } Теперь допустим, что мы пишем шаблоны и хотим иметь переменные типа шаблона, инициализированные значением по умолчанию. Тогда у нас возникает проблема: ведь с помощью простого определения для встроенных типов этого сделать нельзя. template <typename T> void fоо() { Т х; // Значение х не определено, // если Т — встроенный тип } По этой причине для встроенных типов можно явно вызывать стандартный конструктор, который инициализирует их нулем (или значением false для величин типа bool), т.е. int () дает нуль. Следовательно, можно обеспечить соответствующую инициализацию по умолчанию даже для встроенных типов. Для этого нужно использовать приведенный ниже код. template <typename T> void foo() { Т х = Т(); // Значение х равно 0 (или false), // если Т — встроенный тип } v Для гарантии того, что член шаблонного класса, имеющий параметризованный тип, будет инициализирован, следует определить конструктор по умолчанию, который использует список инициализации членов класса. template <typename T> class MyClass { private: T x;
5.6. Использование строковых литералов в качестве аргументов шаблонов функций 79 public: MyClassO : х() { // Гарантируется, что х // будет проинициализирован // даже для встроенных типов } }; 5.6. Использование строковых литералов в качестве аргументов шаблонов функций < В том случае, когда шаблон функции имеет параметры ссылочного типа, передача аргументов, являющихся строковыми литералами, может вызвать неожиданные ошибки при работе программ. Рассмотрим пример. // basics/max5.hpp #include <string> // Обратите внимание на ссылочные параметры inline T const& max(T const& a, T const& b) { return a < b ? b : a; } int main() { std: -.string s; ::max("apple","peach"); // ВЕРНО: тип одинаков ::max("apple","tomato"); // ОШИБКА: типы разные ::max("apple",s); // ОШИБКА: типы разные } Проблема заключается в том, что в зависимости от длины строковые литералы имеют разные типы, т.е. являются разными массивами. Другими словами, "apple" и "peach" имеют тип char const [6], в то время как "tomato"— тип char const [7]. Корректным является только первый вызов функции, поскольку шаблон предполагает, что оба параметра имеют одинаковый тип. Однако если будут объявлены параметры не ссылочного типа, то вместо них можно подставить строковые литералы разного размера. // basics.тахб.срр #include <string> // Обратите внимание: параметры не ссылочного типа
80 Глава 5. Основы работы с шаблонами template <typename T> inline T max(T a, T b) / { return a < b ? b : а; } int main () { std::string s; ::max("apple","peach"); // ВЕРНО: тип одинаков ::max("apple","tomato"); // ВЕРНО: сведение массивов //до одинаковых типов ::max("apple", s) ; // ОШИБКА: типы разные } Объясняется это следующим образом: в процессе вывода аргументов преобразование из массива в указатель (часто называемое сведением (decay)) происходит только в том случае, если параметр имеет не ссылочный тип. Это показано на примере приведенной ниже программы. // basics/refnoref.срр #include <typeinfo> #include <iostream> template <typename T> void ref(T constfc x) { std::cout « "x in ref(T const&): " « typeid(x).name() « '\n'; } template <typename T> void nonref(T x) { std::cout « "x in nonref(T): " « typeid(x).name() « '\n'; } int main() { ref("hello"); nonref("hello"); } В данном примере аргумент, представляющий собой строковый литерал, передается шаблонам функций, параметры которых объявлены как параметры ссылочного и не ссы-
5.6. Использование строковых литералов в качестве аргументов шаблонов функций 81 лочного типов соответственно. В обоих шаблонах функции для вывода на экран информации о типах сгенерированных экземпляров параметров используется оператор type id, который возвращает lvalue типа std:: type_info; это значение инкапсулирует представление типа выражения, передаваемого оператору type id. Функция-член name () класса std: : type_infо предназначена для возвращения понятного человеку текстового представления этого типа. На самом деле в стандарте C++ не сказано, что функция name () должна возвращать что-либо осмысленное, но в хороших реализациях C++ вы должны получить строку, которая содержит описание типа выражения, переданного type id (в некоторых реализациях эта строка возвращается во внутреннем кодированном (mangled) представлении, однако существуют средства для преобразования такой строки в форму, понятную человеку). Например, вывод программь^может иметь следующий вид: х in ref(T const&): char[6] x in nonref(T): const char* Проблема несоответствия между массивом символов и указателем на символы, если вам придется с ней столкнуться, может оказаться неожиданным препятствием в работе3. К сожалению, универсального способа решения этой проблемы не существует. В зависимости от контекста, можно прибегнуть к одному из перечисленных ниже способов. • Использование вместо ссылок значений, не являющихся таковыми (однако это может повлечь за собой лишнее копирование). • Перегрузка, позволяющая использовать как параметры ссылочного типа, так и параметры не ссылочного типа (однако это может привести к неоднозначности (см. раздел Б.2.2, стр. 510). • Перегрузка для конкретных типов данных (таких, как std: : string). • Перегрузка для массивов, например: template <typename Т, int N, int M> T const* max (T const (&a)[N] , T const (&b)[M]) { return a < b ? b : a; } • Использование прикладными программистами явного преобразования типов. Для данного примера лучше всего подходит перегрузка max () для строк (см. раздел 2.4, стр. 37). Это необходимо в любом случае, поскольку без перегрузки при вызове ntax () для строковых литералов будет происходить сравнение указателей: сравнение а < Ь означает сравнение адресов двух строковых литералов, что ничего не дает в пла- Именно по этой причине средствами первоначальной стандартной библиотеки C++ нельзя было создать пару значений, инициализируемую строковыми литералами: std::make_pair("key","value"); // ОШИБКА согласно [31] Эта ошибка была исправлена в первом списке технических опечаток посредством замены параметров ссылочного типа в make_pair параметрами не ссылочного типа [32].
82 Глава 5. Основы работы с шаблонами не упорядочения по алфавиту. Это еще одна причина, по которой в большинстве случаев следует отдавать предпочтение строковому классу наподобие std:: string перед строками в С-стиле. Более подробно этот вопрос рассматривается в разделе 11.1, стр. 193. 5.7. Резюме • Для обращения к имени типа, которое зависит от параметра шаблона, следует предварить его ключевым словом typename. • Вложенные классы и функции-члены также могут быть шаблонами. Одним из применений этой возможности является реализация обобщенных операций с преобразованиями внутренних типов (проверка типов при этом не устраняется). • Шаблонные версии операторов присвоения не заменяют операторы присвоения по умолчанию. • Шаблоны классов можно использовать в качестве параметров шаблона—это так называемые шаблонные параметры шаблонов. • Для шаблонных параметров шаблонов должно выполняться точное соответствие. Значения аргументов по умолчанию для шаблонных параметров шаблона игнорируются. • Явный вызов конструктора по умолчанию гарантирует инициализацию переменных и членов шаблонов значением по умолчанию, даже если эти шаблоны ин- станцируются со встроенными типами. • Для строковых литералов преобразование массива в указатель в процессе вывода аргументов имеет место тогда и только тогда, когда параметры не являются ссылками.
Глава 6 Применение шаблонов на практике Код шаблонов имеет некоторые отличия от обычного кода. В грубом приближении шаблоны можно поместить где-то между макросами и обычными (нешаблонными) объявлениями. Это, разумеется, не более чем упрощение, но оно имеет следствия не только для способов написания алгоритмов и структур данных, в которых используются шаблоны, но и для логики представления и анализа программ, включающих шаблоны. В данной главе рассмотрим некоторые практические аспекты применения шаблонов, не углубляясь в технические детали, которые лежат в их основе (многие из них описаны в главе 10, "Инстанцирование"). В целях упрощения будем исходить из предположения, что системы компиляции C++ содержат достаточно традиционные компиляторы и компоновщики (системы C++, которые не относятся к этой категории, встречаются крайне редко). 6.1. Модель включения Существует несколько способов Организации исходного кода шаблонов. В данном разделе представлен наиболее популярный на момент написания этой книги подход — модель включения. 6.1.1. Ошибки при компоновке Большинство программистов, работающих на С и C++, как правило, предпочитают следующий способ организации нешаблонного кода: • классы и другие типы полностью помещаются в заголовочные файлы; обычно это файлы с такими расширениями, как . hpp (или . Н, . h, . hh, . hxx); • при использовании глобальных переменных и (невстраиваемых) функций в заголовочный файл помещаются только объявления, а определения — в так называемый .С-файл; это обычно файлы с расширением . срр (.С, .с, . ее, .схх). Описанная схема хорошо себя зарекомендовала на практике: во-первых, при ее использовании необходимые определения типов легко доступны всей программе, а во-
84 Глава 6. Применение шаблонов на практике вторых, она позволяет исключить ошибку дублирования определений переменных и функций компоновщиком. Рассмотрим распространенную ошибку, которую часто допускают программисты, начинающие работать с шаблонами. Эта ошибка проиллюстрирована с помощью приведенного ниже (ошибочного) программного кода. Шаблон, как это часто делается для "обычного кода", объявляется в заголовочном файле. // basics/myfirst.hpp #ifndef MYFIRSTJHPP- • ч #define MYFIRSTLHPP // Объявление шаблона template <typename T> void print_typeof (T constfc); #endif // MYFIRSTLHPP Здесь print_typeof () представляет собой объявление простой вспомогательной функции, предназначенной для вывода некоторой информации. Реализация этой функции помещается в .С-файл. // basics/myfirst.cpp #include <iostream> #include <typeinfo> #include "myfirst.hpp" // Реализация/определение шаблона template <typename T> void print__typeof (T constfc x) ; { std::cout « typeid(x).name() « std::endl; } В этом примере для вывода строки описания типа передаваемого выражения используется оператор type id (см. раздел 5.6, стр. 79). И наконец, шаблон используется в другом .С-файле, в который объявление нашего шаблона включено с помощью директивы #include. // basics/myfirstmain.cpp #include "myfirst.hpp" // Использование шаблона int main() { double ice = 3.0; print_typeof(ice); // Вызов шаблона функции // для типа double }
6. 1. Модель включения 85 Компилятор C++, скорее всего, воспримет приведенную программу без замечаний, однако компоновщик вьщаст сообщение об ошибке отсутствия определения функции print_typeof (). Причина этой ошибки в том, что шаблон функции print_typeof () не инстанци- рован. Для того чтобы инстанцировать шаблон, компилятор должен знать, какое именно определение должно быть инстанцировано, и для каких аргументов шаблона. Однако в предыдущем примере два фрагмента информации, о которых идет речь, расположены в разных файлах, компилирующихся отдельно друг от друга. Следовательно, когда наш компилятор видит вызов print_typeof (), но в его поле зрения нет определения для инстанцирования, он обоснованно предполагает, что где-то такое определение есть, и создает ссылку на это определение (разрешить которую должен компоновщик). С другой стороны, в тот момент, когда компилятор обрабатывает файл myf irst. cpp, у него нет указания, что он должен инстанцировать определение шаблона, которое содержится в этом файле, для каких-то конкретных аргументов. 6.1.2. Шаблоны в заголовочных файлах > Обычно описанная проблема решается с помощью того же подхода, что и применяемый для макросов или для встраиваемых функций. Другими словами, определения шаблона включаются в заголовочный файл, в котором объявляется этот шаблон. Чтобы сделать это для нашего примера, можно добавить в конце файла myf irst. hpp директиву #include "myfirst.cpp" или включить myf irst. cpp в каждый .С-файл, в котором используется шаблон. Существует еще один способ: избавиться от myf irst. cpp и переписать myf irst .hpp так, чтобы он содержал все объявления и определения шаблонов. // basics/myfirst2.hpp #ifndef MYFIRST_HPP #define MYFIRST_HPP #include <iostream> #include <typeinfo> // Объявление шаблона template <typename T> void print_typeof(T constfc); // Реализация/определение шаблона template <typename T> void print_typeof(T const& x) std::cout << typeid(x).name() << std::endl; #endif // MYFIRST__HPP
86 Глава 6. Применение шаблонов на практике Такой способ организации кода с шаблонами получил название модели включения (inclusion model). Легко убедиться, что теперь, после внесения правок, наша программа корректно компилируется, компонуется и выполняется. Здесь следует сделать несколько замечаний. Наиболее существенное из них заключается в том, что при описанном выше подходе увеличивается расход ресурсов из-за включения в код заголовочного файла myfirst.hpp. В данном примере дополнительный расход ресурсов возникает не за счет размера самого определения шаблона, а за счет включения заголовочных файлов, используемых определением нашего шаблона; в данном случае это <iostream> и <typeinf о>. Можно убедиться, что подобные включения оборачиваются десятками тысяч строк кода, поскольку такие заголовочные файлы, как <iostrieam>, содержат свои определения шаблонов. На практике такое увеличение объема кода составляет реальную проблему, поскольку компилятору требуется намного больше времени для компиляции больших программ. Поэтому рассмотрим некоторые возможные способы решения данной проблемы в следующих разделах. Следует заметить: реальные программы, время компиляции и компоновки которых измеряется в часах, отнюдь не редкость (у нас бывали случаи, когда на создание программы из исходного кода могло уйти несколько дней). Несмотря на проблемы с временем компиляции программ, все же рекомендуем читателям, где это возможно, придерживаться описанной модели. Мы исследовали еще две альтернативные модели, но, по нашему мнению, их конструктивные недостатки более серьезны, чем рассмотренная проблема увеличения времени компиляции и компоновки кода (хотя нужно отметить, что альтернативные варианты имеют свои преимущества, непосредственно не относящиеся к конструктивным аспектам разработки программного обеспечения). Другое, более тонкое замечание относительно модели включения состоит в следующем. Существует одно важное отличие невстраиваемых шаблонов функций от встраиваемых функций и макросов: в первом случае развертывания кода в месте вызова не происходит; во втором— при инстанцировании шаблона функции создается ее новая копия. Поскольку это делается автоматически, компилятор может создать две копии функции в двух разных файлах, и некоторые компоновщики при обнаружении двух разных определений одной и той же функции выдадут сообщения об ошибках. Теоретически подобные ситуации не должны вас беспокоить; как справиться с ними — проблема компилятора C++. На практике все, как правило, корректно работает, и вы не должны вникать в эти вопросы. Однако в больших проектах, для которых создаются собственные библиотеки, могут возникать проблемы, справиться с которыми помогут схемы инстан- цирования, рассматриваемые в главе 10, "Инстанцирование", и тщательное изучение документации к вашему компилятору C++. В заключение следует отметить: все, что говорилось относительно обычного шаблона функции в нашем примере, применимо также к функциям-членам и статическим данным-членам, а также к шаблонам функций-членов.
6.2. Явное инстанцирование 87 6.2. Явное инстанцирование Применение модели включения гарантирует, что все необходимые шаблоны будут инстанцированы, поскольку компилятор C++ будет автоматически генерировать эти экземпляры по мере необходимости. Стандарт C++ обеспечивает еще одну возможность — инстанцирование шаблонов вручную с помощью директивы явного инстанцирования. 6.2.1. Пример явного инстанцирования шаблона Для того чтобы проиллюстрировать инстанцирование шаблона вручную, давайте вновь обратимся к нашем(у примеру, который приводил к ошибке при компоновке (см. раздел 6.1.1). Чтобы избежать ошибки, добавим в нашу программу приведенный ниже файл. // basics/myfirstinst.срр #include "myfirst.срр" // Явное инстанцирование print__typeof () // для типа double template void print_typeof<double>(double const&); Директива явного инстанцирования содержит ключевое слово template, за которым следует объявление объекта, экземпляр которого необходимо сгенерировать, с полностью выполненными подстановками. В нашем примере это делается для обычной функции, но явное инстанцирование шаблонов возможно и для функций-членов или статических данных-членов. // Явно инстанцированный конструктор // MyClasso для int template MyClass<int>::MyClass(); // Явное инстанцирование шаблона // функции max() для int template int const& max(int constfc, int const&); Можно также явно инстанцировать шаблон класса, что, по сути, означает инстанцирование всех членов этого класса, для которых оно возможно. При этом исключаются члены класса, которые предварительно были специализированы, а также уже инстанцированные. // Явное инстанцирование класса Stack // для типа int template class Stack<int>; // Явное инстанцирование некоторых функций-членов // класса Stacko для строк template Stack<std::string>::Stack(); template void Stack<std::string>::push(std::string const&); template std::string Stack<std::string>::top();
88 Глава 6. Применение шаблонов на практике // ОШИБКА: невозможно явно инстанцировать // функцию-член класса, который уже // инстанцирован template- Stack<int>::Stack(); В программе должно быть не более одного явного инстанцирования для каждого отдельного типа. Другими словами, можно явно создать экземпляры как для print_typeof <int>, так и для print_typeof <double>, но каждая директива должна быть включена в программу только один раз. В противном случае при компоновке будут выданы сообщения об ошибках дублирования определений. Инстанцирование шаблонов вручную имеет очевидный недостаток: программист должен тщательно следить за тем, какой тип шаблона инстанцируется. Для крупномасштабных проектов это быстро становится слишком обременительным, поэтому использовать данный метод не рекомендуется. Нам приходилось работать над несколькими проектами, где масштабы этой опасности были недооценены, и чем дальше мы продвигались вперед, тем больше сожалели о принятом решении. Однако явное инстанцирование имеет и свои преимущества, поскольку его можно использовать в соответствии с потребностями программы. Очевидно, что при явном ин- станцировании исключается раздувание кода за счет больших заголовочных файлов. Исходный код определений шаблонов может оставаться скрытым, и при этом в клиентской программе не будет лишних инстанцирований шаблонов. И наконец, для некоторых приложений может оказаться полезной возможность контролировать точное местоположение (т.е. объектный файл) экземпляра шаблона. При автоматическом инстанцирований шаблонов это невозможно (более подробно данный вопрос рассматривается в главе 10, "Инстанцирование"). 6.2.2. Сочетание модели включения и явного инстанцирования Чтобы программист мог выбирать, когда использовать модель включения, а когда явное инстанцирование, можно отделить объявления и определения шаблонов, поместив их в два разных файла. На практике эти файлы обычно именуются как заголовочные (с использованием расширений, применяющихся для таких файлов). Таким образом, файл myfirst.cpp из нашего примера получит имя myfirstdef .hpp. На рис. 6.1 это проиллюстрировано на примере шаблона класса Stacko.
6.3. Модель разделения :Д; ' \1 stack,'hpp;, ;"'^; . /'/>;/> ''' /' #ifndef STACK_KPP #define STACK_HPP #include <vector> template<typename T> class Stack { private: std::vector<T> elems; Stack(); void push(T const&); void pop(); | T top() const; }; #endif "' !;'::лч->У:/; \ V^',>;V Sbackdef *'&p£>r>;?'?J1 /-,';-:';^ -/- -/'-''i'♦"'' "''"& #ifndef STACKDEF_HPP #define STACKDEF_HPP #include "stack.hppH tempiate<typename T> void Stack<T>::push(T cost& elera) { elems.push_back(elem); } #endif '", <"' 1 '''■ '' 1 '". "' Рис. 6.1. Разделение объявления и определения шаблона Таким образом, если потребуется использовать модель включения, можно просто включить в код заголовочный файл stackdef .hpp. Если же остановить свой выбор на явном инстанцировании, то будет включен заголовочный файл stack.hpp, а .С-файл обеспечен необходимыми директивами явного инстанцирования (рис. 6.2). 6.3. Модель разделения Оба подхода, описанные в предыдущих разделах, хорошо работают и соответствуют стандарту C++. Однако этот же стандарт обеспечивает альтернативный механизм экспорта шаблонов. Такой подход иногда называют моделью разделения шаблонов C++.
90 Глава 6. Применение шаблонов на практике г$\ \уц \УУМ\ Г У У \/?Р. Щу ' '•У \у 1' У'-- \^'" VH, X'hi* If у. i5H/ «tkckbestl .-capp*:; ^ ;'„:y\\'-' - > Ci У'^-'"-, У /' ~ '">',' '. ^)', -*, У У i'": J ''l/ #include "stack.hpp" #include <iostream> #include <string> int main() . { Stack<int> intStack; intStack.push(42); std::cout « intStack.top() « std::endl; intStadk.pop(); Stack<std::string> stringStack; stringStack.push{"hello"); std::cout << stringStack.top() « std::endl; } :stacXJmst«cpp,r ' .' >r / /Уу /**/.-' -V ■ "y-. \f/ '/[ \ ■/*•" #include "stackdef.hpp" #include <string> // Инстанцирование класса Stacko для int 1 template Stack<int>; // Инстанцирование некоторых функций-членов // Stacko для строк template Stack<std::string>::Stack() ; template void Stack<std::string>::push(std::string template std::string Stack<std::string>::top(); 'ШШУуШУШ^УШШ^уУШуВШШ "",/1,', IV'C;--''' V )УУ;У\ УШуШУУуц l[ /,;У} УУ^^У^у^УУ^''?*''УтЛ \Уууу\-УуУУ:у':\ ! У'У:У};-,';:/,1,у *' ' А "•а 'У/'У ■'' ■■ > '<', ' У; '. '\ 1 Уу '''<'?<•;<// const&); ЩШм У,-У У-' 1 -5 ' ' У^ 1 ' '/ ' ' *'' 1 '.- /г, у-У'УЛ у//\"Уу-'?У\ | 1 ''<""'"'v'*'' У''У ' I \'УУ&:/;'У';л ^'У'АууУЛ x'^'-l'i'jyyi.y'y 1 Р*4?&*у?Щ Рис. 6.2. Явное инстанцирование с использованием двух заголовочных файлов 6.3.1. Ключевое слово export В принципе использовать экспорт очень легко. Для этого нужно определить шаблон только в одном файле и пометить его определение и все объявления, не являющиеся определениями, ключевым словом export. Для примера из предыдущего раздела будет получено следующее объявление шаблона функции: // basics/myfirst3.hpp #ifndef MYFIRST__HPP #define MYFIRST HPP // Объявление шаблона export
6.3. Модель разделения 91 template <typename T> void print_typeof(T const&); #endif // MYFIRST_HPP Экспортируемые шаблоны можно использовать без обязательной видимости их определений. Другими словами, место, где шаблон используется, и место, где он определяется, могут находиться в двух разных единицах трансляции. Сейчас в нашем примере файл myfirst.hpp содержит только объявления функций-членов шаблона класса, и этого вполне достаточно для их использования. По сравнению с первоначальным кодом, который вызывал ошибки при компоновке, для получения хорошо работающего кода понадобилось всего лишь добавить одно ключевое слово —- export. В файле, обрабатываемом препроцессором (т.е. в единице трансляции), с помощью ключевого слова export достаточно пометить только первое объявление шаблона. Последующие повторные объявления, включая определения, будут неявно содержать этот атрибут. Именно поэтому файл myf irst .cpp в нашем примере не нуждается в модификации. Определения в этом файле экспортируются неявно, поскольку они были объявлены таковыми в заголовочном файле, включенном с помощью #include. С другой стороны, вполне допустимо указывать ключевые слова export и в определениях шаблона — это улучшит удобочитаемость кода. Ключевое слово export применимо к шаблонам функций, функциям-членам шаблонов классов, шаблонам функций-членов и статическим данным-членам шаблонов классов; его также можно использовать для объявлений шаблонов классов. В последнем случае это означает, что будет экспортирован каждый член класса, который может быть экспортированным; однако сами шаблоны классов не экспортируются (следовательно, их определения по-прежнему должны оставаться в заголовочных файлах). В этом случае встраиваемые функции-члены можно объявлять как явно, так и неявно, однако эти встраиваемые функции не экспортируются. export template <typename T> class MyClass { public: void memfunl(); // Экспортируется void memfun2() { // He экспортируется, ... // поскольку является неявно ... // встраиваемой } void memfun3(); //He экспортируется, // поскольку является явно // встраиваемой } ; template <typename T>
92 Глава 6. Применение шаблонов на практике inline void MyClass<T>::memfun3() { } Заметим, что ключевое слово export нельзя сочетать с inline и оно должно предшествовать ключевому слову template. Приведенный ниже код некорректен. template <typename T> class Invalid { public: export void wrong(T); // ОШИБКА: за export не // следует template }; export template <typename T> // ОШИБКА: export inline void Invalid<T>::wrong(T) // и inline одновременно { } export inline T const& max (T constfc, T const& b) { // ОШИБКА: export и inline одновременно return a < b ? b : a; } 6.3.2. Ограничения модели разделения Наступил момент, когда читатель вправе спросить: почему же авторы так горячо пропагандировали модель включения, если существует такое волшебное средство, как экспорт шаблонов? Тому существует несколько объяснений. Во-первых, даже по прошествии четырех лет после принятия стандарта только одна компания смогла реально реализовать поддержку ключевого слова export . Следовательно, опыт работы с этим ключевым словом не получил такого широкого распространения, как в случае других средств языка C++. Кстати, это означает еще и то, что все наши выводы основаны только на небольших крупицах опыта. Во-вторых, хотя экспорт может показаться почти волшебным средством, в действительности это средство панацеей от всех бед не является. В конечном счете в процессе инстанцирования шаблона задействовано как место, где генерируется экземпляр шаблона, так и место, где находится его определение. Следовательно, хотя на первый взгляд оба места в исходном коде разнесены, тем не менее между ними существует невидимая связь, которую система создает "за кулисами". Это означает, например, следующее: если файл, содержащий определение, изменяется, то должны быть перекомпилированы как Насколько нам известно, это Edison Design Group, Inc. (EDG) [14]. Кстати, их технология доступна через продукты других производителей.
6.3. Модель разделения 93 сам измененный файл, так и все файлы, связанные ним. По существу, получена почти та же модель включения, хотя она не так очевидно просматривается в исходном коде. В результате становится малоприменим традиционный инструментарий типа make и nmake. Это также означает, что для надежного учета ведения всей "бухгалтерии", связанной с зависимостями между исходными файлами, потребуется дополнительная работа компилятора, и в конечном итоге может оказаться, что общее время, необходимое для компиляции и компоновки, окажется не меньше, чем в модели включения. И наконец, использование экспортируемых шаблонов может привести к неожиданным семантическим последствиям (которые подробно рассмотрены в главе 10, "Инстанцирование"). Существует широко распространенное заблуждение, которое заключается в том, что механизм export обеспечивает возможность поставки библиотек шаблонов без открытия исходного кода с их определениями (как в случае библиотек объектов, не являющихся шаблонами)2. Это заблуждение в том плане, что сокрытие кода не является языковым вопросом: с равным успехом можно реализовать как механизм сокрытия включаемых определений шаблонов, так и механизм сокрытия экспортируемых определений шаблонов. Такая возможность потенциально осуществима, хотя текущие реализации и не поддерживают эту модель. Однако при ее реализации неизбежно возникновение новых проблем с ошибками компиляции, которые будут ссылаться на скрытый исходный код. 6.3.3. Составление программы для модели разделения Основная идея — подготовить исходный код программы таким образом, чтобы можно было легко переключаться между моделью включения и моделью экспорта, используя небольшое количество директив препроцессора. Ниже показано, как это можно сделать в нашем простом примере. // basics/myfirst4.hpp #ifndef MYFIRSTJHPP ttdefine MYFIRST_HPP // Если определено USEJEXPORT, используется export #if defined(USE_EXPORT) #define EXPORT export #else #define EXPORT #endif // Объявление шаблона EXPORT template <typename T> Заметим, что далеко не все считают закрытость исходного кода преимуществом.
94 Глава 6. Применение шаблонов на практике void print_typeof (T const&) ; // Включение определений, если USE_EXPORT не задано #if !defined(USE_EXPORT) #include "myfirst.cpp" #endif #endif // MYFIRST_HPP Теперь можно выбирать между двумя моделями путем определения или пропуска символа USE_EXPORT. Если определение USE_EXPORT в программе предшествует myf irst. hpp, используется модель разделения. // Использование модели разделения #define USE_EXPORT #include "myfirst.hpp" Если в программе не определен символ USE_EXPORT, используется модель включения, поскольку в этом случае в myfirst.hpp автоматически включаются определения из myfirst.cpp. // Использование модели включения #include "myfirst.hpp" Код получился достаточно гибким, однако еще раз подчеркнем, что эти две модели, помимо очевидных логических различий, имеют и тонкие семантические различия. Заметим, что можно также явно инстанцировать экспортируемые шаблоны. В этом случае определение шаблона может находиться в другом файле. Чтобы иметь возможность выбора между моделью включения, моделью разделения и явным инстанцирова- нием шаблонов, можно сочетать организацию кода, управляемого с помощью USE__EXPORT, с соглашениями, описанными в разделе 6.2.2. 6.4. Шаблоны и inline Чтобы сократить время выполнения программ, небольшие по размерам функции обычно объявляются как встраиваемые. Спецификатор inline указывает, что в месте вызова функции следует отдавать предпочтение встраиванию тела функции, а не механизму обычного вызова. Однако выполнять такую встроенную подстановку в месте вызова компилятор не обязан. Как шаблоны функций, так и встраиваемые функции могут быть определены в нескольких единицах трансляции. Обычно это делается путем помещения определения в заголовочный файл, который включается несколькими .С-файлами. На основе сказанного выше может сложиться впечатление, что шаблоны функций являются встраиваемыми по умолчанию, однако это не так. При написании шаблонов
6.5. Предварительно откомпилированные заголовочные файлы 95 функций, которые должны обрабатываться как встраиваемые, необходимо явно использовать спецификатор inline (если только функция уже не является встраиваемой, поскольку ее определение находится внутри объявления класса). Следовательно, многие небольшие по размерам шаблоны функций, которые не являются частью определения класса, следует объявлять с помощью inline3. 6.5. Предварительно откомпилированные заголовочные файлы Заголовочные файлы C++ могут достигать больших размеров даже без использования шаблонов, вследствие чего их компиляция занимает много времени. Применение шаблонов еще более усугубляет Ситуацию, поэтому для того, чтобы удовлетворить требования программистов ко времени компиляции, производители программного обеспечения во многих случаях обеспечивают возможность работы с так называемыми предварительно откомпилированными заголовочными файлами. Эта модель не включена в стандарт и зависит от конкретного производителя. Подробное описание создания и использования предварительно откомпилированных заголовочных файлов читатель может найти в документации к различным компиляторам C++, которые поддерживают эту возможность; тем не менее полезно иметь представление о том, как работает это средство. При компиляции файла компилятор начинает (^начала файла и проходит его до конца. При обработке каждой лексемы файла (которые могут поступать и из файлов, включенных с помощью директивы #include) компилятор изменяет свое внутреннее состояние, например добавляя записи в таблицу символов. По окончании этой работы компилятор может генерировать код в объектных файлах. В основе механизма использования предварительно откомпилированных заголовочных файлов лежит следующее соображение: код можно организовать таким образом, чтобы несколько файлов начинались с одних и тех же строк кода. Теперь предположим, что все файлы, которые требуется откомпилировать, начинаются с одних и тех же N строк кода. Тогда можно откомпилировать первые N строк и полностью сохранить состояние компилятора в этот момент в так называемом предварительно откомпилированном заголовочном файле. Таким образом, при компиляции каждого файла в программе можно повторно загрузить сохраненное состояние откомпилированного кода и начать его компиляцию со строки N+1. Дело в том, что операция повторной загрузки сохраненного состояния выполняется на несколько порядков быстрее, чем реальная компиляция первых N строк кода, однако, как правило, требует большего расхода машинных ресурсов, чем простая компиляция. Это увеличение по грубым оценкам составляет от 20 до 200%. Для повышения эффективности использования предварительно откомпилированных заголовочных файлов необходимо добиться того, чтобы подлежащие компиляции файлы В дальнейшем в книге это правило применяется не всегда, поскольку это может уводить нас от рассматриваемой темы.
96 Глава 6. Применение шаблонов на практике по возможности начинались с максимального количества одинаковых строк кода. На практике это означает, что файлы должны начинаться с одних и тех же директив #include — именно они потребляют значительную часть времени компиляции. Следовательно, необходимо обратить особое внимание на порядок включения заголовочных файлов. Например, для файлов #include <iostream> #include <vector> #include <list> и #include <list> #include <vector> не имеет смысла использовать предварительно откомпилированные заголовочные файлы, поскольку исходный код файлов начинается неодинаково. Некоторые программисты считают, что лучше подключить с помощью #include несколько дополнительных ненужных заголовочных фалов, чем отказаться от возможности ускорения компиляции за счет предварительно скомпилированного заголовочного файла. Такой подход значительно упрощает стратегию включения кода. Например, обычно достаточно просто создать заголовочный файл с именем std. hpp, включающий все стандартные заголовочные файлы4. #include <iostream> #include <string> #include <vector> #include <deque> #include <list> Такой файл предварительно компилируется, после чего каждую программу, в которой используется стандартная библиотека, можно просто начинать с директивы #include "std.hpp" Обычно на включение такого файла при компиляции требуется немалое время, однако, если в системе достаточно памяти, механизм использования предварительно откомпилированных заголовочных файлов обеспечивает значительно более быструю обработку кода в сравнении с включением каждого отдельного стандартного заголовочного файла без предварительной компиляции. Особенно хорошо для этого подходят стандартные заголовочные файлы, поскольку они редко изменяются, а следовательно, предварительно Теоретически стандартные заголовочные файлы не обязательно должны соответствовать реальным физическим файлам. На практике, однако, это так, причем такие файлы очень велики по размерам.
6.5. Предварительно откомпилированные заголовочные файлы 97 откомпилированный заголовочный файл для нашего файла std.hpp можно создать один раз5. Предварительно откомпилированные заголовочные файлы обычно являются частью конфигурации зависимостей проекта (например, при необходимости их обновление обеспечивается программой make). Один из привлекательных подходов к использованию предварительно скомпилированных заголовочных файлов заключается в создании уровней предварительно скомпилированных заголовочных файлов. Такие уровни начинаются с наиболее широко используемых и стабильных заголовочных файлов (например, это может быть наш std. hpp) и заканчиваются заголовочными файлами, которые предположительно какое-то время будут оставаться неизменными и поэтому их предварительная компиляция имеет смысл. Когда заголовочные файлы находятся в стадии интенсивной разработки, создание предварительно откомпилированного файла может занимать больше времени, чем составляет экономия за счет его повторного использования. Ключевой момент этого подхода заключается в том, что предварительно откомпилированный заголовочный файл для более стабильного уровня может быть повторно использован в целях улучшения времени предварительной компиляции менее стабильного заголовочного файла. Например, предположим, что, помимо нашего заголовочного файла std.hpp (предварительно откомпилированного), создается еще один заголовочный файл— core.hpp, включающий дополнительные возможности, специфические для нашего проекта, но при этом имеющий определенный уровень стабильности. #include "std.hpp" ttinclude "core_data.hpp" #include "core__algos.hpp" Поскольку приведенный выше файл начинается с #include " std.hpp", компилятор может загрузить предварительно откомпилированный заголовочный файл и продолжать работу со следующей строки без повторной компиляции всех стандартных заголовочных файлов. Когда файл будет полностью обработан, создается новый предварительно откомпилированный заголовочный файл. После этого в приложениях можно будет использовать #include "core.hpp", что обеспечит быстрый доступ к большему количеству функциональных возможностей, поскольку компилятор может загружать последний предварительно откомпилированный заголовочный файл. Некоторые члены комитета по стандарту C++ считают концепцию современного заголовочного файла std.hpp настолько удобной, что предложили ввести его в стандарт, поэтому не исключено, что мы получим возможность писать #include <std>. Предлагается даже неявное включение этого файла, чтобы все возможности стандартной библиотеки были доступны даже без Директив # include.
98 Глава 6. Применение шаблонов на практике 6.6. Отладка шаблонов При отладке шаблонов возникают два вида проблем. Первый — это проблемы авторов шаблонов: как удостовериться, что создаваемый шаблон будет функционировать для любых аргументов, удовлетворяющих условиям, которые документированы его создателем? Второй— проблемы другой заинтересованной стороны: как может пользователь шаблона выявить, какие из требований к параметрам шаблона нарушены, если шаблон функционирует не так, как указано в документации? Прежде чем подробно рассматривать этот вопрос, целесообразно обсудить, какого рода ограничения могут накладываться на параметры шаблонов. Данный раздел в основном посвящен ограничениям, несоблюдение которых приводит к ошибкам компиляции. Будем называть их синтаксическими ограничениями. Синтаксические ограничения — это, например, потребность в наличии конструктора определенного типа, требование, чтобы определенный вызов функции не был неоднозначным, и т.д. Ограничения другого типа будем называть семантическими ограничениями. Эти ограничения поддаются механической проверке намного труднее. В общем случае делать это даже нецелесообразно. Например, можно требовать, чтобы для параметра типа шаблона был определен оператор < (что является синтаксическим ограничением), но обычно требуется, чтобы этот оператор определял некоторый вид упорядочения в своей области определения (что является семантическим ограничением). Для обозначения набора ограничений, который постоянно повторяется для библиотеки шаблонов, часто используется термин концепция. Например, стандартная библиотека C++ базируется на таких концепциях, как итератор с произвольным доступом (random access iterator) и конструируемый по умолчанию (default constructible). Концепции могут образовывать иерархии в том смысле, что одна концепция может быть усовершенствованием другой. Более совершенная концепция включает все ограничения базовой концепции с добавлением некоторых новых. Например, концепция итератора с произвольным доступом является усовершенствованием концепции двунаправленного итератора (bidirectional iterator) в стандартной библиотеке C++. Используя данную терминологию, можно сказать, что отладка кода шаблонов в значительной степени сводится к определению того, как нарушаются концепции в реализации шаблона и при его использовании. 6.6.1. Дешифровка ошибок-романов Текст обычных сообщений об ошибках компиляции, как правило, является лаконичным и отражающим суть ошибки. Например, когда компилятор говорит "class X has no member ' fun'" (в классе X отсутствует член "fun"), то обычно не составляет труда определить, что именно неверно в вашем коде (например, вы ошибочно ввели "run" вместо "fun"). В случае шаблонов это не так. Рассмотрим относительно простой фрагмент кода, в котором задействована стандартная библиотека C++. Он содержит небольшую ошибку: используется list<string>, но поиск проводится с помощью объекта- функции greater<int>, а не greater<string>:
6.6. Отладка шаблонов 99 std::list<std::string> coll // Поиск первого элемента, большего "А" std::list<std::string>::iterator pos; pos = std::find_if( coll.begin(),coll.end(), // Диапазон поиска std::bind2nd(std::greater<int>(),nA"));// Критерий поиска Такого рода ошибки часто случаются, когда программист вырезает и вставляет код, но забывает внести в него необходимые изменения. Одна из версий популярного компилятора GNU C++ выдает при этом приведенное ниже сообщение об ошибке. /local/include/stl/_algo.h: In function 'struct _STL::_Lis t_iterator<_STL::basic_string<char,_STL::char_traits<char>/ __STL: :allocator<char> >,_STL: :_Nonconst__traits<_.STL: :basic_ string<char,_STL::char_traits<char>,_STL::allocator<char> > > >_STL::find_if<_STL:: _List_.it era tor<_STL::basic_string<c har,_STL::char_traits<char>,_STL::allocator<char> >,_STL::_ Nonconst_traits<_STL: :basic_string<char,_STL: : char_.traits<c har>,_.STL: :allocator<char> > > >, _STL: :binder2nd<_STL: :gre ater<int> > > (_STL: :_List_iterator<_.STL: :basic_string<char, _STL::char_traits<char>,_STL::allocator<char> >,_STL::_Nonc onst_traits<_STL::basic_string<char,_STL::char_traits<char> ,_STL::allocator<char> > > >,_STL::_List_iterator<_STL::bas ic_string<char,_STL::char_traits<char>,_.STL::allocator<char > >,_STL::_Nonconst_.traits<_.STL::basic_string<char,_STL::ch ar_.traits<char>,_JSTL: :allocator<char> > > >,_STL::birider2nd <_STL: :greater<int> >/__STL: : input_.it erator_tag) ' :/local/inc lude/stl/_algo.h:115: instantiated from '_STL::find_if<_STL ::_List_Iterator<_.STL::basic_string<char,_STL::char_traits<i char>,_STL::allocator<char> >,_STL::_.Nonconst_.traits<_STL:: basic_string<char,_STL::char_.traits<char>,_.STL::allocator<c har> > > >, _STL::binder2nd<_STL::greater<int> > >(_STL::_L ist_iterator<_STL: :basic_string<char,_STL: : char_.traits<char >,_STL: :allocator<char> >,_STL: :Nonconst_traits<_STL: -.basic _string<char,_STL::char_traits<char>,_STL:: allocator<char> > > >,_STL::List_iterator<STL::basic_string<char,_STL::cha r_traits<char>,_STL:: allocator<char> >,_STL::_Nonconst_tra its<_STL::basic_string<char,_STL::char_traits<char>,_.STL::a llocator<char> > > >_STL::binder2nd<_STL::greater<int> >)'t estprog.cpp:18: instantiated from here/local/include/stl/_a lgo.h:78: no match for call to •(_STL::binder2nd<_STL::grea ter<int> >) (_STL::basic_string<char,_STL::char_traits<char >,_STL::allocator<char> > &)■/local/include/stl/_function.h :261: candidates are: bool _STL::binder2nd<_STL::greater<in t> >::operator ()(const int &) const
100 Глава 6. Применение шаблонов на практике Такое сообщение на первый взгляд больше смахивает на роман, чем на диагностическое сообщение, и одним своим видом способно полностью деморализовать новичков в области шаблонов. Однако сообщения, подобные приведенному, при наличии некоторой практики поддаются пониманию, и местонахождение ошибок можно легко определить. В первой части нашего сообщения говорится, что ошибка произошла в экземпляре шаблона функции (с ужасно длинным именем), запрятанном глубоко внутри заголовочного файла / local /include/st 1 /_algo .h. Далее компилятор сообщает, почему он сгенерировал этот конкретный экземпляр шаблона. В данном случае "отсчет" начинается со строки 18 файла testprog.cpp (это файл, содержащий код нашего примера), в которой вызывается генерация экземпляра шаблона f ind_if из строки 115 заголовочного файла _algo. h. Компилятор сообщает все это для того, чтобы вы знали, что такие-то экземпляры шаблонов сгенерированы, и смогли восстановить цепочку событий, которые вызвали генерацию экземпляров шаблона. Однако в случае нашего примера есть основания полагать, что должны быть инстанциро- ваны все шаблоны. Но почему же тогда программа не работает? Ответ на этот вопрос содержится в последней части сообщения, там, где говорится "no match for call" — это означает, что вызов функции не может быть сгенерирован из-за несоответствия типов аргументов и параметров. Более того, сразу же за этим в строке, содержащей "candidates are", поясняется, что единственным типом-кандидатом является целочисленный тип (тип параметра— const int&). Вернувшись назад, к строке 18, вы увидите std: :bind2nd (std: :greater<int>(), "A") — строка действительно содержит целочисленный тип (int), а он несовместим с объектами строкового типа, поиск которых проводится в нашем >- примере. Стоит заменить <int> на std:: string — и проблема будет решена. Нет сомнений в том, что сообщение об ошибке можно было структурировать получше. Ничто не мешает опустить описание проблемы перед историей инстанцирования шаблонов, а вместо развернутых имен наподобие "MyTemplate<YourTemplate<int> >" выводить структурированные описания, например "MyTemplate<T>", где Т = YourTemplate<int>, чтобы сократить чрезмерно длинные имена. Однако нельзя отрицать и то, что вся информация в этом диагностическом сообщении в некоторых ситуациях может оказаться весьма полезной. Поэтому не стоит удивляться тому, что аналогичную информацию выдают и другие компиляторы (хотя в некоторых из них используется упомянутая выше структуризация). 6.6.2. Мелкое инстанцирование Диагностические сообщения, подобные приведенному выше, выдаются, когда ошибка обнаруживается в конце длинной цепочки инстанцирований. Чтобы проиллюстрировать это, рассмотрим (несколько искусственный) пример кода. template <typename T> void clear(T const& p) { *р = 0; // Предполагается, что Т
6.6. Отладка шаблонов 101 } // является указателем template <typename T> void core(T constfc p) { clear(p); } template <typename T> void middle(typename T::Index p) { core(p); } .template <typename T> void shell(T const& env) { typename T::Index i; middle<T>(i); } class Client { public: typedef int Index; }; Client main_client; int main() { shell (main_client) ; } В этом примере иллюстрируется типичное разделение на уровни при разработке программного обеспечения: шаблоны функций высшего уровня, таких как shell (), зависят от компонентов наподобие middle (), в которых, в свою очередь, используются такие базовые средства, как core (). При инстанцировании shell () необходимо также инстанцировать все нижестоящие уровни. В данном примере проблема обнаруживается на самом глубоком уровне: core () инстанцируется с типом int (из Client: : Index в middle ()), и попытка получить значение по указателю для этого типа является ошибкой. Хорошая диагностика включает отслеживание цепочки действий, ведущих к ошибке, через все уровни, но вы уже видели, в каком громоздком виде может выводиться такое количество информации. Превосходное обсуждение вопросов, относящихся к данной проблеме, читатель найдет в [34]. В этой книге Бьерн Страуструп (Bjarne Stroustrup) определяет два класса подходов, позволяющих заранее определить, удовлетворяют ли аргументы шаблона некоторому набору ограничений. Это делается либо с помощью расширения языка, либо за счет предварительного использования параметров. Первый класс методов частично будет
102 Глава 6. Применение шаблонов на практике рассмотрен в разделе 13.11, стр. 244. Методы второго класса предполагают стимуляцию выдачи всех возможных ошибок путем мелкого инстанцирования (shallow instantiation). Это делается следующим образом: в шаблон помещается неиспользуемый код, единственное назначение которого — заставить компилятор сообщить об ошибке, если этот код инстанцируется с аргументами шаблона, которые не отвечают требованиям более глубоких уровней шаблонов. В нашем предыдущем примере можно добавить в shell () код, с помощью которого можно получить значение по указателю с типом Т: : Index. template <typename T> inline void ignore(T const&) { } template <typename T> void shell(T const& env) { class ShallowChecks { void deref(T::Index ptr) { ignore(*ptr); } }; typename T::Index i; middled) ; } Теперь, если задан такой тип Т, что получение значения по указателю для Т: : Index невозможно, в локальном классе ShallowChecks будет диагностирована ошибка. Отметим, что, поскольку локальный класс в действительности не используется, дополнительный код не влияет на время выполнения функции shell (). К сожалению, многие компиляторы будут выдавать предупреждение о том, что класс ShallowChecks (и его члены) не используется. Для подавления таких предупреждений можно прибегнуть к трюкам наподобие использования ignore (), но при этом повышается сложность кода. Очевидно, что по сложности разработки фиктивный код в нашем примере может оказаться эквивалентен коду, который реализует реальные функциональные возможности шаблона. Чтобы уменьшить эту сложность, естественно попытаться собрать различные фрагменты фиктивного кода в некоторое подобие библиотеки* Например, такая библиотека может содержать макрос, который развертывается в код, генерирующий соответствующую ошибку, если при подстановке параметра шаблона нарушается концепция, на которую опирается данный параметр. Наиболее широко распространенной из таких библиотек является Concept Check Library, которая входит в состав дистрибутива Boost [3]. К сожалению, описанная методика является плохо переносимой (способы диагностирования ошибок существенно зависят от компилятора). Кроме того, ее применение иногда маскирует проблемы, которые невозможно обнаружить на высоком уровне.
6.6. Отладка шаблонов 103 6.6.3. Длинные имена В сообщении об ошибке, которое анализировалось в разделе 6.6.1, продемонстрирована еще одна проблема, связанная с шаблонами: в коде сгенерированного экземпляра шаблона могут присутствовать чрезмерно длинные имена. Например, в реализации, рассмотренной выше, std: : string разрастается, приобретая следующий вид: __STL::basic_string<char,_STL::char_traits<char>, __STL: :allocator<char> > Некоторые программы, в которых задействована стандартная библиотека C++, приводят к именам длиной более 10000 символов. Такие чрезмерно длинные образования могут к тому же вызывать ошибки или предупреждения при компиляции, компоновке и отладке. Чтобы как-то сгладить эту проблему, в современных компиляторах используется сжатие, но в сообщениях об ошибках это сжатие не применяется. 6.6.4. Трассировщики До сих пор рассматривались ошибки, возникающие при компиляции или компоновке программ, в состав которых входят шаблоны. Однако наиболее важная часть работы по тестированию кода программы на предмет ее корректного функционирования зачастую приходится на этап, следующий за успешным завершением создания исполняемого файла. Присутствие в коде шаблонов иногда резко усложняет эту задачу, поскольку поведение обобщенного кода, представленного шаблоном, существенно зависит от клиента этого шаблона (во всяком случае, намного больше, чем в случае обычных классов и функций). Облегчить этот аспект отладки за счет обнаружения проблем в определениях шаблонов на более ранних этапах разработки можно с помощью программы трассировки. Трассировщик — это определяемый пользователем класс, который можно использовать в качестве аргумента тестируемого шаблона. Часто такой класс создается с единственной целью — соответствовать требованиям шаблона и не более того. Важно то, что трассировщик должен отслеживать операции, в которых он задействован. Это позволяет, например, экспериментально проверять эффективность алгоритмов или последовательность выполняемых операций. Ниже приведен пример трассировщика, который можно использовать для тестирования алгоритма сортировки. // basics/tracer.hpp #include <iostream> class SortTracer { private: int value; int generation; static long n_created; static long n__destroyed; // Сортируемое значение // Поколение трассировщика // Вызовы конструктора // Вызовы деструктора
104 Глава 6. Применение шаблонов на практике static long n__assigned; // Количество присвоений static long n_compared; // Количество сравнений static long n_max_live; // Максимальное количество // существующих объектов // Вычисление максимального количества // существующих объектов static void update_max_JLive() { if (n_created-n_destroyed > n_max_live) { n__max__live = n_created-n_destroyed; } } public: static long creations() { return n_created; } static long destructions() { return n_destroyed; } static long assignments() { return n_assigned; } static long comparisons() { return n__compared; } static long max_live() { return n__max_live; } public: // Конструктор SortTracer(int v = 0) : value(v), generation(1) { ++n_created; update_max__live () ; std::cerr « "SortTracer #" « n__created « ", created generation " « generation « " (total: " « n_created - n_destroyed « ")\n"; } // Конструктор копирования SortTracer(SortTracer const& b) : value(b.value), generation(b.generation+1) { ++n_created; update__max_live () ; std::cerr « "SortTracer #" « n_created
6.6. Отладка шаблонов 105 « ", copied as generation " « generation « " (total: " « n_created - n_destroyed « ")\n"; } // Деструктор -SortTracer() { ++n_destroyed; update__max_JLive () ; std::cerr « "SortTracer generation " « generation « " destroyed (total: " « n_created - n_destroyed « ")\n"; } // Присвоение SortTracer& operator = (SortTracer const& b) { ++n_assigned; std::cerr « "SortTracer assignment #" « n_assigned « " (generation " « generation « •» = » « b.generation « ")\n"; value = b.value; return *this; } // Сравнение friend bool operator < (SortTracer constfc a, SortTracer constfc b) { ++n_compared; std::cerr « "SortTracer comparison #" « n_compared « " (generation " « a.generation « " < " « b.generation « ")\n"; return a.value < b.value; } int val() const { return value; } }; Помимо сортируемого значения value класс трассировщика содержит ряд других значений, предназначенных для отслеживания процесса сортировки. Так, generation отслеживает количество копирований данного объекта из оригинала. Остальные статические члены класса служат для отслеживания количества создаваемых объектов (вызовов конструктора), уничтожений объектов, сравнений, присвоений и максимального числа одновременно существующих объектов.
106 Глава 6. Применение шаблонов на практике Статические члены класса трассировщика определяются в отдельном .С-файле. // basics/tracer.cpp tinclude ntracer.hpp" long SortTracer::n_created = 0; long SortTracer::n_destroyed = 0; long SortTracer::n_max_live = 0; long SortTracer::n_assigned =t0; long SortTracer::n_compared = 0; Данный трассировщик обеспечивает возможность отслеживать создание и уничтожение объектов, а также операции присвоения и сравнения, которые выполняются данным шаблоном. В приведенной ниже тестовой программе проиллюстрировано его применение для трассировки алгоритма std: : sort из стандартной библиотеки C++. // basics/tracertest.cpp #include <iostream> #include <algorithm> tinclude "tracer.hpp" int main() { // Подготовка входных данных SortTracer input[] = {7, 3, 5, 6, 4, 2, 0, 1, 9, 8}; // Вывод начальных значений for(int i = 0; i < 10; ++i) { std::cerr « input[i].val() « ' ■; } std::cerr « std::endl; // Запоминание начальных условий long created_at_start = SortTracer::creations(); long max_live_at_start = SortTracer::max_live(); long assigned_at_start =• SortTracer: :assignments () ; long compared^at_start = SortTracer::comparisons(); // Работа алгоритма std::cerr « " [ Start std::sort() ] \n" ; std: :sorto(&input [0] , &input [9]+l) ; std::cerr « " [ End std:: sort () ] \n" ; // Проверка результатов for(int i = 0; i < 10; ++i) { std::cerr « input[i].val() « • f;
6.6. Отладка шаблонов 107 } std::cerr « "\n\n"; // Окончательный отчет std::cerr « "std::sort() of 10 SortTracer's" « " was performed by:\n " « SortTracer: :creations() - created__at_start « " temporary tracers\n n « "up to " « SortTracer: :max__live () « " tracers at the same time (" « max___live_at_start « " before) \n " « SortTracer::assignments()-assigned_at_start « " assignments\n " « SortTracer::comparisons()-comparecLat_start « " comparisons\n\n"; } При запуске этой программы выводится достаточно большой объем информации, однако самое интересное содержится в окончательном отчете. Об одной из реализаций алгоритма std: : sort () трассировщик рассказал следующее: std::sort() of 10 SortTracer's was performed by: 15 temporary tracers up to 12 tracers at the same time (10 before) 33 assignments 27 comparisons Например, хотя в процессе сортировки в нашей программе были созданы 15 временных трассировщиков, одновременно существовало не более двух дополнительных объектов. Данный трассировщик, таким образом, играет двоякую роль: он показывает, что для стандартного алгоритма sort () не требуется никаких дополнительных функциональных возможностей, кроме имеющихся в трассировщике (например, нет необходимости в операторах == или >), и дает представление о расходе ресурсов на выполнение алгоритма. Однако он ничего не говорит о корректности шаблона сортировки. 6.6.5. Интеллектуальные трассировщики Трассировщики относительно просты и эффективны, но они позволяют отслеживать выполнение шаблона только для конкретных входных данных и конкретных функциональных возможностей. Можно не знать, каким условиям должен удовлетворять оператор сравнения, чтобы алгоритм сортировки был корректен, но в нашем примере проверена работа оператора сравнения, который ведет себя точно так же, как "меньше чем" для целочисленных значений. Существуют расширенные варианты трассировщиков, которые иногда называются интеллектуальными трассировщиками (oracles или run-time analysis oracles). Это трасси-
108 Глава 6. Применение шаблонов на практике ровщики, подключенные к так называемой машине логического вывода (inference engine) — программе, которая может запоминать определенные утверждения и правила, относящиеся к ним, чтобы делать на их основании определенные заключения. Одна из таких систем, которая применялась к некоторым частям реализации стандартной библиотеки, а именно MELAS, рассматривается в [26]6). Интеллектуальные трассировщики обеспечивают возможность в некоторых случаях динамически проверять алгоритмы шаблонов без полного определения подставляемых аргументов шаблона (аргументами являются интеллектуальные трассировщики) или входных данных (для машины логического вывода могут потребоваться некоторые предположения относительно входных данных). Однако сложность алгоритмов, которые можно проанализировать таким образом, пока еще невелика вследствие ограниченности машин логического вывода, а количество выполняемой при этом работы весьма значительно. Из-за этого мы не будем останавливаться на разработке интеллектуальных трассировщиков, а читателей, которые заинтересовались этим вопросом, отошлем к упомянутой публикации и содержащимся в ней ссылкам. 6.6.6. Прототипы Выше упоминалось, что трассировщики часто обеспечивают интерфейс, минимально необходимый для тестируемого с их помощью шаблона. Если такой минимальный трассировщик не генерирует динамического вывода, его иногда называют прототипом (archetype). Прототип обеспечивает возможность удостовериться в том, что реализация шаблона не требует больших синтаксических 01раничений, чем предполагалось. Обычно конструкторы шаблона разрабатывают прототипы для всех концепций библиотеки шаблонов. 6.7. Заключение Организация исходного кода в виде заголовочных и .С-файлов представляет собой практическое следствие различных проявлений так называемого правила одного определения (one-definition rule — ODR), которое детально рассматривается в приложении А, "Правило одного определения". Вопрос о том, какой модели — включения или разделения — следует отдавать предпочтение, достаточно спорный. Модель включения представляет собой практичное решение, обусловленное в основном существующими реализациями компиляторов C++. Однако первые реализации C++ были другими: включение определений шаблонов было неявным, что создавало некоторую иллюзию разделения (см. главу 10, "Инстанцирование", где дается подробное описание этой первоначальной модели). Один из авторов этой работы, Дэвид Мюссер (David Musser), принимал активное участие в разработке стандартной библиотеки C++. В частности, он сконструировал и реализовал первые ассоциативные контейнеры.
6.8. Резюме 109 В [34] содержится представление точки зрения Бьерна Страуструпа (Bjarne Stroustrup) на организацию кода шаблонов и рассматривается сопутствующие ей проблемы реализации. Это представление, вне всяких сомнений, не является моделью включения. Да, на определенном этапе процесса стандартизации бытовало мнение, что именно модель включения является единственной жизнеспособной методикой в этой области. Однако после напряженной дискуссии чаша весов начала склоняться в сторону более разъединенной модели, которая в конечном счете оформилась в модель разделения. В противоположность модели включения это теоретическая модель, не основанная ни на каких существующих реализациях, и прошло более пяти лет, прежде чем появилась ее первая опубликованная реализация в мае 2002 года. Иногда заманчиво вообразить себе такое расширение концепции предварительно компилируемых заголовочных файлов, когда в одном процессе компиляции могут загружаться несколько предварительно скомпилированных заголовочных файлов. Это обеспечило бы более тонкий подход к предварительной компиляции. Основным препятствием в данном вопросе является препроцессор: макрос в одном заголовочном файле может полностью изменить смысл последующих заголовочных файлов, а после того как файл откомпилирован и завершена обработка макросов, очень сложно внести какие-либо исправления, связанные с влиянием на препроцессор других заголовочных файлов. Достаточно удачная попытка улучшить диагностику компилятора C++ путем добавления фиктивного кода в шаблоны высокого уровня содержится в Concept Check Library Джереми Сика (Jeremy Sick) [3], являющейся частью библиотеки Boost [5]. 6.8. Резюме • Шаблоны представляют очень сложную проблему для классической модели компиля- тор+компоновщик, поэтому имеются различные способы организации кода шаблонов: модель включения, явное инстанцирование шаблонов и модель разделения. • Обычно следует использовать модель включения (т.е. размещать весь код шаблонов в заголовочных файлах). • Разделение кода шаблонов в различных заголовочных файлах (отдельно объявления и определения) позволяет более легко переключаться между моделью включения и явным инстанцированием. • В стандарте C++ определена раздельная модель компиляции шаблонов (с использованием ключевого слова export), которая, однако, в настоящее время не имеет широкого распространения. • Отладка кода шаблонов сопряжена с рядом проблем. • Экземпляры шаблонов могут иметь чрезвычайно длинные имена. • Чтобы воспользоваться преимуществами применения предварительно откомпилированных заголовочных файлов, следует убедиться в том, что в разных файлах программы соблюдается один и тот же порядок следования директив #include.
Глава 7 Основные термины в области шаблонов Предыдущие главы книги были посвящены знакомству с основами концепций шаблонов в C++. Теперь, прежде чем перейти к более подробному рассмотрению шаблонов, хотелось бы уделить внимание терминам, которые используются при изложении материала. В этом есть необходимость, поскольку в сообществе C++ (и даже в рамках стандарта) четкое понимание концепций и терминологии отсутствует. 7.1. "Шаблон класса" или "шаблонный класс" В C++ структуры, классы и объединения имеют общее название типы класса. Без дополнительного уточнения слово "класс" обычно служит для обозначения типов класса, заданных с помощью ключевых слов class или struct1. Особо следует отметить, что понятие "тип класса" включает объединения, а "класс" — нет. Существует некоторая путаница в отношении того, как следует именовать класс, являющийся шаблоном. • Термин шаблон класса (class template) означает, что класс является шаблоном. Другими словами, это параметризованное описание семейства классов. • Термин шаблонный класс (template class), с другой стороны, используется • как синоним для шаблона класса; • для обозначения классов, сгенерированных из шаблона; В C++ единственное различие между class и struct заключается в том, что доступ по Умолчанию для класса является закрытым (private), в то время как доступ по умолчанию к членам структуры — открытым (public). Однако мы предпочитаем использовать class для типов, в которых применяются новые возможности C++, a struct — для обычных структур С, которые Могут использоваться как "простые старые данные" (plain old data — POD).
112 Глава 7. Основные термины в области шаблонов • для обозначения классов с именем, которое является идентификатором шаблона. Разница между вторым и третьим значениями весьма незначительна и в остальной части книги не играет сколько-нибудь заметной роли. Из-за упомянутой неточности в данной книге мы старались избегать термина шаблонный класс. Аналогично, мы используем термины шаблон функции (function template) и шаблон функции-члена (member function template), но стараемся избегать терминов шаблонная функция (template function) и шаблонная функция-член (template member function). 7.2. Инстанцирование и специализация Процесс создания обычных классов, функций или функций-членов из шаблонов путем подстановки реальных значений вместо их аргументов называется инстанцировани- ем шаблонов. Сущность, полученная в результате инстанцирования шаблонов (класс, функция, функция-член) в общем случае называется специализацией. Однако в C++ процесс инстанцирования не является единственным способом получить специализацию. Существуют альтернативные механизмы, позволяющие программисту явно задавать объявление, привязанное к определенной подстановке параметров шаблона. Как упоминалось в разделе 3.3, стр. 49, такая специализация вводится с помощью конструкции templateo. template <typename Tl, typename Tl> // Первичный шаблон class MyClass { // класса }; "' templateo // Явная специализация class MyClass<std::string,float> { }; "' Строго говоря, это так называемая явная специализация (explicit specialization) (в отличие от инстанцируемой, или генерируемой специализации (instantiated specialization, generated specialization)). Как отмечалось в разделе 3.4, стр. 51, специализации, в которых остаются параметры шаблона, называются частичными специализациями (partial specialization). template <typename T> // Частичная специализация class MyClass<T/T> { ь '" template <typename T> class MyClass<bool,T> { // Частичная специализация }; Если речь идет о специализации (явной или частичной), то общий шаблон называется первичным или основным шаблоном (primary template).
7.3. Объявления и определения ИЗ 7.3. Объявления и определения В предыдущих главах понятия объявления (declaration) и определения (definition) встречались не слишком часто. Однако оба понятия достаточно точно определены в стандарте C++, и именно эти значения используются в данной книге. Объявление (declaration) является конструкцией C++, которая вводит или повторно задает имя в области видимости C++. Такое задание всегда включает частичную классификацию имени, но для корректности объявления не требуется указание всех деталей, например: class С; //С объявлен как класс void f(int p); // f() объявлена как функция, а // р — как именованный параметр extern int v; // v объявлена как переменная Отметим, что макроопределения и метки перехода, несмотря на то что они имеют "имена", в C++ объявлениями не считаются. Объявления становятся определениями (definition), когда делается известной информация об их структуре или, в случае переменных, когда для них должна быть выделена память. Для определений типов классов и функций это означает, что должно быть предоставлено заключенное в скобки тело. В случае переменных для определения достаточно инициализации и отсутствия директивы extern. Ниже приведены примеры, которые дополняют представленные выше неопределенные объявления. class C{}; // Определение (и объявление) класса С void f(int p) { // Определение (и объявление) функции f() Std::COUt << р << Std::endl; } extern int v=l; // Инициализация делает это // выражение определением v int w; // Объявления глобальных переменных, //не предваряемые extern, тоже // являются определениями Распространение этого принципа на шаблоны приводит к тому, что объявление шаблона класса или шаблона функции является определением, если он имеет тело. Следовательно, template <typename T> void func(T); является объявлением, но не определением, в то время как template <typename T> class S{}; представляет собой определение.
114 Глава 7. Основные термины в области шаблонов 7.4. Правило одного определения В языке C++ на повторные объявления различных сущностей накладываются определенные ограничения. Вся совокупность этих ограничений известна как правило одного определения (one definition rule — ODR). Детали этого правила очень сложны и охватывают огромное множество ситуаций. В последующих главах различные аспекты правила одного определения будут проиллюстрированы для каждого рассматриваемого случая, а полное его описание читатель найдет в приложении А, "Правило одного определения". На данном этапе достаточно знать лишь основные положения этого правила. • Невстраиваемые функции и функции-члены, так же как и глобальные переменные и статические данные-члены, должны определяться однократно в рамках про- граммы в целом. • Типы классов (включая структуры и объединения) и встроенные функции следует определять по крайней мере один раз в пределах единицы трансляции, и все эти определения должны быть идентичными. Единица трансляции представляет собой то, что получается в результате обработки исходного файла процессором; другими словами, она включает содержимое файлов, заданных директивами # include. Далее в книге связываемый объект (linkable entity) будет означать одну из следующих вещей: невстраиваемая функция или функция-член, глобальная переменная или статические данные-члены, включая любой из перечисленных объектов, сгенерированный из шаблона. 7.5. Аргументы и параметры шаблонов Сравним шаблон класса template <typename Т, int N> class ArraylnClass { public: T array[N]; }; с похожим обычным классом: class DoubleArraylnClass { public: double array[10]; }; Последний становится, по сути, эквивалентен первому, если заменить параметры Т и N значениями double и 10 соответственно. В C++ эта подстановка обозначается как class ArrayInClass<double/10>
7.5. Аргументы и параметры шаблонов 115 Заметим, что за именем шаблона следуют так называемые аргументы шаблона в угловых скобках. Зависят ли эти аргументы от параметров шаблона или нет, комбинация имени шаблона, за которым следуют аргументы в угловых скобках, называется идентификатором шаблона (template-id). Это имя может использоваться почти так же, как и соответствующие нешаблонные объекты, например: int main () { ArrayInC.lass<double, 10> ad; ad.array[10] = 1.0; } Важно различать параметры шаблона и аргументы шаблона. Коротко говоря, можно сказать, что мы "передаем аргументы, чтобы они стали параметрами" . Или, если быть более точным: • параметрами шаблона являются те имена, которые перечислены после ключевого слова template в объявлении или определении шаблона (в нашем примере — Т и N); • аргументы шаблона являются элементами, которые подставляются вместо параметров шаблона (double и 10 в нашем примере). В отличие от параметров шаблона, аргументы шаблона могут представлять собой нечто большее, чем просто "имена". Подстановка аргументов шаблона вместо параметров шаблона выполняется явно посредством идентификатора шаблона, однако есть различные ситуации, когда подстановка выполняется неявно (например, если вместо параметров подставляются их аргументы по умолчанию). Основной принцип заключается в том, что любой аргумент шаблона должен быть величиной или значением, которое можно определить при компиляции. Как станет понятно позже, это сулит огромные выгоды в плане расхода ресурсов при работе шаблонных объектов. Поскольку параметры шаблона в конечном счете заменяются значениями времени компиляции, они сами могут быть использованы для образования выражений, вычисляемых во время компиляции. Эта возможность используется в шаблоне Arrayln- Class для задания размера члена массива array. Размер массива должен быть так называемым константным выражением, и параметр шаблона N именно таковым является. Поскольку параметры шаблона — это объекты времени компиляции, их можно также использовать для создания корректных аргументов шаблонов. Приведем пример: template <typename T> class Dozen { В академических кругах "аргументы" иногда называются фактическими параметрами (actual parameters), а "параметры" — формальными параметрами (formal parameters).
116 Глава 7. Основные термины в области шаблонов public: ArrayInClass<T,12> contents; }; / Обратите внимание на то, что в данном примере Т является как параметром шаблона, так и аргументом шаблона. Таким образом, обеспечивается механизм конструирования более сложных шаблонов из более простых. Разумеется, этот механизм не имеет существенных отличий от механизмов компоновки типов и функций.
Часть II Углубленное изучение шаблонов Первая часть данной книги представляет собой учебное пособие по основным концепциям языка, на которых базируются шаблоны C++. Содержащегося в ней материала вполне достаточно для ответа на большинство вопросов, возникающих при обычном практическом программировании на C++. Вторая часть книги организована в виде справочника— в ней содержатся ответы на менее типичные вопросы, которые могут возникнуть при использовании расширенных средств языка для достижения более сложных и интересных эффектов при программировании. При первом чтении книги эту часть- справочник можно пропустить, возвращаясь к определенным темам по ссылкам в ходе изучения следующих глав или при поиске терминов в предметном указателе. Наша цель — сделать материал книги более понятным и полным, сохраняя при этом сжатый характер его изложения. Поэтому приведенные в ней примеры являются короткими и зачастую до известной степени искусственными. Это сделано для того, чтобы не уклоняться в сторону от рассматриваемой темы, т.е. не затрагивать вопросов, которые к ней не относятся. Кроме того, здесь освещены возможные изменения и расширения языка шаблонов в C++. Данная часть книги включает перечисленные ниже темы. • Базовые вопросы, касающиеся объявлений шаблонов. • Значение имен в шаблонах. • Механизм инстанцирования шаблонов C++. • Правила вывода аргументов шаблонов. • Специализация и перегрузка. • Будущие возможности.
Глава 8 Вглубь шаблонов В этой главе дается более глубокий обзор основных понятий из области шаблонов, с которыми читатель познакомился в первой части книги. Речь идет об объявлениях шаблонов, ограничениях, накладываемых на параметры и аргументы шаблонов и т.п. 8.1. Параметризованные объявления В настоящее время в C++ поддерживаются два основных типа шаблонов — шаблоны классов и шаблоны функций (см. раздел 13.6, стр. 238, где рассмотрены возможные будущие изменения в данной области). Эта классификация охватывает и шаблоны членов классов. Объявления таких шаблонов практически идентичны объявлениям обычных классов и функций, за исключением того, что для шаблонов указывается выражение параметризации вида tempi at e< . . . перечисление параметров . . . > или export template< . . . перечисление параметров . . . > (ключевое слово export подробно рассматривается в разделах 6.3, стр. 89, и 10.3.3, стр. 174). К объявлениям фактических параметров вернемся в последующих разделах, а сейчас рассмотрим пример, в котором проиллюстрированы два вида шаблонов, являющихся объявлениями членов класса и объявлениями с обычной областью видимости в пространстве имен. template <typename T> class List { // Шаблон класса в области // видимости пространства имен public: template <typename T2> // Шаблон функции-члена List(List<T2> const&); // (конструктора) };
120 Глава 8. Вглубь шаблонов template <typename T> template <typename T2> List<T>::List (List<T2> const&b) // Определение шаблона { // функции-члена вне класса } template <typename T> int length(List<T> const&) class Collection { template <typename T> class Node { }; template <typename T> class Handle; template <typename T> T* alloc() { } }; template <typename T> class Collection::Node { }; // Шаблон функции //в области видимости // пространства имен // Определение // шаблона класса-члена // внутри класса // Еще один шаблон класса- // члена (без определения) // Определение шаблона // функции-члена внутри // класса (неявно // встраиваемой) // Определение шаблона // класса-члена вне // класса Обратите внимание на то, что шаблоны-члены класса, определенные вне пределов охватывающего их класса, могут иметь несколько конструкций параметризации tem- plate<. . . >: одну для самого шаблона и по одной для каждого охватывающего шаблона класса. Конструкции перечисляются начиная с самого внешнего шаблона класса. Возможны шаблоны объединений (они трактуются как разновидность шаблона класса). template <typename T> union AllocChunk { Т object; unsigned char bytes[sizeof(T)]; }; Шаблоны функций, как и объявления обычных функций, мбгут иметь аргументы по умолчанию. template <typename T> void report_top(Stack<T> constfc, int number = 10);
8.1. Параметризованные объявления 121 template <typename T> void fill(Array<T>*, T const& = T()); // T() является нулем для встроенных типов Из последнего объявления видно, что аргумент по умолчанию может зависеть от параметра шаблона. При наличии двух переданных аргументов при вызове функции f ill () аргумент по умолчанию не инстанцируется. Таким образом гарантируется, что, если невозможно инстанцировать аргумент по умолчанию для конкретного Т, ошибки при этом не будет. Например: class Value { public: Value(int); // Конструктора по // умолчанию нет }; void init (Array<Value>* array) { Value zero(O); fill(array,zero); // ВЕРНО: Т() не используется fill(array); // ОШИБКА: Т() используется, // но он некорректен для Т = Value } Используя аналогичную запись, помимо двух основных типов шаблонов можно параметризовать еще три вида объявлений. Все три соответствуют определениям членов шаблонов классов1. 1. Определения функций-членов шаблонов классов. 2. Определения вложенных классов-членов шаблонов классов. 3. Определения статических членов-данных шаблонов классов. Хотя эти определения можно параметризовать, они не являются шаблонами в строгом смысле этого слова. Их параметры полностью определяются шаблоном, членами которого они являются. Ниже приведены примеры таких определений. template <int I> class CupBoard { void open(); class Shelf; Static double total_weight; }; Они очень похожи на обычные члены класса, но их иногда (ошибочно) называют шаблонами членов.
122 Глава 8. Вглубь шаблонов template <int I> void CupBoard<I>::open(); { } template <int I> class CupBoarcU :Shelf { }; template <int I> double CupBoard::total_weight =0.0; Несмотря на то что такие параметризованные определения обычно называются шаблонами, существуют контексты, где этот термин к ним неприменим. 8.1.1. Виртуальные функции-члены Шаблоны функций-членов не могут быть объявлены как виртуальные. Это ограничение накладывается потому, что в обычной реализации механизма вызова виртуальных функций используется таблица фиксированного размера, одна строка которой соответствует одной виртуальной функции. Однако число инстанцированных шаблонов функции- члена не является фиксированным, пока не завершится трансляция всей программы. Следовательно, для того чтобы поддержка шаблонов виртуальных членов-функций стала возможной, требуется реализация радикально нового вида механизма позднего связывания в компиляторах и компоновщиках C++. В отличие от функций-членов, обычные члены шаблонов классов могут быть виртуальными, поскольку их число при инстанцировании класса фиксировано. template <typename T> class Dynamic { public: virtual -Dynamic(); // ВЕРНО: один деструктор //на экземпляр Dynamic<T> template <typename T2> virtual void copy (T2 const&); // ОШИБКА: неизвестно количество // экземпляров сору() на один // экземпляр Dynamic<T> }; 8.1.2. Связывание шаблонов Каждый шаблон должен иметь имя, и это имя должно быть уникальным в пределах его области видимости, за исключением шаблонов функций, которые могут быть пере-
8.1. Параметризованные объявления 123 гружены (см. главу 12, "Специализация и перегрузка"). Особо отметим, что, в отличие от типов классов, для шаблонов классов не допускается использование имен, совпадающих с именами объектов других видов. int С; class С; //ВЕРНО: имена классов и не классов // находятся в разных "пространствах" int X; template <typename T> class X; // ОШИБКА: конфликт с переменной X struct S; template <typename T> class S; // ОШИБКА: конфликт со структурой S Для имен шаблонов используется связывание, но это не обязательно связывание языка С. Возможно применение нестандартных правил связывания, зависящих от реализации (однако нам неизвестна реализация, которая поддерживает нестандартные правила связывания имен для шаблонов). extern "C++" template<typename T> void normal(); // Это связывание по умолчанию: данная спецификация // связывания может быть опущена extern "С" template<typename T> void invalid(); // Неверно: шаблоны не могут иметь С-связывания extern "Xroma" template<typename T> void xroma_link(); // Нестандартная ситуация, но, возможно, // "некоторые компиляторы будут когда-нибудь // поддерживать связывание, совместимое с языком Xroma Шаблоны обычно имеют внешнее связывание. Единственным исключением являются шаблоны функций в области видимости пространства имен, описанные как static. template<typename T> void external(); // Ссылается на тот же объект, что //и объявление с этим же именем //(и областью видимости) // в другом файле template<typename T>
124 Глава 8. Вглубь шаблонов static void internal(); // Не имеет никакого отношения к // шаблону с тем же именем //в другом файле Заметим, что шаблон не может быть объявлен в функции. 8.1.3. Первичные шаблоны С помощью обычных конструкций объявлений шаблонов объявляются так называемые первичные шаблоны. В таких объявлениях отсутствуют аргументы шаблона в угловых скобках после имени. template<typename T> class Box; // ВЕРНО: первичный // шаблон template<typename T> class Box<T>; // ОШИБКА template<typename T> void translate(T*); // ВЕРНО: первичный // шаблон template<typename T> void translate<T>(T*); // ОШИБКА Вторичные шаблоны классов получаются при объявлении так называемых частичных специализаций, которые рассматриваются в главе 12, "Специализация и перегрузка". Шаблоны функций всегда должны быть первичными (см. раздел 13.7, стр. 239, где рассмотрены возможные изменения в этой области в будущем). 8.2. Параметры шаблонов Существует три вида параметров шаблонов. 1. Параметры типа (сегодня они используются наиболее часто). 2. Параметры, не являющиеся типами. 3. Шаблонные параметры шаблонов Параметры шаблона задаются в начальном параметризованном объявлении шаблона. Такие объявления не обязательно должны быть именованными: template <typename/ int> class X; Однако если дальше в тексте шаблона имеется ссылка на параметр, то имя параметра конечно же необходимо. Заметим также, что имя параметра шаблона может использоваться в последующих объявлениях параметров (но не в предшествующих). template <typename Т, // Первый параметр // используется в Т* Root, // объявлении второго template<T*> class Buf> // и третьего параметров class Structure;
8.2. Параметры шаблонов 125 8.2.1. Параметры типа Параметры типа задаются с помощью ключевых слов typename либо class; оба варианта эквивалентны . За ключевым словом должен следовать простой идентификатор, за которым идет запятая, означающая начало следующего объявления параметра, закрывающая угловая скобка (>) для обозначения конца параметризованного выражения или знак равенства (=) для обозначения начала заданного по умолчанию аргумента шаблона. В пределах объявления шаблона параметр типа ведет себя подобно имени, заданному с помощью typedef. Например, нельзя использовать имя вида class Т, где Т является параметром шаблона, даже если вместо Т подставляется тип класса. template <typename Allocators class List { class Allocator* allocator; // ОШИБКА friend class Allocator; // ОШИБКА }; Вполне вероятно, что механизм, обеспечивающий возможность задавать такие объявления дружественных конструкций, появится в будущем. 8.2.2. Параметры, не являющиеся типами Не являющиеся типами параметры — это константные значения, которые могут быть определены при компиляции или при компоновке . Тип такого параметра (другими словами, тип значения, которое он обозначает) должен быть одним из следующих: • целочисленный тип или тип перечисления; • тип указателя (включая указатели на обычные объекты, функции и члены классов); • ссылочный тип (как ссылки на объекты, так и ссылки на функции). На сегодня все прочие типы в этот перечень не входят (хотя в будущем возможно включение в него типов с плавающей точкой; см. раздел 13.4, стр. 235). Возможно, это покажется несколько неожиданным, но объявление параметра шаблона, не являющегося типом, в некоторых случаях также может начинаться с ключевого слова typename. Ключевое слово class не означает, что подставляемый параметр должен иметь тип класса. Это может быть практически любой доступный тип. Однако в качестве аргументов шаблона (независимых или объявленных с помощью typename или class) нельзя использовать типы Класса, которые определяются в функции (локальные классы). Шаблонные параметры шаблона также не обозначают типы, однако они не рассматриваются в качестве параметров, не являющихся типами.
126 Глава 8. Вглубь шаблонов template <typename Т, // Параметр типа typename Т::Allocator* Allocator> // Параметр, не // являющийся типом class List; Разницу здесь увидеть легко: в первом случае за ключевым словом следует простой идентификатор, а во втором — полное имя (другими словами, имя, содержащее два двоеточия, — ::). В разделах 5.1, стр. 65, и 9.3.2, стр. 154, объясняется необходимость ключевого слова typename в параметре, не являющемся типом. Возможно использование типов функций и массивов, но они неявно сводятся к типу соответствующего указателя. template <int buf[5]> class Lexer; // Реально это int* template <int* buf> class Lexer; // ВЕРНО: повторное // объявление Параметры, не являющиеся типами, объявляются почти так же, как и переменные, но они не могут иметь спецификаторов, таких, как static, mutable и т.д. Возможно использование модификаторов const или volatile, но указание таких модификаторов у параметров внешнего уровня вложенности попросту игнорируется. template <int const length> class Buffer; // Модификатор const здесь лишний template <int length> class Buffer; // Объявление аналогично предыдущему И наконец, параметры, не являющиеся типами, всегда являются rvalue. Их адрес нельзя получить, и им нельзя ничего присвоить. 8.2.3. Шаблонные параметры шаблона Такие параметры являются символами-заполнителями для шаблонов классов. Они объявляются во многом подобно шаблонам классов, однако при этом нельзя использовать ключевые слова struct и union. template <template<typename X> class C> // ВЕРНО void f(C<int>* p); template <template<typename X> struct C> // ОШИБКА: void f(C<int>* p); // struct здесь //не допускается template <template<typename X> union C> // ОШИБКА: void f(C<int>* p); // union здесь // не допускается В области видимости своих объявлений шаблонные параметры шаблонов используются точно так же, как и другие шаблоны класса.
8.2. Параметры шаблонов 127 Параметры шаблонных параметров шаблонов могут иметь аргументы, заданные по умолчанию. Эти аргументы применяются в том случае, когда при использовании шаблонного параметра шаблона соответствующие параметры не указаньд. template. <template<typename Т, typename A = MyAllocator > class Containers class Adaptation { Container<int> storage; // Неявно эквивалентен // Container<T,MyAllocator> }; Имя параметра шаблонного параметра шаблона может использоваться только в объявлениях других параметров данного шаблонного параметра шаблона. Это утверждение иллюстрируется на примере приведенного ниже (несколько искусственного) шаблона. template <template<typename T, T*> class Buf> class Lexer { static char storage[5]; Buf<char, &Lexer<buf>::storage> buf; }; template <template<typename T> class List> class Node { static T* storage; // ОШИБКА: параметр шаблонного // параметра шаблона здесь // использовать нельзя }; Однако обычно имена параметров шаблонного параметра шаблона не используются, и поэтому им зачастую вообще не присваиваются имена. Например, рассмотренный выше шаблон Adaptation можно объявить следующим образом: template <template<typename, typename = MyAllocator> class Container> class Adaptation { Container<int> storage; // Неявно эквивалентно // Container<int/MyAllocator> } ; 8.2,4. Аргументы шаблона, задаваемые по умолчанию В настоящее время аргументы шаблона, задаваемые по умолчанию, допускаются только для объявлений шаблонов классов (см. раздел 13.3, стр. 233). Аргументом по
128 Глава 8. Вглубь шаблонов умолчанию может быть снабжен параметр шаблона любого типа (но при этом аргумент по умолчанию должен соответствовать "своему" параметру). Очевидно, что аргумент, заданный по умолчанию, не должен зависеть от собственного параметра, однако он может зависеть от предшествующих ему параметров. template<typename Т, typename Allocator = allocator<T> > class List; Так же как и задаваемые по умолчанию аргументы функций, параметры шаблона могут иметь аргумент по умолчанию только в случае, когда аргументами по умолчанию снабжены также и все последующие параметры. Последующие значения по умолчанию обычно указываются в том же объявлении шаблона, но они могут также быть объявлены в предыдущих объявлениях этого шаблона. Сказанное поясняет приведенный ниже пример. template<typename Tl, typename T2, typename ТЗ,, typename T4 = char, typename T5 = char> class Quintuple; //ВЕРНО template<typename Tl, typename T2, typename T3 = char, typename T4, typename T5> class Quintuple; //ВЕРНО: Т4 и Т5 уже имеют // значения по умолчанию template<typename Tl = char, typename T2, typename T3, typename T4, typename T5> class Quintuple; //ОШИБКА: Tl не может иметь аргумент // по умолчанию, поскольку у Т2 // его нет Задаваемые по умолчанию аргументы шаблона не могут повторяться. template<typename T = void> class value; template<typename T = void> class value; // ОШИБКА: повторяется // аргумент по умолчанию 8.3. Аргументы шаблонов Аргументы шаблонов — это значения, которые подставляются вместо параметров шаблона при инстанцировании шаблона. Такие значения можно задавать несколькими способами. • Явные аргументы шаблона: за именем шаблона могут следовать явно указанные значения аргументов шаблона, заключенные в угловые скобки. Полученное в результате имя называется идентификатором шаблона (template-id). • Введенное имя класса: в области видимости шаблона класса X с параметрами шаблона Р1,Р2,.. . имя этого шаблона (X) может быть эквивалентно идентифи-
8.3. Аргументы шаблонов 129 катору шаблона Х<Р1/Р2/... >. Более подробно это разъясняется в разделе 9.2.3, стр. 150. • Аргументы шаблона, заданные по умолчанию: при наличии таких аргументов явно указанные аргументы шаблона в экземплярах шаблонов классов могут быть пропущены. Однако, даже если все параметры шаблона имеют значения по умолчанию, все равно должны быть указаны (возможно, пустые) угловые скобки. • Вывод аргументов: аргументы шаблонов функций, не указанные явно, могут быть получены путем вывода из типов аргументов вызова функции в ее вызове. Более подробно это описано в главе 11, "Вывод аргументов шаблонов". Вывод осуществляется и в некоторых других ситуациях. Если все аргументы шаблона могут быть получены путем вывода, указывать угловые скобки после имени шаблона функции не требуется. 8.3.1. Аргументы шаблонов функций Аргументы шаблона функции могут быть заданы явно либо получены путем вывода на основе способа использования шаблона. // details/max.срр tempiate<typename T> inline T constfc max(T const& a, T constfc b) { return a < b ? b : a; } int main() { max<double>(1.0, -3.0); max(1.0, -3.0); max<int>(1.0, 3.0); } Некоторые аргументы шаблона невозможно получить путем вывода (см. главу 11, "Вывод аргументов шаблонов"). Соответствующие параметры лучше помещать в начале списка параметров шаблона с тем, чтобы их можно было задать явно, а остальные получить путем вывода. // details/implicit.срр template<typename DstT, typename SrcT> inline DstT implicit_cast(SrcT const& x) // Явное указание аргументов // шаблона // Неявный вывод типа double // для аргументов шаблона // Явное задание <int> подавляет // вывод; следовательно/ // результат имеет тип int
30 Глава 8. Вглубь шаблонов { // SrcT выводится, a DstT — нет return x; int main() double value = implicit_.cast<double>(-l); Если в данном примере изменить порядок следования параметров шаблона (другими словами, если написать template<typename SrcT, typename DstT>), оба аргумента шаблона в вызове implicit_cast нужно будет задавать явно. Поскольку шаблоны функций могут быть перегружены, явного указания всех аргументов шаблона функции может оказаться недостаточно для идентификации конкретной функции: в некоторых случаях таким образом задается семейство функций. В приведенном ниже примере иллюстрируется следствие из этого наблюдения. template<typename Func, typename T> void apply (Func func_ptr, T x) { func_ptr(x) ; } template<typename T> void single(T); template<typename T> void multi(T); template<typename T> void multi(T*); int main() { apply(&single<int>,3); // ВЕРНО apply(&multi<int>,7); // ОШИБКА: нет единственной // multi<int> } * В этом примере первый вызов apply () корректен, поскольку тип выражения &single<int> является недвусмысленным. В результате значение аргумента шаблона для параметра Func легко получается путем вывода. Однако во втором вызове &multi<int> тип может быть одним из двух разных типов, а следовательно, в данном случае Func нельзя получить путем дедукции. Более того, явное указание аргументов шаблона функции может привести к попытке сконструировать неверный тип C++. Рассмотрим следующий перегруженный шаблон функции (RT1 и RT2 являются неопределенными типами): template<typename T> RT1 test(typename T::X const*); template<typename T> RT2.test(...); Выражение test<int> для первого из двух шаблонов функций не имеет смысла, поскольку у типа int нет типа-члена X. Однако для второго шаблона такая проблема от-
8.3. Аргументы шаблонов 131 сутствует. Следовательно, выражением &test<int> задается адрес единственной функции. Однако из-за того, что подстановка int в первом шаблоне невозможна, это выражение не становится некорректным. Очевидно, что принцип "неверная подстановка не является ошибкой" (substitution- failure-is-not-an-error — SFINAE) представляет собой важную составную часть практического применения перегрузки шаблонов функций. Благодаря этому принципу становится возможным замечательный прием, используемый при компиляции. Например, предположим, что типы RT1 и RT2 определены следующим образом: typedef char RT1; typedef struct {char a[2]; } RT2; Во время компиляции (другими словами, используя так называемое константное выражение) можно проверить, имеет ли данный тип Т тип-член X. #define type_has_member_type__X (T) \ (sizeof(test<T>(0)) == 1) Чтобы понять выражение в этом макросе, удобно анализировать его снаружи внутрь. Прежде всего, выражение sizeof будет равно 1, если выбран первый шаблон test (который возвращает char с размером 1). Второй шаблон возвращает структуру с размером по меньшей мере 2 (поскольку она содержит массив с размером 2). Другими словами, мы имеем конструкцию, позволяющую на основе константного выражения определить, какой из шаблонов— первый или второй— был выбран для вызова функции test<T> (0). Очевидно, что если данный тип Т не имеет типа-члена X, то первый шаблон не мог быть выбран. Однако, если данный тип имеет тип-член X, выбирается первый шаблон, поскольку при распознавании имени перегруженной функции по типам ее параметров (см. приложение Б, Разрешение перегрузки") предпочтение отдается преобразованию от нуля к константе, соответствующей нулевому указателю, а не привязке аргумента к параметру-троеточию (такие параметры являются самым слабым видом связывания в аспекте распознавания имени перегруженной функции по типам ее параметров). Аналогичная методика используется в главе 15, "Классы свойств и стратегий". Принцип "неверная подстановка не является ошибкой" защищает только от создания неверных типов, но не от вычисления неверных выражений. Следовательно, приведенный ниже пример неверен. template*:int I> void f(int (&) [24/(4-1)]); template<int I> void f(int (&) [24/(4+1)]); ^t main () { &f<4>; // Ошибка: деление на нуль (принцип // SFINAE не применяется) Этот код является ошибочным, несмотря на то что за счет второго шаблона обеспечивается подстановка, которая не приводит к делению на нуль. Ошибки такого рода про-
132 Глава 8. Вглубь шаблонов исходят в самих выражениях, а не при связывании выражения с параметром шаблона. Следующий пример вполне корректен: template<int N> int g() { return N; } template<int* P> int g() { return *P;} int main() { return g<l>(); // 1 не может быть привязана //к параметру int*. } // Применим принцип SFINAE Другие примеры применения принципа SFINAE вы найдете в разделах 15.2.2, стр. 293, и 19.3, стр. 376. 8.3.2. Аргументы типов Аргументы типов шаблона являются "значениями", которые указываются для параметров типов шаблона. В качестве аргументов шаблона могут выступать почти все обычно используемые типы, но есть два исключения. 1. В число аргументов типов шаблонов не могут входить локальные классы и перечисления (другими словами, типы, которые объявляются в определении функции). 2. Аргументами шаблонов не могут быть типы, которые включают неименованные типы класса или неименованные типы перечислений (однако аргументами шаблона могут быть неименованные классы или перечисления, которые получают имена с помощью объявления typedef). Эти два исключения иллюстрируются в приведенном ниже примере, template <typename T> class List { }; typedef struct { double x, y, z; } Point; typedef enum { red, green, blue } *ColorPtr; int main () { struct 'Association { { int* p; int* q; }; List<Assocation*> error 1; // Ошибка: локальный тип
8.3. Аргументы шаблонов 133 //в аргументе шаблона List<ColorPtr> error2; // Ошибка: неименованный тип //в аргументе шаблона List<Point> ok; // ВЕРНО: неименованный тип // класса, именованный при // помощи typedef } При использовании в качестве аргументов шаблона других типов, их подстановка вместо параметров шаблона должна приводить к корректным конструкциям. template <typename T> void clear (T p) { *р = 0; // Требуется, чтобы к Т была применима // унарная операция разыменования } int main () { int a; clear(a); // ОШИБКА: для int не поддерживается // унарная операция разыменования } 8.3.3. Аргументы, не являющиеся типами Не являющиеся типами аргументы шаблона представляют собой значения, которые подставляются вместо не являющихся типами параметров. Такая величина может быть одной из перечисленных ниже. • Другой параметр шаблона, не являющийся типом и имеющий верный тип. • Значение константы времени компиляции с целочисленным типом или типом перечисления. Это справедливо только в случае, когда параметр имеет тип, соответствующий типу этого значения (или типу, к которому оно может быть неявно преобразовано: например, тип char допускается для параметра с типом int). • Имя внешней переменной или функции, которой предшествует встроенный унарный оператор & (получение адреса). Для переменных функций и массивов & можно опускать. Такие аргументы шаблона соответствуют не являющимся типом параметрам с типом указателя. • Аргументы того же вида, но не предваряемые оператором &, являются корректными аргументами для не являющихся типом параметров ссылочного типа. • Константный указатель на член класса, другими словами, выражение вида &С: : т, где С — тип класса, am — нестатический член класса (данные или функция). Такие значения соответствуют только не являющимся типом параметрам с типом указателей на член класса.
134 Глава 8. Вглубь шаблонов При установлении соответствия аргумента параметру, который является указателем или ссылкой, преобразования, определенные пользователем (конструкторы с одним аргументом и операторы преобразования), а также преобразования объекта-наследника в объект-родитель не рассматриваются, даже если в иных обстоятельствах эти преобразования являются корректными неявными преобразованиями. Допустимы неявные преобразования, которые придают аргументу свойства const или volatile. Ниже приведено несколько примеров, не являющихся типами аргументов шаблонов. template <typename T,T nontype_param> class С; C<int,33>* cl; // Целочисленный тип int a; C<int*,&a>* c2; // Адрес внешней переменной void f(); void f(int); C<void (*)(int),&f>* c3; // Имя функции: разрешение перегрузки // приводит к выбору f(int) class X { int n; static bool b; >• C<bool&, X::b>* c4; // Статические члены класса // являются допустимыми C<int X::*,&X::n>* c5; // Пример указателя на член класса template<typename T> void templ_func(); C<void() , &templ__func<double> >* c6; // Экземпляры шаблона функции // являются функциями Основным ограничением для аргументов шаблона является следующее: компилятор или компоновщик должны быть способны точно определить их значения при создании исполняемого файла. Значения, которые не известны к моменту начала выполнения программы (например, адреса локальных переменных), не отвечают требованию, состоящему в том, что шаблоны должны быть инстанцированы к моменту завершения построения программы. Но даже при выполнении данного ограничения существует несколько константных выражений, которые (возможно, это покажется странным) в настоящее время некорректны: • нулевые указатели; • числа с плавающей точкой; • строковые литералы.
8.3. Аргументы шаблонов 135 Одна из проблем со строковыми литералами состоит в том, что два идентичных литерала могут храниться по двум разным адресам. Существует альтернативный (но громоздкий) способ определения шаблонов, генерация экземпляров которых осуществляется через строки: определение дополнительной переменной для хранения строки. template <char const* str> class Message; extern char const hello[] = "Hello World!11; Message<hello>* hello_msg; Отметим, что в данном примере необходимо указывать ключевое слово extern, поскольку в противном случае переменная константного массива будет иметь внутреннее связывание. Еще один пример приведен в разделе 4.3, стр. 62. В разделе 13.4, стр. 235, рассматриваются возможные будущие изменения в этой области. Ниже приведено несколько других неверных примеров. template<typename Т, T nontype__param> class С; class Base { int i; } base; class Derived : public Base { } derived_obj; C<Base*,&derived_obj>* errl; // ОШИБКА: преобразования // производного.класса к // базовому не рассматриваются C<int&, base.i>* err2; // ОШИБКА: поля переменных //не считаются переменными int a[10]; C<int*, &a[0]>* еггЗ; // ОШИБКА: адреса отдельных // элементов массива также //не допускаются 8.3.4. Шаблонные аргументы шаблонов Шаблонный аргумент шаблона должен быть шаблоном класса с параметрами, которые точно соответствуют параметрам шаблонного параметра шаблона, вместо которого он подставляется. Аргументы шаблона, заданные по умолчанию для шаблонного аргумента шаблона, игнорируются (но если шаблонный параметр шаблона имеет аргументы
136 Глава 8. Вглубь шаблонов по умолчанию, они учитываются при инстанцировании). Таким образом, приведенный ниже пример некорректен. #include <list> //В этом файле есть объявление // namespace std { // template <typename T, // typename Allocator = allocator<T> > // class list; //} template<typename Tl, typename T2, template<typename> class Containers // Ожидается, что Container - шаблон с одним параметром class Relation { public: private: Container<Tl> doml; Container<T2> dom2; }; int main () { Relation<int,double,std::list> rel; // ОШИБКА: std::list имеет более одного } I/ параметра шаблона } Проблема в этом примере заключается в том, что шаблон std: : list стандартной библиотеки имеет более одного параметра. Второй параметр (который описывает так называемый распределитель памяти) имеет значение по умолчанию, но оно не учитывается ^ при установлении соответствия std: : list параметру Container. Иногда выход из таких ситуаций заключается в том, что для шаблонного параметра шаблона задается параметр со значением по умолчанию. Для предыдущего примера можно переписать шаблон, как показано ниже. #include <memory> template<typename Tl, typename T2, template<typename T, typename = std::allocator<T> > class Container> // Теперь шаблон Container может быть шаблоном // контейнера из стандартной библиотеки class Relation { public:
8.3. Аргументы шаблонов 137 private: Container<Tl> doml; Container<T2> dom2; } Понятно, что это не совсем то, что нужно, но зато такое решение обеспечивает возможность использования стандартных шаблонов контейнеров^ В разделе 13.5, стр. 237, рассмотрены возможные изменения в этой области в будущем. Тот факт, что синтаксически для объявления шаблонного параметра шаблона может быть использовано только ключевое слово class, не следует толковать как указание, что в качестве подставляемых аргументов допускаются только шаблоны класса, объявленные с помощью ключевого слова class. В действительности для шаблонного параметра шаблона вполне корректными аргументами являются "шаблоны структур" и "шаблоны объединений". Это утверждение аналогично приведенному выше, которое гласит, что в качестве аргумента для параметра типа шаблона, объявленного с помощью ключевого слова class, можно использовать любой тип. 8.3.5. Эквивалентность Два набора аргументов шаблона являются эквивалентными, если значения аргументов попарно идентичны друг другу. Для аргументов типа имена, заданные с помощью typedef, не имеют значения— в конечном счете сравнивается тип, лежащий в основе имени. Для целочисленных аргументов, не являющихся типом, сравнивается значение аргумента; способ получения этого значения роли не играет. Сказанное выше иллюстрируется следующим примером: template <typename Т, int I> ' class Mix; typedef int Int; Mix<int, 3*3>* pi; Mix<Int, 4+5>* p2; // p2 имеет тот же тип, что и pi Функция, сгенерированная из шаблона функции, никогда не эквивалентна обычной Функции, даже если обе имеют один и тот же тип и одно и то же имя. Отсюда вытекают Два важных следствия для членов классов. 1. Функция, сгенерированная из шаблона функции-члена, никогда не может переопределять виртуальную функцию. 2. Конструктор, сгенерированный из шаблона конструктора, никогда не может быть конструктором копирования по умолчанию (точно так же оператор присвоения, сгенерированный из шаблона присвоения, никогда не является оператором копирующего присвоения; однако это гораздо меньшая проблема, поскольку, в отличие от конструкторов копирования, операторы присвоения никогда не вызываются неявно).
138 Глава 8. Вглубь шаблонов 8.4. Друзья Основная идея объявления дружественных конструкций проста: определить классы или функции, имеющие привилегированную связь с классом, в котором присутствуют эти объявления. Содержание же этой идеи несколько сложнее, и тому есть две причины. 1. Объявления дружественных конструкций могут быть единственными объявлениями объектов. 2. Объявление дружественной функции может быть определением. Объявления дружественных классов не могут быть определениями и, следовательно, реже создают проблемы. В контексте шаблонов единственный новый аспект объявлений дружественных классов — это возможность именовать конкретный экземпляр шаблона класса как дружественный. template <typename T> class Node; template <typename T> class Tree { friend class Node<T>; }; Заметим, что шаблон класса должен быть видим в точке, где один из его экземпляров делается другом класса или шаблона класса. В случае обычного класса такое требование отсутствует. template <typename T> class Tree { friend class Factory; // ВЕРНО, даже если это // первое объявление Factory friend class // Ошибка, если класс Node не class Node<T>; // является видимым в этой точке }; Более подробно этот вопрос рассматривается в разделе 9.2.2, стр. 149. 8.4.1, Дружественные функции Чтобы сделать экземпляр шаблона функции дружественным, после имени дружественной функции должны указываться угловые скобки. Угловые скобки могут содержать аргументы шаблона, но если аргументы можно вывести, угловые скобки могут быть пустыми. template <typename Tl, typename T2> void combine(Tl, T2); class Mixer {
8.4. Друзья 139 friend void combineo(int&, int&) ; // ВЕРНО: Tl = int&, T2 = int& friend void combine<int,int>(int,int); // ВЕРНО: Tl = int, T2 = int friend void combine<char>(char,int); // ВЕРНО: Tl = char, T2 = int friend void combine<char>(cha,r&, int) ; // ОШИБКА: не соответствует шаблону combine() friend void combine<>(long,long) { ... } // ОШИБКА: определение не разрешено! Заметим, что здесь нельзя определять экземпляр шаблона (максимум, что можно сделать— это определить специализацию) и, следовательно, объявление дружественной конструкции, именующее экземпляр, не может быть определением. Если за именем не следуют угловые скобки, возможны два варианта. 1. Если имя не полное (другими словами, не содержит двух двоеточий), оно не может служить ссылкой на экземпляр шаблона. Если в точке объявления дружественной конструкции нет видимой соответствующей нешаблонной функции, дружественная конструкция является первым объявлением этой функции. Объявление может быть также определением. 2. Если имя полное (содержит : :), оно должно ссылаться на ранее объявленную функцию или шаблон функции. Подходящей функции отдается предпочтение перед подходящим шаблоном функции. Однако такое объявление дружественной конструкции не может быть определением. Лучше разобраться в описанных возможностях читателю поможет приведенный ниже пример. void multiply(void*); // Обычная функция template <typename T> void multiply(T); // Шаблон функции class Comrades { friend multiply(int) {} // Определение новой функции // : -.multiply(int) friend :: multiply (void*) ; // Ссылка на обычную функцию выше; // но не на экземпляр multiply<void*> friend ::multiply(int); // Ссылка на экземпляр шаблона friend ::multiply<double*>(double*); // Полные имена также // могут иметь угловые скобки,
140 Глава 8. Вглубь шаблонов //но шаблон должен быть видимым friend ::error() {} // ОШИБКА: полное имя дружественной // конструкции не может быть определением }; В предыдущих примерах дружественные функции объявлялись в обычном классе. Те же правила применимы и при объявлении дружественных функций в шаблонах классов; при этом в определении функции, которая объявляется как дружественная, могут присутствовать параметры шаблона. template <typename T> class Node { Node<T>* allocate(); }; template <typename T> class List { friend Node<T>* Node<T>::allocate(); }; Однако, когда дружественная функция определяется в шаблоне класса, возникает интересный эффект: ведь все объявленное в шаблоне не является конкретной сущностью до тех пор, пока шаблон не будет инстанцирован. А теперь рассмотрим следующий пример: template <typename T> class Creator { friend void appear() { // Новая функция ::appear(), которая не существует, // пока не будет инстанцирован шаблон Creator } Creator<void> miracle; // ::appear() создается // в этой точке Creator<double> oops; // ОШИБКА: ::арреаг() создается // во второй раз! В данном примере два различных экземпляра создают два идентичных определения, а это прямое нарушение правила одного определения (ODR) (см. приложение А). Таким образом, мы должны гарантировать, что шаблонные параметры шаблона класса присутствуют в типе любой дружественной функции, определенной в этом шаблоне (за исключением ситуации, когда инстанцирования более одного экземпляра шаблона
8.4. Друзья 141 в файле гарантированно не будет, но это довольно редкая ситуация). Применим это правило к предыдущему примеру. template <typename T> class Creator { friend void feed(Creator<T>*){ // для каждого Т генерируется своя функция ::feed() } }; Creator<void> one; // генерируется ::feed(Creator<void>*) Creator<double> two;// генерируется ::feed(Creator<double>*) В данном примере при каждом инстанцировании шаблона Creator создается своя функция. Отметим, что, хотя эти функции генерируются как часть инстанцирования шаблона, сами они являются обычными функциями, а не экземплярами шаблона. Заметим также, что, поскольку тело этих функций определяется внутри определения класса* они являются неявно встраиваемыми. Следовательно, когда одна и та же такая функция генерируется в двух разных единицах трансляции, это не является ошибкой. Более подробно данный вопрос освещен в разделах 9.2.2, стр. 149, и 11.7, стр. 201. 8.4.2. Дружественные шаблоны Обычно при объявлении дружественной конструкции, которая является экземпляром шаблона функции или класса, можно точно указать, что именно должно быть дружественным. Иногда, однако, желательно, чтобы дружественными по отношению к классу были все экземпляры шаблона. Отсюда вытекает понятие так называемого дружественного шаблона. class Manager { template<typename T> friend class Task; template<typename T> friend void Schedule<T>::dispatch(Task<T>*); tempiate<typename T> friend int ticket () { return ++Manager::counter; } static int counter; Так же, как и в случае обычных объявлений дружественных конструкций, дружественный шаблон может быть определением, только если он именует неполное имя функции, за которым не следуют угловые скобки. Дружественными шаблонами могут быть только первичные шаблоны и их члены. Любые частичные и явные специализации, связанные с первичным шаблоном, автоматически являются дружественными.
142 Глава 8. Вглубь шаблон* 8.5. Заключение ' Основная концепция и синтаксис шаблонов C++ остаются относительно стабильными, начиная со времени их появления в конце 1980-х годов. Изначально концепция шаблонов включала в себя шаблоны классов и шаблоны функций, а также параметры типа и параметры, не являющиеся типом. Впоследствии в исходную конструкцию был внесен ряд существенных дополнений, в основном обусловленных потребностями стандартной библиотеки C++. Главными среди этих дополнений являются шаблоны-члены. Интересно, что при голосовании в стандарт C++ были внесены только функции-члены, а шаблоны-члены классов стали частью стандарта по редакторской оплошности. Дружественные шаблоны, аргументы шаблонов по умолчанию и шаблонные параметры шаблонов были добавлены в язык относительно недавно. Возможность объявления шаблонных параметров шаблонов иногда называют обобщенностью высшего порядка (high-order genericity). Первоначально они были введены для поддержки конкретной модели распределителя памяти в стандартной библиотеке C++, но эта модель позднее была заменена другой, для которой шаблонные параметры шаблонов не требуются. Позже они едва не были исключены из языка, поскольку их спецификация в процессе стандартизации достаточно долго оставалась незавершенной. Тем не менее со временем большинство членов комитета все же проголосовали за то, чтобы оставить их в стандарте, и их спецификация была наконец завершена.
Глава 9 Имена в шаблонах Имена в большинстве языков программирования представляют собой фундаментальную концепцию. Они являются средством, с помощью которого программист может обращаться к ранее созданным объектам. Когда компилятор C++ встречает имя, он должен выполнить его поиск, чтобы определить, на какой объект ссылается это имя. С точки зрения реализации C++ в этом отношении является сложным языком. Рассмотрим, например, выражение С+ х*у;. Если х и у — имена переменных, данное выражение является умножением, но если х является именем типа, то это не что иное, как объявление у как указатель на объект типа х. Из этого небольшого примера видно, что C++ (как и С) является так называемым контекстно-зависимым языком. Другими сдовами, конструкцию языка не всегда можно распознать без знания ее более широкого контекста. Естественно задать вопрос: а какое отношение это имеет к шаблонам? Шаблоны являются конструкциями, которые имеют дело с несколькими контекстами: контекст, в котором шаблон появляется, контекст, в котором шаблон инстанцируется, и контекст, связанный с аргументами шаблона, для которых происходит инстанцирование. Следовательно, теперь вас не должно очень удивить то, что имена в C++ требуют к себе особого внимания. 9.1. Систематизация имен Имена в C++ классифицируются разными способами, причем этих способов существует огромное количество. Чтобы помочь справиться с этим изобилием терминологии, все способы классификации имен сведены в табл. 9.1. К счастью, многие вопросы, касающиеся шаблонов C++, станут гораздо понятнее, если ознакомиться с основными концепциями именования. 1. Имя является полным (квалифицированным) именем (qualified name), если область видимости, которой оно принадлежит, явно указывается либо с помощью оператора разрешения области видимости (: :), либо с помощью оператора доступа к членам класса (. или ->). Например, this->count— полное имя, a count— нет (даже если само по себе count в действительности является ссылкой на член класса).
144 Глава 9. Имена в шаблонах/ 2. Имя является зависимым именем, если оно каким-либо образом зависит от napaf метра шаблона. Например, std: :vector<T>: : iterator—зависимое имя, если Т — параметр шаблона, и независимое, если Т является известным значением, заданным с помощью конструкции typedef (например, int). Таблица 9.1. Систематизация имен Классификация Пояснения и примечания Идентификатор Идентификатор функции оператора Идентификатор функции преобразования типа Идентификатор шаблона Неполный (unqualified) идентификатор Полный (qualified) идентификатор Имя, которое содержит только неразрывные последовательности букв, знаков подчеркивания (_) и цифр. Идентификатор не может начинаться с цифры; кроме того, некоторые идентификаторы зарезервированы в реализации языка: их нельзя самостоятельно вводить в программы (используйте простое правило: избегайте идентификаторов, начинающихся с подчеркиваний и двойных подчеркиваний). Понятие "буква" интерпретируется расширенно: сюда включаются специальные универсальные имена символов (universal character names — UCN), с помощью которых кодируются знаки из неалфавитных языков Ключевое слово operator, за которым следует символ, обозначающий оператор, например operator new или operator [ ]. Многие операторы имеют альтернативные представления. Например, operator & можно записать как operator bitand, даже если он обозначает унарный оператор получения адреса Используется для обозначения определенного пользователем неявного оператора преобразования, например operator int&, который может также быть представлен как operator int bitand Имя шаблона, за которым следуют аргументы шаблона, заключенные в угловые скобки, например List<T,int,o>. (Строго говоря, для имени идентификатора шаблона стандартом C++ разрешены только простые идентификаторы. Однако это, возможно, недосмотр: должен быть разрешен также идентификатор функции оператора, т.е. opera tor+<X< int > >) Обобщение идентификатора. Неполный идентификатор может быть любым из приведенных выше видов идентификаторов (идентификатор, идентификатор функции оператора, идентификатор функции преобразования типа или идентификатор шаблона), а также "имя деструктора" (например, записи типа -Data или ~List<T,T,N>) Полное имя, которое включает имя класса или пространства имен либо оператор разрешения глобальной области видимости. Заметим, что такое имя может быть полным само по себе. Примеры: : :х, S: :х, Аггау<Т>: :у и : :N: :А<Т>: : z.
9.2. Поиск имен 145 Окончание табл. 9.1 Классификация Пояснения и примечания Полное имя Неполное имя Имя Зависимое имя Независимое имя Этот термин в стандарте не определен, но мы используем его для обозначения имен, которые подвергаются так называемому полному (квалифицированному) поиску. В частности, это могут быть полные или неполные идентификаторы, которые используются после уточнения с помощью явного оператора доступа (. или ->). Примеры: S: :x, this->f и р->А: :т. Однако просто class_mem в контексте, когда он неявно эквивалентен this->class_mem, не является полным именем: доступ к члену класса должен быть явным Неполный идентификатор, который не является полным именем. Это нестандартный термин, но он соответствует именам, которые подвергаются тому, что в стандарте именуется неполным (unqualified) поиском Полное или неполное имя Имя, которое каким-либо образом зависит от параметра шаблона. Очевидно, что любое полное или неполное имя, которое явно содержит параметр шаблона, является зависимым. Более того, полное имя, которое включает оператор доступа к члену класса (. или - >), является зависимым, если тип выражения в левой части оператора доступа зависит от параметра шаблона. В частности, b в this->b является зависимым именем, когда оно присутствует в шаблоне. И наконец, идентификатор ident в вызове вида ident (х, у, z) является зависимым именем тогда и только тогда, когда любое из выражений аргументов имеет тип, который зависит от параметра шаблона Имя, которое не является зависимым согласно данному выше определению ^^^^^ С этой таблицей полезно познакомиться хотя бы для того, чтобы получить некоторое представление о терминах, которые иногда используются при описании тем, касающихся шаблонов C++. Однако запоминать точное значение каждого термина вовсе не обязательно. Если возникнет необходимость, всегда можно вернуться к данной таблице. 9.2. Поиск имен Существует много незначительных деталей, касающихся поиска имен в C++, но здесь мы остановимся только на нескольких основных концепциях. Подробностям будем уделять внимание только в случаях, когда нужно убедиться в правильности интуитивной трактовки, и в "патологических" случаях, которые, тем не менее, описаны в стандарте. Поиск полных имен проводится в области видимости, вытекающей из уточняющей конструкции. Если эта область видимости является классом, то поиск также проводится и в базовых классах. Однако при поиске полных имен не рассматриваются области ви-
146 Глава 9. Имена в шаблон щ. 1 димости, охватывающие данную. Основной принцип такого поиска иллюстрируется щ№ веденным ниже кодом. int х; class В { public: int i; }; class D : public В { >; void f(D* pd) { pd->i =3; // Будет найдено B::i D::x =2; // ОШИБКА: ::х из охватывающей // области видимости найдено не будет } Поиск же неполных имен, напротив, выполняется в последовательно расширяющихся областях видимости, охватывающих данную (однако в определениях функций-членов сначала проводится поиск в области видимости класса и его базовых классов, а уже затем в охватывающих областях видимости). Такая разновидность поиска называется обычным поиском (ordinary lookup). Приведенный ниже пример иллюстрирует главную идею, лежащую в основе обычного поиска. extern int count ; // (1) int lookup_example(int count) // (2) { if (count < 0) { int count =1; // (3) lookup_example(count); // Неполное имя count // ссылается на (3) } return count + ::count; // Первое, неполное count // ссылается на (2) } // Второе, полное count ^ // ссылается на (1) Современные методы поиска неполных имен в дополнение к обычному поиску могут включать так называемый поиск, зависящий от аргументов (argument-dependent lookup — ADL)X. Прежде чем перейти к подробному рассмотрению ADL, рассмотрим механизм этого поиска на примере шаблона max (). Этот поиск также называется поиском Кёнига (или расширенным поиском Кёнига) в честь Эндрю Кёнига (Andrew Koenig), который впервые предложил вариант данного механизма.
9.2. Поиск имен 147 template <typename T> inline T const& max(T constfc a, T constfc b) { return a < b ? b : a; } Предположим, что нам необходимо применить этот шаблон к типу, определенному в другом пространстве имен. namespace BigMath { class BigNumber { }; bool operator < (BigNumber const&, BigNumber constfc); } using BigMath::BigNumber; void g(BigNumber const& a, BigNumber const& b) { BigNumber x = max(a,b); } Проблема заключается в том, что шаблону max () ничего не известно о пространстве имен BigMath и с помощью обычного поиска не будет найден оператор <, применимый к значениям типа BigNumber. Если не ввести некоторые специальные правила, такие ситуации в значительной степени сокращают применимость шаблонов в контексте пространств имен C++. Поиск ADL является ответом C++ на необходимость введения таких специальных правил. 9.2.1. Поиск, зависящий от аргументов ADL применяется только к неполным именам, которые выглядят наподобие имени функции, не являющейся членом, в вызове функции. Если при обычном поиске будет найдено имя функции-члена или имя типа, то ADL не применяется. ADL также запрещен, если имя функции, которая должна быть вызвана, заключено в круглые скобки. В противном случае, если после имени следует заключенный в круглые скобки список выражений аргументов, ADL выполняется путем поиска имени в пространствах имен и классах, "ассоциированных" с типами аргументов вызова. Точное определение этих ассоциированных пространств имен и ассоциированных классов будет дано позже, но интуитивно их можно определять как все пространства имен и классы, которые очевидным образом непосредственно связаны с данным типом. Например, если тип является указателем на класс X, то ассоциированные классы и пространство имен будут включать X, а также все пространства имен и классы, к которым принадлежит X.
148 Глава 9. Имена в шаблонах ' Точное определение множества ассоциированных пространств имен и ассоциированных классов для данного типа регламентируется приведенными ниже правилами. • Для встроенных типов это пустое множество. • Для указателей и массивов множество ассоциированных пространств имен и классов — это пространства имен и классы типа, на который указывает указатель или который является типом элемента массива. • Для перечислимых типов ассоциированным пространством имен является пространство имен, в котором объявлено перечисление. Для членов классов ассоциированным классом является включающий их класс. • Для классов (включая объединения) множеством ассоциированных классов является сам тип класса, включающий его класс, а также все непосредственные или опосредованные базовые классы. Множество ассоциированных пространств имен— это пространства имен, в которых объявлены ассоциированные классы. Если класс является инстанцированным экземпляром шаблона, то сюда включаются также типы аргументов типа шаблона, а также классы и пространства имен, в которых объявлены шаблонные аргументы шаблона. • Для функций множества ассоциированных пространств имен и классов включают пространства имен и классы, ассоциированные со всеми типами параметров, а также ассоциированные с типами возвращаемых значений. > • Для указателей на члены класса X множества ассоциированных пространств имен и классов включают пространства имен и классы, ассоциированные с X в дополнение к ассоциированным с типом члена класса (если это тип указателя на функцию-член, то учитываются также типы параметров и возвращаемых значений этой функции-члена). При применении ADL осуществляется последовательный поиск имени во всех ассоциированных пространствах имен так, как если бы это имя было уточнено с помощью каждого их этих пространств имен (директивы using при этом игнорируются). Этот механизм иллюстрируется приведенным ниже примером. // details/adl.cpp #include <iostream> namespace X { template<typename T> void f(T); } namespace N { using namespace X; enum E { el }; void f(E) { std::COUt << "N::f(N::E) called\n"; }
9.2. Поиск имен 149 void f(int) { std::cout « "::f(int) called\n"; } int main() { ::f(N::el); // Полное имя функции: ADL не используется f(N::el); // Обычный поиск дает ::f(), a ADL — // N::£(), которой отдается предпочтение } Заметим, что в данном примере директива using в пространстве имен N при выполнении ADL игнорируется. Следовательно, X: : f () никогда даже не будет рассматриваться как кандидат для вызова в main (). 9.2.2. Внесение дружественных имен Объявление дружественной функции может быть первым объявлением функции- кандидата при поиске. В этом случае считается, что функция объявлена в области видимости ближайшего пространства имен (или, возможно, в глобальном пространстве имен), в которую входит класс, содержащий объявление дружественной функции. Относительно спорный вопрос— должно ли это объявление быть видимым в области видимости, в которую оно "вносится". Эта проблема главным образом относится к шаблонам. Рассмотрим пример. template<typename T> class С { friend void f(); friend void f(C<T> constfc); } ; void g(C<int>* p) { f(); // Видима ли f()? f(*p); // Видима ли f(C<int> constfc)? } Проблема заключается в том, что если объявления дружественных конструкций видимы в охватывающем пространстве имен, то инстанцирование шаблона класса может сделать видимыми объявления обычных функций. Некоторым программистам это может показаться странным, и поэтому в стандарте C++ указывается, что объявления дружественных конструкций обычно не делают имя видимым в охватывающей области видимости. Однако существует интересный прием программирования, который зависит от объявления (и определения) функции только в объявлении дружественной конструкции (см. раздел 11.7,
150 Глава 9. Имена в шаблонах стр. 201). Поэтому в стандарте также указано, что дружественные функции обнаруживаются, когда класс, по отношению к которому они являются дружественными, принадлежит к числу ассоциированных классов, рассматриваемых при ADL. Рассмотрим еще раз последний пример. Вызов f () не имеет ассоциированных классов или пространств имен, поскольку не имеет аргументов: это некорректный вызов в нашем примере. Однако вызов f (*p) имеет ассоциированный класс C<int> (поскольку это тип *р) и с ним также ассоциировано глобальное пространство имен (поскольку это пространство имен, в котором объявлен тип *р). Следовательно, объявление второй дружественной функции может быть найдено, если класс C<int> в действительности полностью инстанцирован до вызова. Чтобы обеспечить выполнение этого условия, предполагается, что вызов, инициирующий поиск дружественных конструкций в ассоциирован- ных классах, фактически вызывает инстанцирование класса (если оно еще не выполнено). 9.2.3. Внесение имен классов Имя класса "внесено" в область видимости этого класса и, следовательно, является доступным в данной области видимости как неполное имя (однако оно недоступно в качестве полного имени, поскольку это запись, которая используется для обозначения конструкторов). Например: // details/injееt.cpp #include <iostream> int C; class С { private: -int i[2] ; public: static int f() { return sizeof(C); } }; int f() { return sizeof(C); } int main() { лотя это очевидным образом входило в намерения тех, кто писал стандарт C++, из самог стандарта это не ясно.
9.3. Синтаксический анализ шаблонов 151 Std::COUt « "C::f() = « « C::f() << " , " « "::f() = " « ::f() « std::endl; } Функция-член С: : f () возвращает размер типа С, в то время как функция : : f () возвращает размер переменной С (другими словами, размер объекта типа int). Шаблоны классов также имеют внесенные имена классов. Однако они еще более непривычны, чем обычные внесенные имена классов: за ними могут идти аргументы шаблона (в этом случае они являются внесенными именами шаблона класса), но если за ними не следуют аргументы шаблона, то они представляют класс с использованием параметров шаблонов в качестве аргументов (или, при частичной специализации, с использованием аргументов специализации). Это поясняет следующую ситуацию: template<template<typename> class TT> class X { }; template<typename T> class С { С а; // ВЕРНО: то же, что и "С<Т> а; " C<void> b; // ВЕРНО Х<С> с ; // ОШИБКА: С без списка аргументов шаблона //не определяет шаблон Х<::С> d; // ОШИБКА: <: - диграф [ Х< ::С> е; // ВЕРНО: требуется пропуск между < и :: } Обратите внимание на то, как неполное имя ссылается на внесенное имя, и на то, что имя шаблона не рассматривается, если за ним не следует список аргументов. Однако можно заставить компилятор найти имя шаблона, если использовать квалификатор области видимости файла : :, хотя при таком способе необходимо быть предельно внимательным и не допустить образование диграфа <:, который интерпретируется как левая квадратная скобка. Диагностика таких (пусть и относительно редких) ошибок весьма затруднительна. 9.3. Синтаксический анализ шаблонов В большинстве случаев компилятор выполняет два фундаментальных действия — лексический и синтаксический анализ текста программы. При лексическом анализе исходный текст программы рассматривается как последовательность символов, из которой генерируется последовательность лексем. Например, если компилятор встречает последовательность символов int* p = 0;, лексический анализатор разделяет ее на отдельные лексемы — ключевое слово int, символ оператора *, идентификатор р, символ оператора =, целочисленный литерал 0 и символ ;. После лексического анализа в дело вступает синтаксический анализ, который ищет в последовательности лексем известные разрешенные языковые конструкции путем рекурсивной свертки лексем или обнаруженных конструкций в конструкции более высокого
152 Глава 9. Имена в шаблонах уровня3. Например, лексема 0 является корректным выражением, комбинация символа *, за которым следует идентификатор р, является корректным объявлением переменной; объявление, за которым следует знак "=", сопровождаемый выражением "О", в свою очередь является корректным объявлением. И наконец, ключевое слово int является известным именем типа; когда за ним следует объявление *р = 0,,мы получаем инициализирующее объявление переменной р. 9.3.1. Зависимость от контекста в нешаблонных конструкциях Как вы, вероятно, знаете или предполагаете, поиск лексем осуществляется легче, чем синтаксический анализ. К счастью, синтаксический анализ достаточно хорошо разработан теоретически, так что использование теории синтаксического анализа позволяет довольно легко разрабатывать синтаксические анализаторы для множества различных языков программирования. Полнее всего теория синтаксического анализа разработана для так называемых контекстно-свободных языков, в то время как C++ является контекстно- зависимым языком программирования. В связи с этим компилятор C++ использует таблицы символов при лексическом и синтаксическом анаЛизе. Когда проводится анализ объявления, оно вносится в таблицу символов. После этого при обнаружении идентификатора из таблицы символов можно легко определить его тип. Например, если компилятор C++ обнаруживает во входном потоке х*, лексический анализатор ищет х в таблице символов. Если это тип, то синтаксический анализатор получает на входе приведенную ниже последовательность. identifier, type, x symbol, * Компилятор делает заключение, что это начало объявления. Однако, если оказывается, что х не является типом, синтаксический анализатор получает от лексического другу10 последовательность: identifier, nontype, x symbol, * Данная конструкция может быть корректно разобрана синтаксически только как умножение. Детали применяемых правил зависят от конкретной стратегии реализации, но суть остается именно такой. Еще один пример контекстной чувствительности иллюстрируется в следующем выражении: Х<1>(0) Подробнее о процессах лексического и синтаксического анализа вы можете прочесть в книге Ахо А., Сети Р., Ульман Д. Компиляторы: принципы, технологии и инструменты. — М. : Издательский дом "Вильяме", 2001. —Прим. ред.
9.3. Синтаксический анализ шаблонов 153 Если X является именем шаблона класса, то в предыдущем выражении целое 0 приводится к типу Х<Г>, сгенерированному из этого шаблона. Если X не является шаблоном, то предыдущее выражение эквивалентно следующему: (Х<1)>0 Другими словами, X сравнивается с 1, а результат этого сравнения— "истина" или "ложь" (которые в данном случае неявно преобразуются в 1 или 0) — сравнивается с 0. Хотя код, подобный приведенному, используется редко, он является корректным кодом C++ (и, кстати, корректным кодом С). Следовательно, синтаксический анализатор C++ будет проводить поиск имен, находящихся перед <, и интерпретировать < как угловую скобку, только если имя является именем шаблона; в противном случае < служит обычным символом оператора "меньше чем". Такая форма контекстной чувствительности — одно из неудачных последствий выбора угловых скобок для ограничения списка аргументов шаблона. Ниже приведен пример еще одного такого следствия. template<bool B> class Invert { public: static bool const result = !B; }; void g() { bool test = B<(1>0)>:: result; // Требуются скобки! } Если опустить скобки в выражении В< (1>0) >, то символ "больше чем" будет ошибочно принят в качестве закрывающего список аргументов шаблона. Это сделало бы код неверным, поскольку компилятор воспринял его как эквивалент ((В<1>) ) 0>: : result . Лексический анализатор также не лишен проблем, связанных с угловыми скобками. Ранее (см. раздел 3.3, стр. 49) уже отмечалась необходимость вставки пробела в случае вложенных идентификаторов шаблона наподобие List<List<int> > a; // л —пробел обязателен! Пробел между двумя закрывающими угловыми скобками обязателен: без этого промежутка два символа > образуют лексему сдвига вправо » и, следовательно, никогда не будут интерпретироваться как две отдельные лексемы. Это следствие так назьшаемого принципа поиска лексемы максимальной длины. Он заключается в том, что компилятор должен собирать в лексему настолько много последовательных символов, насколько это возможно. V 4 Отметим, что двойные скобки, которые используются для того, чтобы избежать синтаксического анализа выражения (в<1>) о как оператора приведения, — еще один источник синтаксической неопределенности.
154 Глава 9. Имена в шаблонах Именно этот вопрос наиболее часто становится камнем преткновения для начинающих пользователей шаблонов. Ряд компиляторов C++ модифицированы таким образом, что распознают эту ситуацию и интерпретируют » в такой ситуации как два отдельных символа > (выводя предупреждение о том, что это некорректный код C++). Комитет по стандартизации C++ обсуждает также вопрос о том, чтобы при пересмотре стандарта сделать это поведение обязательным (см раздел 13.1, стр. 231). Еще один пример неприятностей, связанных с принципом поиска лексемы максимальной длины: с угловыми скобками следует аккуратно использовать оператор разрешения контекста (: :). class X { >; List<::X> many__X ; // СИНТАКСИЧЕСКАЯ ОШИБКА! Здесь проблема заключается в том, что последовательность символов <: является так называемым диграфом5, т.е. альтернативным представлением символа [. Следовательно, компилятор фактически получает выражение, эквивалентное List [ :X> many__X;, которое лишено всякого смысла. Здесь также решением проблемы будет добавление пробельного символа. List< ::X> many_X; // А--пробел обязателен! 9.3.2. Зависимые имена типов Проблемы с именами в шаблонах не всегда удается удовлетворительно классифицировать. В частности, один шаблон не может заглянуть в другой шаблон, поскольку содержимое последнего может оказаться некорректным в силу явной специализации (более подробно данный вопрос освещен в главе 12, "Специализация и перегрузка"). Ниже приведен несколько искусственный пример, иллюстрирующий данное утверждение. template<typename T> class Trap { public: enum { x }; // (1) x не является типом }; template<typename T> class Victim { public: N int y; void poof() { Диграфы были добавлены в язык для того, чтобы упростить ввод исходного текста C++ на различных типах клавиатур, в частности на тех, где отсутствуют некоторые символы (такие, как #, [ и ]).
9.3. Синтаксический анализ шаблонов 155 Тгар<Т>::х*у; // (2) Объявление или умножение? } }; templateo class Trap<void> { // Специализация! public: typedef int x; // (3) Здесь х является типом }; void boom(Trap<void>& bomb) { bomb.poof(); } Когда компилятор выполняет синтаксический анализ строки (2), он должен решить, с какой конструкцией он имеет дело — с объявлением или умножением. Это решение, в свою очередь, основывается на том, является ли зависимое полное имя Тгар<Т>: :х именем типа. Неплохо бы, конечно, заглянуть в этот момент в шаблон Trap; тогда бы вы увидели, что, согласно строке (1), Тгар<Т>: :х не является типом, поэтому для строки (2) остается только умножение. Однако несколько позже это заключение оказывается ложным, поскольку для случая, когда Т является void, имеется специализация шаблона, в которой Тгар<Т>: : х представляет собой тип int. В определении языка эту проблему можно решить следующим образом: указать, что в общем случае зависимое имя не является типом, за исключением тех ситуаций, когда это имя предваряется ключевым словом typename. Если же оказывается, что после подстановки аргументов шаблона это имя не является именем типа, значит, программа ошибочна и компилятор C++ должен сообщить об этом в момент инстанцирования шаблона. Заметим, что такое применение typename отличается от использования этого ключевого слова Для обозначения параметров шаблона, являющихся типом. В отличие от указания параметров типа, заменить typename ключевым словом class в описанной ситуации нельзя. Предварять имя ключевым словом typename необходимо в следующих случаях: • когда это имя находится в шаблоне; • если оно является полностью квалифицированным; • если оно не используется в качестве списка спецификаций базового класса или в списке инициализаторов членов^ определении конструктора; • если оно является зависимым от параметра шаблона. Кроме того, предварение ключевым словом typename не разрешается, если справедливы по крайней мере первые три условия. Чтобы проиллюстрировать это, рассмотрим следующий (содержащий ошибки) пример: template<typename1 T> struct S: typename2 X<T>::Base {
156 Глава 9. Имена в шаблонах S(): typename3 Х<Т>::Base(typename4 X<T>::Base(0)) {} typename5 X<T> f () { typename6 X<T>::C * р; // Объявление указателя р X<T>::D * q; // Умножение! } typename7 X<int>::C * s; }; struct U { typename8 X<int>::C * pc; }; Каждое typename — корректное либо нет — для удобства указания помечено подстрочным номером. Первое typenamei означает параметр шаблона. К этому первому использованию typename приведенные выше правила не относятся. Второе и третье включение typename не разрешается согласно третьему правилу. Именам базовых классов в этих двух контекстах не может предшествовать typename. Однако typename4 должно быть применено. Здесь имя базового класса не используется для обозначения того, что должно инициализироваться или порождаться из чего-либо, а является частью выражения для создания временного объекта Х<Т>: : Base из аргумента 0 (если угодно — это можно рассматривать как разновидность преобразования типов). Пятое typename запрещено, поскольку имя, которое за ним следует (Х<Т>), не является квалифицированным именем. Шестое вхождение требуется, если это выражение предназначено для объявления указателя. В следующей строке ключевое слово typename отсутствует, поэтому она интерпретируется компилятором как умножение. Седьмое typename необязательно, поскольку оно удовлетворяет первым трем правилам, и не удовлетворяет четвертому. И наконец, typename8 запрещено, поскольку оно не используется внутри шаблрна. 9.3.3. Зависимые имена шаблонов Проблема, во многом подобная той, с которой мы столкнулись в предыдущем разделе, возникает и в случае, когда имя шаблона является зависимым. В общем случае от компилятора C++ требуется; чтобы он интерпретировал знак <, следующий за именем шаблона, как начало списка аргументов шаблона; в противном случае это оператор "меньше чем". Как и в случае с именами типов, компилятор должен предполагать, что зависимое имя не ссылается на шаблон, если программист не обеспечивает дополнительную информацию с помощью ключевого слова template. template<typename T> class Shell { public: template<int N> class In { public: template<int M>
9.3. Синтаксический анализ шаблонов 157 class Deep { public: virtual void f(); }; }; }; template<typename T, int N> class Weird { public: void easel (Shell<T>:-.template In<N>: :template Deep<N>*p) { p->template Deep<N>::f(); // Запрет виртуального вызова } void case2 (Shell<T>: :template In<T>: -.template Deep<T>&p) { p.template Deep<N>::f(); // Запрет виртуального вызова } }; В этом несколько запутанном примере показаны ситуации, когда для операторов уточнения имени (::,-> и .) может потребоваться использовать ключевое слово template. В частности, оно требуется всякий раз, когда тип имени или выражения, предшествующего оператору уточнения, зависит от параметра шаблона, а имя, которое следует за оператором, является идентификатором шаблона (другими словами, имя шаблона, за которым следуют аргументы шаблона в угловых скобках). Например, в выражении р.template Deep<N>::f() тип р зависит от параметра шаблона Т. Следовательно, компилятор C++ не может проводить поиск Deep для выяснения, является ли это имя шаблоном, и необходимо явно указать это с помощью предшествующего имени ключевого слова template. Без этого предваряющего ключевого слова синтаксический анализ р. Deep<N>: : f () проводится следующим образом: ( (р. Deep) <N) >f (). Заметим также, что может потребоваться использовать ключевое слово template несколько раз в пределах одного полного имени, поскольку сами по себе квалификаторы могут быть уточнены посредством зависимого квалификатора (объявления параметров easel и case2 в предыдущем примере). Если опустить ключевое слово template в таких ситуациях, то открывающая и закрывающая угловые скобки анализируются как операторы "меньше чем" и "больше чем". Добавим, что если ключевое слово template не является необходимым, то его использование запрещено6. Нельзя насыщать код квалификаторами шаблонов "просто так". На самом деле из текста стандарта это не очевидно, но те, кто работал над этой частью стандарта, согласны с данным утверждением.
158 Глава 9. Имена в шаблонах 9.3-4. Зависимые имена в объявлениях using Объявления using могут быть привнесены в имена из двух мест— пространств имен и классов. Случай пространств имен в данном контексте нас не интересует, поскольку не существует шаблонов пространств имен. Что касается классов, то в действительности объявления using привносятся только из базового класса в порожденный. Такие объявления using в порожденном классе ведут себя как "символические связи9' (или "ярлыки"), направленные из порожденного класса к базовому, обеспечивая таким образом членам порожденного класса доступ к соответствующему имени базового класса, как если бы оно было объявлено в порожденном классе. Краткий пример, не содержащий шаблонов, проиллюстрирует сказанное лучше, чем множество слов. class BX { public: void f(int); void f(char const*); void g(); }; class DX : private BX { public: using BX::f; }; Объявление using привносит имя f из базового класса ВХ в порожденный класс DX. * В данном случае это имя ассоциировано с двумя разными объявлениями; таким образом подчеркивается, что мы имеем дело с механизмом для имен, а не с отдельными объявлениями. Заметим также, что такой вид using-объявления может сделать доступным член класса, который в противном случае был бы недоступен. Базовый класс ВХ (и соответственно его члены) является закрытым по отношению к классу DX, за исключением функций ВХ: :f, которые введены в открытом интерфейсе DX и являются, следовательно, доступными для клиентов DX. Поскольку механизм using-объявлений перекрывает использовавшийся ранее механизм объявлений доступа, последний не рекомендован к применению (и в будущих версиях C++ может быть исключен из стандарта). class DX : private BX { public: BX::f; // Синтаксис объявлений доступа. Не // рекомендован к использованию; взамен // предлагается using BX::f Вы уже должны сами представлять проблему, возникающую, когда using- объявление привносит имя из зависимого класса. Хотя вы и знаете об имени, неизвестно, является ли оно именем типа, шаблона или чем-либо еще. tempiate<typename T> class BXT { public:
9.3. Синтаксический анализ шаблонов 159 typedef T Mystery; template<typename U> struct Magic; }; template<typename T> class DXTT: private BXT<T> { public: using typename BXT<T>::Mystery; Mystery* p; // Если бы не typename, эта строка // была бы ошибочна }; Если вы хотите, чтобы зависимое имя было введено с помощью using-объявления для обозначения типа, то должны явно указать это путем вставки ключевого слова typename. Как ни странно, но стандарт C++ не предоставляет аналогичного механизма для того, чтобы пометить такие зависимые имена как шаблоны. Приведенный ниже фрагмент кода иллюстрирует эту проблему. template<typename T> class DXTM: private ВХТ<Т> { public: using BXT<T>::template Magic; // ОШИБКА: не соответствует стандарту Magic<T>* plink; // СИНТАКСИЧЕСКАЯ ОШИБКА: Magic }; //не является известным шаблоном Наиболее вероятно, что это недосмотр и впоследствии стандарт будет изменен таким образом, чтобы рассмотренная конструкция была корректной. 9.3.5. ADL и явные аргументы шаблонов Рассмотрим приведенный ниже пример. namespace N { class X { }; template<int I> void select (X*); } void g(N::X* xp) { select<3>(xp); // ОШИБКА: ADL не выполняется В этом примере логично было бы предположить, что в вызове select<3> (xp) шаблон select () отыскивается с помощью ADL. Однако это не так, поскольку компилятор
160 Глава 9. Имена в шаблонах не может принять решение о том, что хр является аргументом вызова функции, пока не будет решено, что <3> является списком аргументов шаблона. И наоборот, невозможно решить, что <3> является списком аргументов шаблона, пока не выяснится, что select () представляет собой шаблон. Поскольку эту проблему курицы и яйца разрешить невозможно, выражение анализируется как (select<3) > (хр), что не имеет смысла. 9.4. Наследование и шаблоны классов Шаблоны классов могут порождать производные классы или сами быть производными классами. В большинстве случаев особой разницы между сценариями с использованием шаблонов и без них нет; однако есть один важный тонкий момент при порождении шаблона класса из базового класса, обращение к которому выполняется с помощью зависимого имени. Давайте сначала рассмотрим более простой случай независимых базовых классов. 9.4.1. Независимые базовые классы В шаблоне класса независимый базовый класс является классом с завершенным типом, который может быть определен без знания аргументов шаблона. Другими словами, для обозначения этого класса используется независимое имя. template<typename X> class Base { public: int basefield; typedef int T; }; class Dl: public Base<Base<void> > { // В действительности это не шаблон public: void f() { basefield =3; // Обычный доступ к } // унаследованному члену класса }; template<typename T> class D2: public Base<double> { // Независимый базовый класс public: void f() { basefield = 7; // Обычный доступ к } // унаследованному члену класса Т strange ; // Т здесь - Base<double>::Т, // а не параметр шаблона! };
9.4. Наследование и шаблоны классов 161 Поведение независимых базовых классов в шаблонах очень похоже на поведение базовых классов в обычных нешаблонных классах, однако здесь имеет место некоторая досадная неожиданность: когда поиск неполного имени выполняется в производном шаблоне, независимые базовые классы рассматриваются до списка параметров шаблона. Это означает, что в предыдущем примере член класса strange шаблона класса D2 всегда имеет тип Т, соответствующий Base<double>: :T (другими словами, int). Например, следующая функция с точки зрения C++ некорректна (при использовании предыдущих объявлений): void g (D2<int*>& d2, int* p) { { d2.strange = p; // ОШИБКА: несоответствие типов! } Такое поведение далеко не интуитивно и требует от разработчика порожденного шаблона внимания по отношению к именам в независимых базовых классах, от которых он порождается, даже когда это порождение является непрямым или имена являются закрытыми. 9.4.2. Зависимые базовые классы В предыдущем примере базовый класс был полностью определенным и не зависел от параметра шаблона. Это означает, что компилятор C++ может искать независимые имена в тех базовых классах, где видимо определение шаблона. Альтернатива (не разрешенная стандартом C++) заключается в отсрочке поиска таких имен, пока шаблон не будет инстан- цирован. Недостаток этого подхода состоит в том, что до инстанцирования откладываются все сообщения об ошибках. Поэтому в стандарте C++ указано, что поиск независимого имени, присутствующего в шаблоне, происходит немедленно после того, как компилятор столкнется с ним. Рассмотрим с учетом сказанного приведенный ниже пример. template<typename T> class DD: public Base<T> { // Зависимый базовый класс public: void f () { basefield =0; // (1) проблема... templateo class Base<bool> { public: enum { basefield }; void g(DD<bool>& d) { d.f(); // (3) ? // Явная специализация 42};// (2) Небольшой трюк
162 Глава 9. Имена в шаблонах В точке (1) имеется ссылка на независимое имя basef ield, поиск которого следует провести немедленно. Предположим, что оно найдено в шаблоне Base и связано с членом класса с типом int в этом классе. Однако после этого компилятор встречает явную специализацию данного класса. Когда это происходит, смысл члена класса basef ield изменяется ■— при том, что его старый смысл уже использован! Так, при инстанцирова- нии определения DD: : f в точке (3) выясняется, что независимое имя в точке (1) связано с членом класса типа int преждевременно— в DD<bool> не существует переменной basef ield, которой можно было бы присвоить новое значение (теперь это элемент перечисления из специализации в точке (2)), так что компилятором будет выдано сообщение q6 ошибке. Чтобы обойти эту проблему, стандарт C++ гласит, что поиск независимых имен не проводится в зависимых базовых классах7 (однако сам поиск выполняется, как только эти имена встречаются компилятором). Таким образом, соответствующий стандарту C++ компилятор выдаст диагностику в точке (1). Для исправления кода достаточно сделать имя basef ield зависимым, поскольку поиск зависимых имен может проводиться только во время инстанцирования шаблона, а к этому моменту точная специализация базового класса, где будет вестись поиск, уже будет известна. Например, в точке (3) компилятор уже будет знать, что базовым по отношению к DD<bool> является класс Base<bool>, явно специализированный программистом. Сделать имя зависимым можно, например, как показано ниже. // Вариант 1: template<typename T> class DD1: public Base<T> { public: void f(){ this->basefield = 0; } // Поиск отложен }; Еще один вариант — введение зависимости с помощью полного имени. // Вариант 2: template<typename T> class DD2: public Base<T> { public: void f() { Base<T>::basefield = 0; } }; Применение этого варианта требует особой тщательности, поскольку если неполное независимое имя используется для формирования вызова виртуальной функции, то уточнение подавляет механизм виртуального вызова и смысл программы изменяется. Несмотря на это, существуют ситуации, когда первый вариант нельзя использовать и приходится применять альтернативный. Это часть так называемого правила двухфазного поиска, в котором различаются первая фаза, когда определения шаблона встречаются впервые, и вторая фаза, когда происходит инстанцирова- ние шаблона (см. раздел 10.3.1, стр. 170).
9.4. Наследование и шаблоны классов 163 template<typename T> class В { public: enum E {el = 6, е2 = 28, еЗ = 496 }; virtual void zero(E e = el); virtual void one(E&); }; template<typename T> class D: public B<T> { public: void f() { typename D<T>::E e; // this->E синтаксически некорректно this->zero(); // D<T>::zero() подавляет виртуальность one(e); // one является зависимым именем в силу // зависимости аргумента функции } }; Заметим, что имя one в вызове one (е) зависимо от параметра шаблона просто потому, что тип одного из явно заданных аргументов вызова является зависимым. Неявно используемые аргументы по умолчанию с типом, который зависит от параметра шаблона, во внимание не принимаются, поскольку компилятор не может их проверить до тех пор, пока не будет проведен поиск, — все та же проблема курицы и яйца. Чтобы избежать таких нюансов, предпочтительно использовать префикс this-> во всех ситуациях, где это только можно, — даже для нешаблонного кода. Если вы обнаружите, что повторяющиеся квалификаторы загромождают ваш код, можно внести имя из зависимого базового класса в порожденный класс раз и навсегда. // Вариант 3: template<typename T> class DD3: public Base<T> { public: using Base<T>: -.basefield; // (1) Теперь зависимое // имя в области видимости void f() { basefield =0; } // (2) Все в порядке }; Поиск в точке (2) успешен и находит объявление using в точке (1). Однако объявление using не проверяется до инстанцирования, так что поставленная цель достигнута. Эта схема имеет несколько несущественных ограничений. Например, если осуществляется множественное наследование из нескольких базовых классов, программист должен точно указать, какой из них содержит необходимый член.
164 Глава 9. Имена в шаблонах 9.5. Заключение Первый компилятор, который действительно был способен проводить синтаксический анализ шаблонов, был разработан компанией Taligent в середине 1990-х годов. До этого — и даже после этого — большинство компиляторов интерпретировали шаблоны как последовательность лексем, которые должны были воспроизводиться в синтаксическом анализаторе во время инстанцирования. Поэтому никакой синтаксический анализ не проводился, за исключением минимально необходимого, направленного на поиск конца определения шаблона. Билл Гиббоне (Bill Gibbons), представитель компании Taligent в Комитете по стандартизации C++, был принципиальным приверженцем того, чтобы сделать шаблоны однозначно поддающимися синтаксическому анализу. Компании Taligent так и не удалось довести работу до конца, и компилятор был приобретен и завершен компанией Hewlett-Packard (HP), став компилятором аС++. Компилятор аС++ быстро завоевал признание благодаря, помимо прочего, высококачественной диагностике. Это признание объясняется также тем, что диагностика шаблонов в этом компиляторе не всегда откладывается до момента инстанцирования шаблона. Относительно рано в процессе разработки шаблонов Том Пеннелло (Tom Pennello) — широко известный специалист по шаблонам из компании Metaware — обратил внимание на некоторые проблемы, связанные с угловыми скобками. Страуструп (Stroustrup) также обращается к этим вопросам [34] и доказывает, что обычно предпочтение отдается угловым, а не круглым скобкам. Однако существуют другие возможности, и Пеннелло, в частности на конференции в Далласе в 1991 году, предлагал использовать фигурные скобки (например, List {: : X})8. В то время эта проблема была мало распространена в связи с тем, что шаблоны-члены не были разрешены. В результате комитет отклонил предложение заменить угловые скобки фигурными. Правило поиска имен для независимых имен и зависимых базовых классов, описанное в разделе 9.4.2, было внесено в стандарт в 1993 году и описано для широкой публики в работе Бьярна Страуструпа [34] в начале 1994 года; первая же общедоступная реализация этого правила появилась только в 1997 году, когда HP включила ее в свой компилятор аС++. Поиск, зависящий от аргумента (ADL), первоначально был предложен Эндрю Кёни- гом (Andrew Koenig) (поэтому ADL иногда называют поиском Кёнига — Koenig lookup) только для операторных функций. Мотивировка была прежде всего эстетической: явно квалифицированные имена операторов с охватывающими пространствами имен в лучшем случае смотрятся ужасно (например, вместо а+b приходится писать N: :operator+ (a,b)), а требование объявлений .using для каждого оператора приводит к чрезвычайно громоздкому коду. Поэтому было решено, что поиск операторов должен проводиться в пространствах имен, связанных с аргументами. Позже ADL был расширен для имен обычных функций. Обобщенные правила ADL называются также расширенным поиском Кёнига. Фигурные скобки тоже не полностью избавляют от проблем. В частности, синтаксис специализации шаблонов классов требовал бы внесения существенных изменений.
Глава 10 Инстанцирование Инстанцирование (instantiation) шаблонов— это процесс, при котором на основе обобщенного определения шаблонов генерируются типы и функции1. В C++ концепция инстанцирования шаблонов играет фундаментальную роль, однако она несколько запутана. Одна из основных причин состоит в том, что определения генерируемых шаблоном элементов не сосредоточены в одном месте исходного кода. Местонахождение определения шаблона, его использования и определения аргументов — все это играет роль. В настоящей главе объясняется, как организовать исходный код для надлежащего использования шаблонов. Кроме того, здесь представлены различные методы, которые используются в большинстве современных компиляторов C++ для инстанцирования шаблонов. Хотя все эти методы семантически эквивалентны, неплохо понимать основные принципы, лежащие в основе стратегии, которой придерживается ваш компилятор. В процессе реализации механизм инстанцирования обрастает набором мелких особенностей и, следовательно, подвергается влиянию конечных спецификаций языка C++. ЮЛ. Инстанцирование по требованию Когда компилятор C++ встречается с использованием специализации шаблона, он создает ее, подставляя вместо параметров шаблона необходимые аргументы . Эти действия выполняются автоматически и не требуют внесения каких бы то ни было указаний в пользовательский код или в определение шаблона. В силу указанной особенности, т.е. инстанцирования шаблона по требованию, которое иногда называют неявным (implicit) или автоматическим (automatic) инстанцированием, шаблоны C++ стоят особняком по отношению к подобным возможностям других компилируемых языков программирования. Иногда термин инстанцирование применяется также для обозначения процесса создания объектов типов. Однако в данной книге этот термин всегда будет относиться к шаблонам. Термин специализация (specialization) применяется в обобщенном смысле. Под ним подразумевается конкретный экземпляр шаблона (см. главу 7, "Основные термины в области шаблонов"). Этот термин не относится к механизму явной специализации (explicit specialization), описываемой в главе 12, "Специализация и перегрузка".
166 Глава 10. Инстанцирование При инстанцировании по требованию компилятор обычно нуждается в доступе к полному определению (а не только к объявлению) шаблона и некоторых его членов в том месте, где этот шаблон используется. Рассмотрим небольшой исходный текст. template<typename T> class С; // (1) Только объявление C<int>* р = 0; // (2) Все в порядке: объявление // C<int> не требуется template<typename T> class С { public: void f(); // (3) Объявление члена }; // (4) Определение шаблона // класса завершено void g (C<int>& с) // (5) Используется только // объявление шаблона { c.f (); // (б) Используется определение // шаблона класса; нужно } // определение C::f() В точке (1) доступно только объявление шаблона, но не его определение (такое объявление иногда называют предварительным (forward declaration)). Как и для обычных классов, определение шаблона класса может и не находиться в области видимости для объявления указателей или ссылок на данный класс (как это сделано в точке (2)). Например, для указания типа, которому принадлежит параметр функции g (), не требуется полное определение шаблона С. Однако, как только компоненту понадобится информация о размере специализации шаблона, или при доступе к члену такой специализации, нужно, чтобы определение шаблона класса находилось полностью в области видимости. Этим объясняется, что в точке (6) исходного кода должно быть доступно определение шаблона класса; в противном случае компилятор не в состоянии проверить наличие и доступность членов (ни закрытых, ни защищенных). Приведем еще одно выражение, требующее инстанцирования предыдущего шаблона класса, чтобы узнать размер конструкции C<void>. C<void>* p = new C<void>; В данном случае инстанцирование необходимо для того, чтобы компилятор мог определить размер объекта C<void>. Возможно, вы заметили, что для данного конкретного шаблона тип аргумента X, который подставляется вместо параметра Т, не влияет на размер шаблона, поскольку в любом случае класс С<Х> будет пустым. Однако от компилятора не требуется, чтобы он был способен это определить. Кроме того, в данном примере при инстанцировании необходимо определить, доступен ли конструктор по умолчанию для класса C<void>, а также убедиться, что в этом классе не объявлены закрытые операторы new или delete.
10.2. Отложенное инстанцирование 167 Необходимость доступа к члену шаблона класса не всегда удается явно проследить на основе исходного кода. Например, для разрешения перегруженной функции в C++ требуется, чтобы типы классов, которым принадлежат параметры функции-кандидата, находились в области видимости. template<typename T> class С { public: C(int); // Конструктор, который вызывается с одним // параметром, можно использовать для неявного }; // преобразования типов void candidate(C<double> constfc); // (1) void candidate(int) {} // (2) int main () { candidate(42); // Могут быть вызваны обе функции, // объявления которых приведены выше } Вызов функции candidate (42) будет разрешен с помощью объявления (2). Однако объявление (1) также можно инстанцировать, чтобы проверить, подходит ли оно для разрешения вызова (благодаря тому, что конструктор с одним аргументом способен неявно преобразовать аргумент 42 в rvalue типа C<double>). Заметим, что компилятор может (но не обязан) выполнить инстанцирование, даже если способен обойтись при разрешении вызова и без него (в приведенном примере именно так и происходит; предпочесть неявное преобразование типов их точному совпадению невозможно). Заметим также, что инстанцирование экземпляра класса C<double> может привести к ошибке (что, возможно, покажется удивительным). 10.2. Отложенное инстанцирование Приведенные примеры иллюстрируют требования, которые существенно не отличаются от требований при использовании обычных, не шаблонных классов. Во многих случаях нужно, чтобы класс был завершенным. В том случае, когда класс задан с помощью шаблона, компилятор генерирует полное определение класса с помощью определения шаблона класса. В связи с этим возникает вопрос: какая часть шаблона инстанцируется? Можно было бы ответить так: ровно столько, сколько необходимо. Другими словами, при инстанцировании шаблонов компилятору следует быть максимально "ленивым". Рассмотрим, что это означает. В процессе неявного инстанцирования шаблона класса инстанцируются все объявления его членов, но не соответствующие определения. Из этого правила есть несколько исключений. Во-первых, если в шаблоне класса содержится безымянное объединение,
168 Глава 10. Инстанцирование члены определения этого объединения также инстанцируются3. Другое исключение связано с виртуальными функциями-членами. При инстанцировании шаблона класса определения этих функций могут как инстанцироваться, так и нет. Во многих реализациях эти определения будут инстанцироваться в силу того, что внутренняя структура, обеспечивающая механизм виртуальных вызовов, требует, чтобы виртуальные функции существовали в виде объектов, доступных для связывания. При инстанцировании шаблонов аргументы функции по умолчанию рассматриваются отдельно. В частности, они не инстанцируются, если не вызывается именно та функция (или функция-член), в которой применяется аргумент по умолчанию. Они не инстанцируются и в том случае, когда при вызове функции аргументы указываются явным образом, т.е. аргументы по умолчанию не используются. Приведем пример, иллюстрирующий все упомянутые случаи. // details/lazy.cpp template <typename T> class Safe { >; template <int N> class Danger { public: typedef char Block[N]; // При N<=0 — ошибка Ь' template <typename T, int N> class Tricky { public: virtual -Tricky() { } void no_body_here(Safe<T> = 3); void inclass() { Danger<N> no__boom__yet ; } // void error() { Danger<0> boom; } // void unsafe(T (*p)[N]); T operator->(); // virtual Safe<T> suspect(); struct Nested { Danger<N> pfew; }; Безымянные объединения всегда представляют собой особый случай в том плане, что их члены всегда можно рассматривать как члены класса, в котором эти объединения содержатся. Безымянное объединение — это, по сути, конструкция, с помощью которой сообщается, что некоторые члены класса совместно используют одно и то же место в памяти.
10.2. Отложенное инстанцирование 169 union { // Безымянное объединение int align; Safe<T> anonymous; }; }; int main() { Tricky<int, 0> ok; } Сначала рассмотрим приведенный выше пример без функции main (). Стандартный компилятор C++ обычно компилирует определения шаблонов, чтобы проверить правильность синтаксиса и соблюдение общих семантических ограничений. Однако при проверке ограничений, в которых участвуют параметры шаблона, компилятор исходит из того, что "все обстоит наилучшим образом". Например, параметр N, с помощью которого в классе Danger определяется член Block, может быть равным нулю или отрицательным (что привело бы к ошибке), однако предполагается, что это не так. Аналогично, сомнительной является спецификация аргумента по умолчанию (= 3) в объявлении члена no__body__here (), поскольку шаблон Safe не инициализируется целым типом! Однако предполагается, что для обобщенного определения класса Saf е<Т> аргумент по умолчанию не понадобится. Если бы объявление функции-члена error () не было закомментировано, компиляция шаблона, в котором оно находится, привела бы к ошибке. Это объясняется тем, что для использования шаблона Danger<0> требуется полностью определить класс Danger<0>, а в результате генерации этого класса предпринимается попытка задать тип массива с нулевым количеством элементов. Это происходит даже в том случае, когда функция-член error () не используется и, следовательно, не инстан- цируется. Ошибка, о которой идет речь, происходит в процессе обработки обобщенного шаблона. Объявление же функции-члена unsafe (T (*p) [N] ) не представляет проблемы до тех пор, пока вместо параметра шаблона N не подставляется конкретное значение. Теперь проанализируем, что происходит при добавлении функции main (). В ходе ее трансляции компилятор подставляет в шаблон Tricky вместо параметра Т тип int, a вместо параметра N— значение 0. Определения всех членов не понадобятся, однако конструктор по умолчанию (объявленный в данном примере неявным образом) и деструктор по умолчанию, несомненно, вызываются. Таким образом, должен быть обеспечен доступ к ним (в нашем случае так и есть). На практике должно быть предоставлено также определение виртуальных членов; в противном случае, скорее всего, произойдет ошибка компоновки. Если бы не было закомментировано объявление виртуальной функции- члена suspect (), определение которой отсутствует, то это привело бы к ошибке. Определения членов inclass () и struct Nested требуют полного определения класса Danger<0> (в котором, как вы уже знаете, содержится недопустимое определение типа). Однако, поскольку эти определения не используются, они не генерируются и ошибки не возникает. Тем не менее происходит генерация объявлений всех членов, которые
170 Глава 10. Инстанцирование в результате подстановки могут содержать некорректные типы. Например, если снять комментарий с объявления unsafe (Т (*р) [N] ), у нас снова получится массив с нулевым количеством элементов, что приведет к ошибке. Аналогично, если бы переменная- член anonymous была объявлена не с типом Saf е<Т>, а с типом Danger<N>, произошла бы ошибка, так как тип Danger< 0 > не может быть сгенерирован. Наконец, рассмотрим оператор ->. Как правило, этот оператор должен возвращать указатель или другой класс, к которому применим оператор - >. На первый взгляд кажется, что генерация класса Tricky<int, 0> приведет к ошибке, поскольку в нем объявлено, что оператор -> возвращает тип int. Однако это не так. Поскольку определения такого рода основываются на некоторых определениях "естественных" шаблонов классов , правила языка сделаны более гибкими. Определенный пользователем оператор - > должен возвращать тип, к которому применим другой (например, встроенный) оператор - >, только в том случае, если он действительно выбирается согласно правилам разрешения перегрузки. Это утверждение остается истинным и тогда, когда оно не относится к шаблонам (хотя в таком контексте от него меньше пользы). Таким образом, объявление перегрузки оператора - > не приводит к ошибке, несмотря на то что в качестве возвращаемого им типа подставляется int. 10.3. Модель инстанцирования C++ Инстанцирование шаблонов — это процесс, в результате которого из определенного шаблона" путем подстановки его параметров генерируется обычный класс. На первый взгляд может показаться, что здесь все довольно просто, однако на практике этот процесс обрастает множеством деталей. 10.3.1. Двухфазный поиск В главе 9, "Имена в шаблонах", вы могли убедиться, что зависимые имена нельзя разрешить при синтаксическом анализе шаблонов. Поэтому в месте инстанцирования щаблона его определение еще раз просматривается компилятором. Однако независимые имена можно обработать при первом просмотре шаблона, выявив при этом многие ошибки. В результате мы приходим к концепции двухфазного поиска (two-phase lookup) : первая фаза — синтаксический анализ шаблона, вторая — его инстанцирование. На первом этапе обрабатываются независимые имена; на этой стадии анализ шаблона проводится с помощью правил обычного поиска (ordinary lookup rules), а также правил поиска, зависящего от аргументов (ADL), если они применимы в данном конкретном Типичный пример — шаблоны так называемых интеллектуальных указателей (smart pointer) (например, указатель std: :auto_ptr<T>, входящий в состав стандартной библиотеки). См. также главу 20, "Интеллектуальные указатели". Кроме того, применяются термины двухэтапный (two-stage lookup) или двухфазный поиск имен (two-phase name lookup).
10.3- Модель инстанцирования C++ 171 случае. Неполные зависимые имена (которые являются зависимыми, как зависимы имена функций при вызове с зависимыми аргументами) тоже просматриваются таким образом. Однако результат этого поиска не рассматривается как завершенный до тех пор, пока в процессе инстанцирования шаблона не будет проведен его дополнительный анализ. На втором этапе, выполняющемся при инстанцировании шаблона в точке инстанцирования (point of instantiation — POI), анализируются зависимые полные имена (в которых параметры шаблонов заменяются аргументами шаблонов, указанными для данного конкретного инстанцирования). Кроме того, выполняется дополнительный ADL для зависимых неполных имен. 10.3.2. Точки инстанцирования Как уже было показано, в исходном коде, использующем шаблон, есть места, в которых компилятор C++ должен иметь доступ к объявлению или определению этого шаблона. Точка инстанцирования (point of instantiation — POI) создается в том случае, когда некоторая конструкция исходного кода ссылается на специализацию шаблона таким образом, что для этой специализации нужно выполнить инстанцирование шаблона. Точка инстанцирования — это место кода, в которое можно вставить шаблон с подставленными аргументами. class Mylnt { public: Mylnt(int i); }; Mylnt operator — (Mylnt const&); bool operator > (Mylnt const&, Mylnt const&); typedef Mylnt Int; template<typename T> void f(T i) { if (i > 0) { g(-i); } } - // (1) void g(Int) { // (2) f<Int>(42); // Точка вызова // (3) } // (4)
172 Глава 10. Инстанцирование Когда компилятор C++ встречает вызов шаблона функции f<Int>(42), он знает, что нужно инстанцировать этот шаблон, подставив вместо параметра Т тип My Int. В результате создается точка инстанцирования. Точки (2) и (3) находятся совсем рядом с местом вызова, однако они не могут быть точками инстанцирования, потому что в языке C++ в этих точках нельзя вставить определение : : f<Int> (Int). Главное различие между точками (1) и (4) заключается в том, что в точке (4) функция g (Int) находится в области видимости, поэтому становится разрешимым вызов g(-i). Если бы точка (1) была точкой инстанцирования, то этот вызов нельзя было бы разрешить, поскольку в этой точке функция g (Int) еще не видна. К счастью, в C++ определяется, что точка инстанцирования для ссылки на специализацию шаблона, не являющегося шаблоном класса, должна находиться сразу после ближайшего определения или объявления области видимости, в котором содержится эта ссылка. В нашем примере это точка (4). Возможно, вас удивит, что в этом примере ^вместо обычного типа int принимает участие тип Mylnt. Дело в том, что на втором этапе поиска имен, который проводится в точке инстанцирования, используется только ADL. Поскольку с типом int не связано никакое пространство имен, то при его применении поиск в точке инстанцирования не проводился бы и функция g не была бы обнаружена. Таким образом, код из предыдущего примера перестанет компилироваться6, если определение типа Int заменить таким: typedef int Int; Если же речь идет о специализации класса, то здесь ситуация меняется. Рассмотрим приведенный ниже пример. template<typename T> class S { public: Т m; }; // (5) unsigned long h() { // (6) return (unsigned long)sizeof(S<int>); // (7) } // (8) Точки (6) и (7), находящиеся в области видимости функции h (), не могут рассматриваться как точки инстанцирования, поскольку в них не может находиться определение В 2002 году Комитет по стандартизации языка C++ изучал альтернативы, принятие которых привело бы к тому, что после рассматриваемой замены определения типа корректность кода сохранилась бы.
10.3. Модель инстанцирования C++ 173 пространства имен класса S<int> (шаблоны не могут находиться в области видимости функции). Согласно правилам, определяющим поведение экземпляров, не являющихся классами, точка инстанцирования могла бы находиться в точке (8). Однако тогда получается, что выражение sizeof (S<int>) является некорректным, поскольку тогда невозможно было бы определить размер класса S<int >, пока не будет достигнута точка (8). Таким образом, точка инстанцирования для ссылки на генерируемый экземпляр класса определяется как точка, находящаяся непосредственно перед ближайшим объявлением пространства имен, которое относится к определению, содержащему ссылку на этот экземпляр. В нашем примере это точка (5). При фактическом инстанцировании шаблона может возникнуть необходимость дополнительных инстанцирований. Рассмотрим небольшой пример. template<typename T> class S { public: typedef int I; }; // (i) template<typename T> void f () { S<char>::I varl = 41; typename S<T>::I var2 = 42; } int main() { f<double> (); } // (2): (2,a), (2,6) В ходе предыдущего рассмотрения уже было установлено, что точка инстанцирования f <double> находится в точке (2). В шаблоне функции f () также содержится ссылка на специализацию шаблона класса S<char>, точка инстанцирования которого находится в (1). Кроме того, здесь же имеется и ссылка на шаблон класса S<T>, но, поскольку эта ссылка содержит зависимость, выполнить инстанцирование в данной точке не получится. Однако в процессе инстанцирования шаблона функции f <double> в точке (2) можно заметить, что понадобится также инстанцировать определение S<double>. Такие вторичные, или транзитивные, точки инстанцирования определяются немного по-другому. Для шаблонов, не являющихся шаблонами классов, вторичные точки инстанцирования совпадают с обычными. Для шаблонов классов вторичные точки инстанцирования находятся непосредственно перед первичными (в ближайшем охватывающем пространстве имен). Для нашего примера это означает, что точка инстанцирования шаблона функции f <double > может быть помещена в точ-
174 Глава 10. Инстанцирование ку (2,6), а непосредственно перед ней, в точке (2,а), находится вторичная точка инстанцирования шаблона класса S<double>. Обратите внимание на отличие этой точки инстанцирования от точки инстанцирования шаблона класса S<char^>. Обычно в единице трансляции содержится несколько точек инстанцирования одного и того же экземпляра. Для экземпляров шаблона класса сохраняется только первая точка инстанцирования, а остальные игнорируются (на самом деле они просто не рассматриваются как точки инстанцирования). Для других экземпляров сохраняются все точки инстанцирования. В любом случае, согласно правилу одного определения, все инстанцирования, которые выполняются в каждой из сохраняющихся точек инстанцирования, должны быть* эквивалентными (хотя компилятор C++ не обязан проверять соблюдение этого правила и сообщать о его нарушении). Это позволяет компилятору выбрать для шаблона» не являющегося шаблоном класса, только одну точку, в которой фактически будет происходить инстанцирование. При этом можно не беспокоиться о том, что инстанцирование в других точках могло бы привести к другому результату. На практике большинство компиляторов откладывают фактическое инстанцирование шаблонов невстраиваемых функций до тех пор, пока не дойдут до конца единицы трансляции. При этом точка инстанцирования соответствующей специализации шаблона сдвигается в конец единицы трансляции. Разработчики, создающие компиляторы C++, намерены возвести этот метод в ранг документированной реализации, однако в стандарте данный вопрос пока не прояснен. 10.3.3. Модели включения и разделения Где бы ни находилась точка инстанцирования, в этом месте каким-то образом должен быть обеспечен доступ к соответствующему шаблону. Для специализации класса это означает, что определение шаблона класса должно быть видимым в точке, которая находится раньше в данной единице трансляции. Для точек инстанцирования шаблонов, не являющихся шаблонами класса, это тоже возможно. Обычно определения таких шаблонов просто добавляются в заголовочные файлы, которые с помощью директивы #include включаются в единицу трансляции. Такая модель, применяемая к определениям шаблонов, называется моделью включения (inclusion model), и во время написания книги это был один из наиболее популярных подходов. Для точек инстанцирования шаблонов, не являющихся шаблонами классов, существует альтернативный метод: такие шаблоны можно объявлять с помощью директивы export и определять в другой единице трансляции. Этот подход известен как модель разделения (separation model). В приведенном ниже фрагменте кода эта модель проиллюстрирована на примере уже знакомого нам шаблона функции max (). // Единица трансляции 1: ttinclude <iostream> export template<typename T> T const& max(T const&, T const&);
10.3. Модель инстанцирования C++ 175 int main() { std::cout « max(7, 42) « std::endl; // (1) } // Единица трансляции 2: export template<typename T> T const& inax(T const& a, T constfc b) { return a<b ? b : a; // (2) } Транслируя первый файл, компилятор обнаружит, что в точке (1) находится инструкция, создающая точку инстанцирования, в которой вместо параметра Т подставляется тип int. После этого компилятор должен убедиться в том, что определение во втором файле инстанцировано для удовлетворения этой точки инстанцирования. 10.3.4. Поиск в единицах трансляции Предположим, что первый файл приведенного выше примера переписан, как показано ниже. // Единица трансляции 1: #include <iostream> export template<typename T> T constfc max(T const&, T const&); namespace N { class I { public: I(int i): v(i) {} int v; }; bool operator < (I constfc a, I const& b) { return a.v < b.v; } } int main() { std::cout « max(N::I(7), N::I(42)).v « std::endl; // (3) } В точке инстанцирования, которая создается в положении (3), снова нужен доступ к определению, содержащемуся во втором файле (единица трансляции 2). Однако в этом
176 Глава 10. Инстанцирование определении используется перегруженный оператор <, который объявлен в единице трансляции 1 и который не видим в единице трансляции 2. Понятно, что для того, чтобы этот пример был работоспособным, процесс инстанцирования должен обратиться к двум разным контекстам объявлений7. Первый контекст — тот, в котором определен шаблон, а второй — тот, в котором объявлен тип I. Чтобы вовлечь в процесс инстанцирования оба этих контекста, имена шаблонов просматриваются в два этапа (см. раздел 10.3.1). На первом этапе происходит синтаксический анализ шаблона (другими словами, компилятор C++ первый раз производит разбор его определения). На этом этапе выполняется поиск независимых имен с применением правил обычного поиска и ADL. Кроме того, с помощью правил обычного поиска просматриваются неполные имена зависимых функций (т.е. функций, аргументы которых являются зависимыми). Полученный результат заносится в память, причем при этом не предпринимаются попытки разрешить перегрузку — это происходит на втором этапе. Второй этап выполняется в точке инстанцирования. Здесь с помощью правил обычного поиска и ADL отыскиваются полные зависимые имена. Зависимые неполные имена (которые уже прошли однократную обработку на первом этапе с помощью правил обычного поиска) теперь просматриваются только с помощью правил ADL, после чего полученный результат комбинируется с результатом обычного поиска из предыдущей стадии. Получившееся в результате множество используется для выбора вызываемой функции в процессе разрешения перегрузки. Хотя описанный механизм двухфазного поиска представляется особенно важным для реализации модели разделения, он используется и в модели включения. Однако во многих ранних реализациях модели включения любой поиск откладывался до того момента, пока не будет достигнута точка инстанцирования . 10.3.5. Примеры Приведем несколько примеров, наглядно иллюстрирующих описанный выше эффект. Первый пример относится к простой разновидности модели включения. template<typename T> void fl(Т х) { gl(x); // (1) } void gl(int) { } Контекст объявления — это множество всех объявлений, доступных в данной точке. Такая реализация приводит к поведению модели включения, близкому к поведению механизма раскрытия макросов.
10.3. Модель инстанцирования C++ 177 int main () { fl(7); // ОШИБКА: не удается найти функцию gl() } // (2): точка инстанцирования шаблона // fl<int>(int) Вызов функции f 1 (7) создает точку инстанцирования f l<int> (int) сразу после функции main () (в точке (2)). Главное в этом инстанцировании — поиск функции gl (). Когда компилятору впервые встречается шаблон функции f 1 (), он выясняет, что неполное имя gl является зависимым, поскольку функция с этим именем вызывается с зависимым аргументом (тип аргумента х зависит от параметра шаблона Т). Поэтому в точке (1) поиск шаблона функции gl осуществляется с помощью обычных правил; однако в этой точке функция gl не видна. В точке (2) (т.е. в точке инстанцирования) поиск функции осуществляется еще раз, причем он проводится в связанных с нею пространствах имен и классах. Поскольку единственный тип аргумента— int, причем с ним не связаны никакие пространства имен и классы, функция gl () найдена не будет, несмотря на то что ее вполне можно обнаружить при обычном просмотре в точке инстанцирования. Второй пример демонстрирует, как модель разделения может привести к неоднозначности перегрузки в разных единицах трансляции. Пример состоит из трех файлов (один из которых заголовочный). // Файл common.hpp: export template<typename T> void f(T) ; class A { }; class В { }; class X { public: operator A() operator B() }; // Файл а.срр: #include "common.hpp" void g(A) { } { return A(); } { return В(); } int main О { f<x>(XO);
178 Глава 10. Инстанцирование } // Файл Ь.срр: #include "common.hpp" void g(B) { } export template<typename T> void f(T x) { g(x) ; } В функции main(), содержащейся в файле а.срр, находится вызов функции f <Х> (X () ), который разрешается с помощью экспортированного шаблона, определенного в файле Ь.срр. В результате инстанцируется вызов функции д(х) с аргументом типа X. Поиск функции д () осуществляется дважды: один раз с помощью правил обычного поиска в файле b. срр (где анализируется шаблон) и еще раз — с помощью правил ADL в файле а. срр (где шаблон инстанцируется). В процессе первого поиска обнару- в живается функция g (В), а в процессе второго — g (A). Наличие определенного пользователем преобразования типов делает обе эти функции жизнеспособными, так что вызов функции f <Х> (X () ) оказывается неоднозначным. Заметим, что в файле b. срр нет и намека на то, что вызов функции g (х) может допускать двузначное толкование. Возможность неожиданного появления дополнительной функции-кандидата возникает из-за двухфазного механизма поиска. Таким образом, при написании и документировании экспортируемых шаблонов следует быть предельно осторожным. 10.4. Схемы реализации В этом разделе рассматриваются некоторые способы, с помощью которых различные реализации C++ поддерживают модель включения. Все эти реализации основываются на двух классических компонентах: на компиляторе и компоновщике. Компилятор преобразует исходный код в объектные файлы, которые содержат машинный код и символические обозначения (перекрестные ссылки на другие объектные файлы и библиотеки). Компоновщик создает исполняемые программы или библиотеки, соединяя объектные файлы в одно целое и разрешая содержащиеся в них перекрестные ссылки. Все, о чем пойдет речь далее, относится именно к такой модели, хотя вполне возможны другие способы реализации языка C++ (которые не приобрели широкой популярности). Например, вполне можно представить себе интерпретатор C++. Если специализация шаблона класса используется в нескольких единицах трансляции, компилятору придется повторить процесс инстанцирования в каждой из этих единиц. Количество возникающих в связи с этим проблем весьма незначительно, поскольку определе-
10.4. Схемы реализации 179 ния классов не генерируют непосредственно код низкого уровня. Эти определения используются только внутри реализаций C++ для проверки и интерпретации различных других выражений и объявлений. Таким образом, множественное инстанцирование определения класса, по сути, не отличается от многократного включения определения класса (обычно с помощью включения заголовочного файла) в разных единицах трансляции. Однако если происходит инстанцирование шаблона (невстраиваемой) функции, ситуация может измениться. Если использовать несколько определений обычных невстроенных функций, то это бы нарушило правило одного определения. Например, предположим, что компилируется и компонуется программа, состоящая из двух приведенных ниже файлов. // Файл а.срр: int main () { } // Файл Ь.срр: int main() { } Компиляторы C++ будут транслировать каждый модуль отдельно, причем без каких- либо проблем, потому что эти единицы трансляции, безусловно, являются корректными с точки зрения C++. Однако попытка связать эти два файла, скорее всего, вызовет протест компоновщика. Дело в том, что дублирование определений не допускается. Рассмотрим теперь другой пример, в котором участвуют шаблоны. // Файл t.hpp: // Общий заголовочный файл (модель включения) template<typename T> class S { public: void f () ; }; template<typename T> void S::f() // Определение функции-члена } void helper(S<int>*); // Файл а.срр: #include "t.hpp" Void helper(S<int>* s) s->f(); // (1) Первая точка инстанцирования S::f
180 Глава 10. Инстанцирование } // Файл Ь.срр: #include "t.hpp" int main () { S<int> s/ helper(&s); s.f(); // (2) Вторая точка инстанцирования S::f } Если компоновщик рассматривает инстанцированные члены шаблонов точно так же, как обычные функции или функции-члены, то компилятор должен гарантировать, что он сгенерирует код только в одной из двух точек инстанцирования: в точке (1) или (2), но не в обеих. Чтобы достичь этого, компилятор должен перенести информацию из одной единицы трансляции в другую, а это никогда не требовалось от компиляторов C++ до введения шаблонов в этот язык. Далее рассматриваются три популярных класса решений, получивших широкое распространение среди разработчиков реализаций языка C++. Заметим, что такая же проблема возникает во всех связываемых объектах, возникающих в результате инстанцирования шаблонов: как в инстанцированных шаблонах обычных функций и функций-членов, так и в инстанцированных статических данных-членах. 10.4.1. "Жадное" инстанцирование Первые компиляторы C++, которые сделали популярным так называемое жадное инстанцирование, были произведены компанией Borland. С тех пор этот подход стал одним из самых распространенных методов среди различных систем C++. В частности, это почти универсальный механизм для разработки сред программирования, предназначенных для персональных компьютеров под управлением операционной системы Windows. В процессе жадного инстанцирования предполагается, что компоновщик осведомлен о том, что некоторые объекты (в частности, подлежащие компоновке инстанцированные шаблоны) могут дублироваться в разных объектных файлах и библиотеках. Обычно компилятор помечает такие элементы особым образом. Когда компоновщик обнаруживает множественные инстанцирования, одно из них он оставляет, а остальные отбрасывает. В этом и заключается суть рассматриваемого подхода. Теоретически жадное инстанцирование обладает некоторыми серьезными недостатками, перечисленными ниже. • Компилятор может потратить время на генерирование и оптимизацию множества инстанцирований, из которых будет использоваться только одно. • Обычно компоновщики не проверяют идентичность двух инстанцирований, так как код, сгенерированный для разных экземпляров одной и той же специализаций шаблона, может незначительно варьироваться, что вполне допустимо. Нельзя до-
10.4. Схемы реализации 181 пустить, чтобы из-за этих небольших различий в работе компоновщика произошел сбой. (Причиной этих различий могут быть несущественные расхождения в состоянии компилятора в моменты инстанцирования.) Однако часто это приводит к тому, что компоновщик не замечает более существенных различий, например когда одно из инстанцирований скомпилировано с максимальной оптимизацией, а другое — с максимальной отладочной информацией. • Потенциально объем всех объектных файлов может существенно превысить их объем при использовании иного подхода, поскольку один и тот же фрагмент кода дублируется несколько раз. На практике оказывается, что эти недостатки не создают особых проблем. Возможно, так получается благодаря тому, что жадное инстанцирование очень выгодно отличается от альтернативных подходов: оно сохраняет традиционную зависимость от исходного кода. В частности, из одной единицы трансляции генерируется только один объектный файл, и каждый объектный файл содержит скомпилированный код всех подлежащих компоновке определений из соответствующего исходного файла (включая инстанциро- ванные определения). Наконец, стоит заметить, что механизм компоновки, позволяющий дублировать определения компонуемых элементов, обычно используется для обработки дублируемых встраиваемых функций и таблиц диспетчеризации виртуальных функций . Если этот механизм недоступен, в качестве альтернативы эти элементы обычно генерируются с внутренним связыванием, но это приводит к увеличению объема генерируемого кода. 10.4.2. Инстанцирование по запросу В этой категории наиболее популярна реализация, представленная компанией Sun Mi- crosystems, начиная с версии 4.0 компилятора C++ этой компании. Концептуально инстанцирование по запросу отличается удивительной простотой и элегантностью, являясь при этом наиболее современной схемой инстанцирования классов из всех рассмотренных нами. В этой схеме создается и поддерживается специальная база данных, совместно используемая при компиляции всех единиц трансляции, имеющих отношение к программе. В нее заносятся сведения об инстанцированных специализациях шаблонов, а также о том, от какого элемента исходного кода они зависят. Сами сгенерированные специализации также обычно сохраняются в этой базе данных. При достижении точки инстанцирования подлежащего компоновке элемента происходит одно из трех перечисленных ниже событий. Если компилятор не в состоянии "встраивать" все вызовы какой-то функции, обозначенной ключевым словом inline, в состав объектного файла вводится отдельная копия этой функции. Обычно вызовы виртуальной функции реализуются как косвенные, причем это осуществляется с помощью таблиц указателей на функции. Фундаментальное исследование подобных аспектов реализации C++ можно найти в [21].
182 Глава 10. Инстанцирование 1. Соответствующая специализация отсутствует. В этом случае происходит инстанцирование, а полученная в результате специализация заносится в базу данных. 2. Специализация имеется в наличии, однако она устарела, поскольку с момента ее создания произошли изменения в исходном коде. В этой ситуации также происходит инстанцирование, а полученная в результате специализация заносится в базу данных вместо предыдущей. 3. В базе данных содержится подходящая специализация. Делать ничего не нужно. Несмотря на концептуальную простоту описанной схемы, ее реализация связана с необходимостью решения некоторых практических задач. В частности, далеко не просто поддерживать правильную взаимосвязь между структурными элементами базы данных, поскольку состояние исходного кода может меняться. Несмотря на то что не будет ошибкой принять третий случай за второй, это увеличит количество работы, которую необходимо выполнить компилятору (а значит, и время компиляции). Кроме того, промышленные компиляторы зачастую выполняют параллельную компиляцию нескольких единиц трансляции, что также усложняет поддержку базы данных. Несмотря на указанные трудности, схема может быть довольно эффективно реализована. Кроме того, в отличие, например, от жадного инстанцирования, которое может привести к большому количеству напрасно затраченной работы, при описанном решении практически отсутствуют патологические случаи, которые могли бы привести к излишней работе компилятора. К сожалению, использование базы данных также может создать некоторые проблемы программисту. Причина большинства этих проблем заключается в том, что традиционная модель трансляции, унаследованная от большинства компиляторов С, больше не применима: в результате компиляции одной единицы трансляции теперь не создается отдельный объектный файл. Предположим, например, что нужно скомпоновать конечную программу. Для этого понадобится не только содержимое каждого объектного файла, связанного с различными единицами трансляции, но и тех объектных файлов, которые хранятся в базе данных. Аналогично, в процессе создания бинарной библиотеки нужно убедиться в том, что инструмент, с помощью которого эта библиотека создается (обычно v это компоновщик или архиватор), располагает сведениями из базы данных. По сути, лю-в бой инструмент, оперирующий с объектными файлами, должен иметь информацию о содержимом базы данных. Многие из этих проблем можно смягчить, не занося инстанцирования в базу данных, а размещая вместо этого их объектный код в объектный файл, • вызвавший данное инстанцирование. С библиотеками связана другая проблема. В одну и ту же библиотеку может быть упаковано несколько сгенерированных специализаций. При добавлении этой библиотеки в другой проект может понадобиться занести в базу данных нового проекта сведения об уже доступных инстанцированиях. Если этого не сделать и если в проекте создаются свои точки инстанцирования для специализаций, которые содержатся в библиотеке, инстанцирования могут дублироваться. Стратегия, которой следует придерживаться в такой ситуации, может состоять в применении той же технологии компоновки, что и при жадном инстанцировании: передать
10.4. Схемы реализации 183 компоновщику сведения об имеющихся инстанцированиях, а затем избавиться от дубликатов (которые, однако, встречаются намного реже, чем в случае жадного инстанцирования). Другие варианты размещений исходных файлов, объектных файлов и библиотек могут вызвать новые проблемы, в частности отсутствие инстанцировании из-за того, что объектный код, содержащий нужное инстанцирование, не скомпонован с конечной исполняемой программой. Эти проблемы объясняются не недостатками подхода, основанного на инстанцировании по запросу; скорее их следует воспринимать как аргумент против создания излишне сложных сред разработки программного обеспечения. 10.4.3. Итеративное инстанцирование Первым компилятором, поддерживающим шаблоны C++, был Cfront 3.0 — прямой потомок компилятора, разработанного Бьерном Страуструпом в процессе создания языка программирования C++ . Одна из особенностей компилятора Cfront, ограничивающая его гибкость, заключалась в том, что он должен был обладать переносимостью на другие платформы. Это означает, что, во-первых, в качестве представления на всех целевых платформах используется язык С и, во-вторых, применяется локальный целевой компоновщик. В частности, при этом подразумевается, что компоновщик не способен обрабатывать шаблоны. Фактически компилятор Cfront генерировал инстанцирования шаблонов как обычные функции С, поэтому он должен был избегать повторных инстанцировании. Хотя исходная модель, на которой основан компилятор Cfront, отличалась от стандартных моделей включения и разделения, можно добиться того, что используемая в этом компиляторе стратегия инстанцирования будет соответствовать модели включения. Таким образом, это.первый компилятора, в котором было воплощено итеративное инстанцирование. Итерации компилятора Cfront описаны ниже. 1. Исходный код компилируется без инстанцирования каких бы то ни было специализаций. 2. Объектные файлы связываются с помощью предварительного компоновщика (prelinker). 3. Предварительный компоновщик вызывает компоновщик и анализирует сгенерированные сообщения об ошибках, чтобы определить, не вызваны ли они отсутствием инстанцировании; если причина ошибки именно в этом, предварительный компоновщик вызывает компилятор для обработки исходных файлов, содержащих необходимые определения шаблонов; при этом параметры компилятора настроены для генерирования отсутствующих инстанцировании. 4. Если сгенерированы какие-либо определения, повторяется шаг 3/ Не поймите эту фразу превратно, придя к заключению, что компилятор Cfront был абстрактным Прототипом. Напротив, он использовался в промышленных целях и послужил основой для многих коммерческих компиляторов C++. Версия 3.0 появилась в 1991 году, но страдала наличием ошибок. Вскоре За этим последовала версия 3.0.1, благодаря которой стало возможным применение шаблонов.
184 Глава 10. Инстанцирование Повторение шагаЗ обусловлено тем, что на практике инстанцирование одного из подлежащих компоновке элементов может привести к необходимости инстанцировать шаблон в другом элементе, который еще не был обработан. Такой итеративный процесс в конечном счете сходится, и компоновщику удается создать завершенную программу. Схема, положенная в основу исходного компилятора Cfront, обладает весьма серьезными недостатками. • Время, затрачиваемое на создание исполняемого файла, увеличивается не только из-за работы предварительного компоновщика, но и за счет повторных компиляций и компоновок. Согласно отчетам некоторых пользователей систем, в основе которых находится компилятор Cfront, время создания исполняемых файлов возросло до "нескольких дней" по сравнению с тем, что раньше в альтернативных схемах это занимало "около часа". • Выдача сообщений об ошибках и предупреждений откладывается до этапа компоновки. Это особенно неприятно, когда компоновка занимает много времени и разработчику часами приходится ждать, пока будет обнаружена всего лишь опечатка в определении шаблона. • Необходимо особо позаботиться о том, чтобы запомнить, где находится исходный код, содержащий то или иное определение (шаг 1). В частности, компилятор Cfront использовал специальное хранилище, с помощью которого решались некоторые проблемы, что, по сути, напоминает использование базы данных при ин- станцировании по запросу. Исходный компилятор Cfront не был приспособлен для поддержки параллельной компиляции. Несмотря на перечисленные недостатки, принцип итерации в улучшенном виде был использован в двух системах компиляции. Одна из них создана группой Edison Design Group (EDG), а вторая известна под названием HP аС++12. Эти системы послужили толчком к развитию дополнительных возможностей шаблонов в C++13. Ниже излагается методика, разработанная группой EDG для демонстрации своих передовых достижений в C++14. Итеративное инстанцирование, реализованное группой EDG, позволяет осуществлять двусторонний обмен информацией между предварительным компоновщиком и компилято- Компилятор HP aC++ возник на основе технологии, разработанной в компании Taligent (позже она была поглощена компанией IBM). Компания HP добавила в компилятор аС++ принцип жадного инстанцирования и сделала его механизмом, применяющимся по умолчанию. 13 Мы не можем считать себя беспристрастными судьями. Однако первые публично доступные реализации таких возможностей, как шаблоны-члены, частичные специализации, современный подход к поиску имен в шаблонах и модель разделения шаблонов, появились благодаря этим компаниям. 14 Компания EDG не занимается прямыми продажами реализаций C++ конечным пользователям. Она поставляет важный и переносимый компонент такой реализации другим производителям, которые интегрируют его в полноценное решение, зависящее от конкретной платформы. Некоторые клиенты компании EDG придерживаются переносимого принципа итерационного инстанцирования, однако они могут легко перейти к жадному инстанцированию (которое не является переносимым, поскольку зависит от особенностей компоновщика).
10.4. Схемы реализации 185 ром на различных стадиях его работы. Это проявляется в том, что предварительный компоновщик обладает возможностью направлять результаты инстанцирования, выполненного для отдельно взятой единицы трансляции, в файл запроса инстанцирований (instantiation request). Компилятор же, со своей стороны, может известить предварительный компоновщик о возможных точках инстанцирования, либо внедряя информацию о них в объектные файлы, либо генерируя отдельные файлы с информацией о шаблонах (template information files). Файл запроса инстанцирований и файл с информацией о шаблонах создаются с именами, совпадающими с именем компилируемого файла, и расширениями .Ни . ti соответственно. Ниже приводится описание принципа работы итераций. 1. Во время компиляции исходной единицы трансляции компилятор EDG считывает содержимое соответствующего файла с расширением . ii (при его наличии), создает инстанцирование и помещает его в этот файл. В то же время он записывает, какие точки инстанцирования ему удалось обработать и поместить в созданный объектный файл или в отдельный файл с расширением . ti. Кроме того, он записывает сведения о том, каким образом скомпилирован обрабатываемый файл. 2. Этап компоновки перехватывается предварительным компоновщиком, проверяющим объектные файлы и соответствующие файлы с расширением .ti, которые будут принимать участие в процессе компоновки. Для каждого еще не сгенерированного инстанцирования он создает соответствующую директиву и добавляет ее в файл с расширением . ii и именем, соответствующим единице трансляции, к которой относится эта директива. 3. Если хоть один из файлов с расширением . ii был модифицирован, предварительный компоновщик повторно вызывает компилятор (этап 1) для обработки соответствующих исходных файлов. 4. По достижении сходимости выполняется единый этап компоновки. В этой схеме параллельная подготовка компонуемых элементов достигается путем поддержания глобальной информации о транслируемых единицах. Время компоновки в таком подходе может существенно возрасти по сравнению с тем, которое затрачивается при жадном инстанцирований и инстанцирований по запросу. Однако, поскольку фактически компоновка на предварительном этапе не выполняется, это возрастание является не таким уж катастрофическим. Еще важнее то, что, поскольку предварительный компоновщик поддерживает глобальное согласование файлов с расширением . ii, эти файлы Могут быть повторно использованы в следующих циклах создания исполняемого фала. Допустим, например, что разработчик внес изменения в исходный код и повторно запустил процесс компиляции и компоновки, чтобы внесенные изменения вступили в силу. На этапе компиляции безотлагательно будут инстанцированы все специализации шаблонов, запросы по которым содержатся в файлах с расширением . ii, оставшихся от предыдущей компиляции. При этом велика вероятнобть того, что предварительному компоновщику просто не понадобится активизировать повторные компиляции.
186 Глава 10. Инстанцирование На практике схема EDG работает вполне удовлетворительно. Несмотря на то что создание исполняемого файла "с нуля" обычно длится дольше, чем в других подходах, время его последующих построений вполне сравнимо со временем, затрачиваемым в других подходах, 10.5. Явное инстанцирование Точку инстанцирования для специализации шаблона можно создать явным образом. Конструкция, с помощью которой это достигается, называется директивой явного инстанцирования (explicit instantiation directive). Синтаксически она состоит из ключевого слова template, за которым следует объявление инстанцируемой специализации. template<typename T> void f(T) throw(T) { } // Четыре примера корректного явного инстанцирования template void f<int>(int) throw(int); template void fo(float) throw(float); template void f(long) throw(long); template void f(char); . Обратите внимание, что корректна каждая из четырех приведенных выше директив инстанцирования. Аргументы шаблона могут быть выведены (см. главу 11, "Вывод аргументов шаблонов"), а спецификации исключений могут быть опущены. Если же эти спецификации не опущены, то они должны соответствовать заданным в шаблоне. Члены шаблонов классов также можно явно инстанцировать. template<typename T> class S { public: void f() { } }; template void S<int>::f(); template class S<void>; Кроме того, все члены, входящие в состав специализации шаблона класса, можно явно инстанцировать путем явного инстанцирования специализации шаблона этого класса. Многие ранние системы компиляции C++, в которых впервые была реализована поддержка шаблонов, не обладали возможностью автоматического инстанцирования. Вместо этого в некоторых системах выдвигалось требование, чтобы используемые специализации шаблонов функций были вручную инстанцированы в одном месте. В процессе такого ручного инстанцирования (manual instantiation) обычно участвуют директивы #pragma, зависящие от реализации.
10.5. Явное инстанцирование 187 В настоящее время в стандарте C++ разработан четкий синтаксис ручного инстанци- рования. Стандарт также указывает, что в программе должно быть не более одного явного инстанцирования для определенной специализации шаблона. Кррме того, если специализация шаблона инстанцируется явным образом, то ее не следует явно социализировать, и наоборот. В исходном контексте ручного инстанцирования эти ограничения могли выглядеть вполне безобидно, но в настоящее время они могут привести к определенным проблемам. Рассмотрим сначала ситуацию, в которой реализуется библиотека. Пусть первая версия входящего в нее шаблона функции выглядит так: // Файл toast.hpp: template<typename T> void toast(T const& x) { } Пользователь библиотеки может включить приведенный выше заголовочный файл и явно инстанцировать содержащийся в нем шаблон. // Пользовательский код: #include "toast.hpp" template void toast(float); К сожалению, если разработчик библиотеки явно специализирует шаблон toast<f loat>, пользовательский код станет некорректным. Ситуация еще более усложнится, если библиотека является стандартной, а ее компоненты реализованы различными производителями. Одни производители могут специализировать некоторые шаблоны явным образом, а другие — нет (или могут задавать иные специализации). Поэтому в пользовательском коде не может быть указано явное инстанцирование компонентов переносимой библиотеки. Во время написания этой книги (2002 год) Комитет по стандартизации C++ склонялся к такому мнению: если после директивы явного инстанцирования следует явная специализация того же объекта, эта директива не будет оказывать влияния на работу программы. (Заключительное решение по этому поводу все еще не принято; если реализация сформулированного правила окажется технически недостижимой, то оно не будет принято в качестве стандарта.) Вторая трудность возникает в связи с существующими ограничениями на явное инстанцирование шаблонов и является результатом их использования для уменьшения времени компиляции. Дело в том, что многие программисты, пользующиеся языком C++, обнаружили, что автоматическое инстанцирование шаблонов оказывает отрицательное Сияние на время создания исполняемого файла. Чтобы уменьшить это время, применяется метод, состоящий в ручном инстанцировании определенных специализаций шаблонов в одной единице трансляции и его запрещении во всех других единицах. Единствен-
188 Глава 10. Инстанцирование ный переносимый способ обеспечить такой запрет— поместить определение шаблона только в той единице трансляции, в которой этот шаблон будет явно инстанцирован. // Единица трансляции 1: template<typename T> void f(); // Определение отсутствует, // что предотвращает возмож- // ность инстанцирования в // данной единице трансляции void g() { f<int>(); } // Единица трансляции 2: template<typename T> void f() { } template void f<int>(); // Ручное инстанцирование void g(); int main () { g(); } Этот способ вполне работоспособен, но требует постоянного контроля над исходным кодом, предоставляющим интерфейс шаблона, что зачастую невозможно: например, исходный файл, в котором содержится шаблон, нельзя модифицировать и он всегда предоставляет определение шаблона. Один из методов, иногда применяемых в подобной ситуации, состоит в том, чтобы объявить шаблон в виде специализаций во всех единицах трансляции (что сделает невозможным автоматическое инстанцирование этой специализации), за исключением той, в которой данная специализация инстанцируется явным образом. Чтобы проиллюстрировать этот подход, модифицируем предыдущий пример, включив в нем определение шаблона в единицу трансляции 1. // Единица трансляции 1: template<typename T> void f() { } templateo void f<int>(); // Объявление без // определения void g() { f<int>(); }
10.5. Явное инстанцирование 189 // Единица трансляции 2: template<typename T> void f() { } template void f<int>(); // Ручное инстанцирование void g(); int main() { g(); } К сожалению, при этом предполагается, что объектный код для вызова явно заданной специализации идентичен вызову подходящей обобщенной специализации. В некоторых случаях это предположение неверно. Встречаются компиляторы, генерирующие различные скорректированные имена (mangled names) для этих двух элементов15. Такие компиляторы не скомпонуют приведенный выше код в единый исполняемый файл. Некоторые компиляторы оснащаются расширениями, позволяющими указывать, что специализацию шаблона не следует инстанцировать в той или иной единице трансляции. Популярный (но нестандартный) синтаксис, который применяется при этом, начинается ключевым словом extern, стоящим перед директивой явного инстанцирования, которая в противном случае вызвала бы инстанцирование. Для компиляторов, которые поддерживают такое расширение, первую единицу трансляции, входящую в состав последнего примера, можно было бы переписать,, как показано ниже. // Единица трансляции 1: template<typename T> void f() { } extern template void f<int>(); // Объявление // без определения v°id g() { f<int>(); . } Скорректированное имя функции— это имя, с которым работает компоновщик. В нем °бычное имя функции сочетается с атрибутами ее параметров, аргументов шаблона, а иногда и с другими свойствами. В результате получается уникальное имя, даже если данная функция перебужена.
190 Глава 10. Инстанцирование 10.6. Заключение Эта глава посвящена двум взаимосвязанным, но разным вопросам: моделям компиляции шаблонов и различным механизмам инстанцирования шаблонов в C++. Модель компиляции определяет смысл шаблона на различных стадиях транслирования программы. В частности, она определяет значение различных конструкций шаблона в процессе его инстанцирования. Важной составной частью модели компиляции является поиск имен. Модели компиляции шаблонов подразделяются на модель включения и модель разделения, которые являются неотъемлемой частью определения языка. Механизмы инстанцирования представляют собой внешние механизмы, позволяющие создавать в конкретных реализациях языка C++ корректные инстанцирования. Эти механизмы могут быть ограничены рамками, накладываемыми компоновщиком и другими инструментами, принимающими участие в процессе создания программ. В исходной реализации шаблонов (в компиляторе Cfront) пришлось выйти за рамки этих двух концепций. В ней создавались новые единицы трансляции дня инстанцирования шаблонов с помощью особых соглашений, касающихся организации исходных файлов. Получаемые в результате единицы трансляции компилировались с помощью модели, которая, по сути, была моделью включения (хотя правила поиска имен C++ в то время были существенно иными). Несмотря на то что в компиляторе Cfront не была реализована модель раздельной компиляции" шаблонов, он создавал видимость раздельной компиляции с помощью неявных включений. Разнообразные последующие реализации использовали аналогичный механизм неявного включения по умолчанию (компания Sun Microsystems) или в качестве одной из доступных возможностей (HP, EDG). Тем самым достигалась определенная совместимость с имеющимся в наличии кодом, разработанным для компилятора Cfront Приведем пример, иллюстрирующий особенности, присущие компилятору Cfront. // Файл template.hpp: template<class T> // В компиляторе Cfront нет // ключевого слова typename void f(T); // Файл template.срр: template<class T> // В компиляторе Cfront нет I // ключевого слова typename void f(T)| { } // Файл app.hpp: class App { }; // Файл main.срр: # include "app.hpp"
10.6. Заключение 191 #include "template.hpp" int main() { App a; f (a); } Во время компоновки компилятором Cfront используется итеративная схема инстанци- рования, после чего создается новая единица трансляции, включающая файлы, которые могут содержать реализации шаблонов, найденных в заголовочных файлах. В компиляторе Cfront принято соглашение, согласно которому расширение заголовочных файлов . h (или аналогичное) заменяется расширением . с (или другим, например . С или . срр). При этом сгенерированная единица трансляции приобретает следующий вид: // Файл main.срр: #include "template.hpp" #include "template.срр" #include "app.hpp" static void __dummy_(App al) { f(al); } Затем полученная единица трансляции компилируется со специальными опциями, при которых отключается генерация кода каких бы то ни было объектов, определенных во включенных файлах. Благодаря этому предотвращается включение множественных определений подлежащих компоновке элементов, содержащихся в файле template . срр (который мог быть уже скомпилирован в другой объектный файл). Функция _dummy_ используется для создания ссылок на специализации, которые необходимо инстанцировать. Обратите внимание на изменение порядка заголовочных файлов. Оно объясняется тем, что в состав компилятора Cfront входит код, анализирующий заголовочные файлы. Благодаря ему заголовочные файлы, которые не используются в генерируемой единице трансляции, опускаются. К сожалению, при наличии макросов, область видимости которых выходит за рамки одного заголовочного файла, этот метод становится ненадежным. В стандартной модели разделения C++, напротив, выполняется раздельное транслирование двух (или большего количества) единиц. После этого происходит инстанцирова- ние, имеющее доступ к обеим единицам трансляции (в первую очередь благодаря ADL). Поскольку этот процесс не основан на включении, он не требует специальных соглашений для заголовочных файлов, а наличие определений макросов в одной единице трансляции не может повредить другим единицам трансляции. Однако, как было описано в Этой главе, преподносить сюрпризы в C++ способны не только макросы, поэтому модель депортирования подвержена и другого рода неприятностям.
Глава 11 Вывод аргументов шаблонов Если при каждом вызове шаблона функции явным образом задавать аргументы шаблона (например, concat<std: : string, int>(s/ 3)), то код может быстро стать громоздким. К счастью, компилятор C++ часто в состоянии автоматически определить, какими должны быть аргументы шаблона. Это достигается с помощью мощного механизма под названием вывод аргументов шаблона (template argument deduction). В настоящей главе подробно объясняется, что происходит в процессе вывода аргументов шаблонов. Как это часто бывает в C++, с этим процессом связано множество правил, соблюдение которых обычно приводит к интуитивно понятному результату. Глубокое понимание материала, изложенного в этой главе, позволит избежать многих досадных неожиданностей. 11.1. Процесс вывода В процессе вывода типы аргументов, с которыми вызывается функция, сравниваются с соответствующими параметризованными типами. По результатам этого сравнения компилятор пытается сделать вывод о том, что именно нужно подставить вместо одного или нескольких выведенных параметров. Проводится независимый анализ каждой пары "аргумент-параметр", и если однозначный вывод сделать не удается, то процесс вывода завершается неудачей. Рассмотрим пример. template<typename T> т const& max(T const& a, T const& b) { return a < b ? b : a; } int g = max(l, 1.0); В вызове функции max () первый аргумент принадлежит типу int, из чего можно заключить, что в роли параметра Т в исходном шаблоне max () должен выступать тип int. Однако второй аргумент принадлежит типу double, а это означает, что вместо параметра типа Т нужно подставить тип double. Этот вывод противоречит предыдущему.
194 Глава 11. Вывод аргументов шаблонов Заметим, что утверждение "вывод выполнить не удается" не означает, что программа некорректна. В конце концов может случиться так, что этот процесс удастся провести для другого шаблона с именем max (шаблоны функций, как и обычные функции, можно перегружать; см. раздел 2.4, стр. 37, и главу 12, "Специализация и перегрузка"). Даже если удалось вывести все параметры шаблона, это еще не означает, что вывод успешен. Бывает и так, что при подстановке выведенных аргументов в оставшуюся часть определения функции получается некорректная конструкция. Приведем пример такой ситуации. template<typename T> typename T::ElementT at(T const& a, int i) { return a[i]; } void f(int* p) { int x = at (p, 7); } При анализе этого кода приходим к выводу, что вместо параметра типа Т нужно подставить тип int * (в силу того, что параметр типа Т используется только в одном месте, никаких связанных с ним неоднозначностей возникать не должно). Однако подстановка int* вместо Т в возвращаемый тип Т:: ElementT явно недопустима в C++, поэтому вывод сделать не удается1. Скорее всего, в этом случае в сообщении об ошибке будет указано, что не удалось найти определение функции at (), соответствующее ее вызову в программе. Если же явно указаны все аргументы шаблона, то ситуация меняется. В таком случае можно прийти к однозначному заключению о том, какой именно из шаблонов функции вызывается (даже если эти шаблоны перегружены), поэтому вероятность того, что удастся выполнить вывод аргумента дня другого шаблона, равна нулю. В этом случае в сообщении об ошибке, вероятно, будет отмечено, что неверно указаны аргументы шаблона функции at (). Это можно проверить на практике, сравнивая сообщения компилятора для предыдущего кода с теми, которые будут выдаваться при компиляции, например, такого кода: void f (int* p) { int x = at<int*>(p, 7); } Рассмотрим, как происходит процедура проверки соответствия параметра и аргумента. Опишем его в терминах соответствия типа А (выведенного из типа аргумента) параметризованному типу Р. Если параметр объявлен как ссылка, считаем, что Р— это тип,, на который делается ссылка, а А — тип аргумента. В противном случае параметр имеет В данном случае неуспешный вывод привел к ошибке. Однако в силу принципа SFINAE (substitution fail is not an error — некорректная подстановка не является ошибкой; см. раздел 8.3.1, стр. 129) при наличии функции, для которой вывод завершается успеышо, код оказывается корректным.
11.1. Процесс вывода 195 тип Р, а тип А получается из него путем сведения (decaying)2 типов массива или функции к указателю на соответствующий тип. При этом квалификаторы верхнего уровня const и volatile игнорируются. template<typename T> void f(T); // Здесь Р - это Т template<typename T> void д(Т&); // Здесь Р — это тоже Т double x[20] ; int const seven = 7; f(x); // Параметр не передается по ссылке: Т — double* д(х); // Параметр передается по ссылке: Т — double[20] f(seven); // Параметр не передается по ссылке: Т — int д(seven); // Параметр передается по ссылке: Т — int const f(7); // Параметр не передается по ссылке: Т — int д(7); // Параметр передается по ссылке: Т — int => // ОШИБКА: не удастся передать 7 // как параметр типа int& При вызове функции f (х) тип массива х сводится к типу double* как следствие того, что Т — это тип double. При вызове f (seven) квалификатор const опускается, поэтому делается вывод, что Т— это тип int. Если же вызывается функция д (х), то компилятор делает вывод, что Т— это тип double [20] (сведения не происходит). Аналогично, поскольку в вызове д (seven) в качестве аргумента используется lvalue типа int const и поскольку квалификаторы const и volatile для передаваемых по ссылке параметров не опускаются, приходим к выводу, что Т— это тип int const. Однако обратите внимание, что при вызове д (7) компилятор заключил бы, что Т — это тип int (поскольку в rvalue-выражениях, которые не находятся в определении класса, не могут присутствовать квалификаторы типов const и volatile). В результате этот вызов привел бы к ошибке, поскольку аргумент 7 нельзя передать параметру типа int&. Тот факт, что для аргументов, которые передаются по ссылке, не происходит сведение, может привести к неожиданным результатам в тех случаях, когда эти аргументы являются строковыми литералами. Еще раз рассмотрим шаблон max (). template<typename T> Т const& max(T corist& a, T constfc b) ; Разумно было бы ожидать, что в выражении max ("Apple" , "Pear") параметр Т будет вьюеден как тип char const*. Однако строка "Apple" принадлежит типу char const [б], а строка "Pear" —типу char const [5]. Никакого сведения массива к указателю на массив не происходит (поскольку вывод типа выполняется на основе па- Сведение— это термин, которым обозначается неявное преобразование типов массива и функции в соответствующий тип-указатель.
196 Глава 11. Вывод аргументов шаблонов раметров, передаваемых по ссылке). Таким образом, вместо параметра Т нужно одновременно подставить и тип char const [ б ], и тип char const [ 5 ], а это, конечно же, невозможно. Более подробное обсуждение этой темы можно найти в разделе 5.6, стр. 79. 11.2. Выводимый контекст Типу аргумента могут соответствовать значительно более сложные параметризованные типы, чем просто Т. Ниже приведено несколько (все еще не слишком сложных) примеров. template<typename T> void fl(T*); template<typename Е, int N> void f2(E(&)[N]); template<typename Tl, typename T2, typename T3> void f3(Tl (T2::*)(T3*)); class S{ public: void f(double*); }; void g (int*** ppp) { bool b[42]; fl(ppp); // Выводится, что Т = int** f2(b); // Выводится, что Е = bool, a N f3(&S::f); // Выводится, что Tl = void, T2 // a T3 = double } Сложные объявления типов составляются из более простых конструкций (операторов объявлений указателей, ссылок, массивов и функций, объявлений указателей на члены, идентификаторов шаблонов и т.д.). Процесс определения нужного типа происходит в нисходящем порядке, начиная с конструкций высокого уровня и продвигаясь к низкоуровневым. Уместно заметить, что этим путем можно подобрать тип для большинства таких конструкций; в этом случае они называются выводимым контекстом (deduced context). Однако некоторые конструкции выводимым контекстом не являются. К их числу относятся следующие: • полное имя типа; имя типа наподобие Q<T>: : X никогда не используется для вывода параметра шаблона Т; • выражения, не являющиеся типами, которые не являются параметрами, не являющимися типами; например, имя типа S<I+1> никогда не используется для вывода параметра I, или параметр Т не выводится путем сравнения с параметром типа int(&)[sizeof(S<T>)]. 42 S,
11.2. Выводимый контекст 197 Эти ограничения не вызывают удивления, поскольку в приведенных примерах вывод может оказаться неоднозначным (может даже оказаться, что подходящих типов бесконечно много), хотя случай с полным именем типа иногда легко не заметить. Если в программе встречается невыводимый контекст, это еще не означает, что программа содержит ошибку или что анализируемый параметр не может принимать участия в выводе типа. Чтобы это проиллюстрировать, рассмотрим более сложный пример. // details/fppm.cpp template <int N> class X { public: typedef int I; void f(int) { } }; template<int N> void fppm(void (X<N>::*p)(X<N>::I)); int main() { fppm(&X<33>::f); // Все в порядке; вывод: N = 33 } Конструкция X<N>: : I, которая находится в шаблоне функции fppm(), не является выводимым контекстом; однако использующийся в ней компонент X<N>, указывающий на принадлежность классу и являющийся составной частью указателя на член класса, — это выводимый контекст. Когда выведенный из этого компонента параметр N подставляется в невыводимый контекст X<N>: : I, получается тип, совместимый с типом фактического аргумента (&Х<33>: : f). Таким образом, для этой пары "аргумент-параметр" вывод удается успешно выполнить до конца. Обратное утверждение тоже верно, т.е. если параметр типа состоит только из выводимого контекста, то это еще не означает, что вывод не приведет к противоречиям. Например, предположим, что у нас имеются надлежащим образом объявленные шаблоны X и Y. Рассмотрим приведенный ниже код. templatestypename T> void f(X<Y<T>, Y<T> >); void g() { f(X<Y<int>, Y<int> >()); // Все в порядке f(X<Y<int>, Y<char> >()); // ОШИБКА: вывод неудачен }
198 Глава 11. Вывод аргументов шаблонов Проблема, связанная со вторым вызовом шаблона функции f (), заключается в том, что для параметра Т на основе двух аргументов функции выводятся разные типы, что приводит к противоречию. В обоих вызовах аргументы функции являются временными объектами, полученными путем вызова конструктора по умолчанию для шаблона класса X. 11.3. Особые ситуации вывода Возможны две ситуации, в которых использующаяся для вывода пара (Л,Р) не берется из аргументов вызова функции и параметров шаблона функции. Первая — это когда вместо имени шаблона функции используется адрес этого шаблона. В этом случае Р — это параметризованный тип, который находится в операторе объявления шаблона функции, а Л — тип функции, на которую ссылается инициализируемый указатель или указатель, которому присваивается значение. Например: template<typename T> void f(T,T); void (*pf)(char, char) = &f; Здесь P — это void (T, T), a A — void (char, char). В результате вывода получается, что вместо параметра Т нужно подставить тип char, а указатель pf — инициализировать адресом экземпляра f <char>. Другая особая ситуация связана с шаблоном оператора преобразования типа. class S { public: template<typename Т, int N> operator T[N]&(); }; В этом случае пара (Р,А) получается таким образом, как если бы в нее входил аргумент того типа, к которому мы пытаемся преобразовать параметр типа, возвращаемый оператором преобразования. Приведенный ниже код иллюстрирует один из возможных вариантов этой ситуации. void f(int (&)[20]); void g(S s) { f(s); } В этом фрагменте делается попытка преобразовать S к типу int (&) [20]. Поэтому тип А — это int [ 20 ], а тип Р — это Т [N]. Процесс вывода выполняется успешно, причем в результате получается, что вместо параметра Т нужно подставить тип int, а вместо N — значение 20.
11.4. Допустимые преобразования аргументов 199 11.4. Допустимые преобразования аргументов Обычно в процессе вывода аргументов шаблонов предпринимается попытка найти такую подстановку для параметров шаблона функции, при которой параметризованный тип Р будет идентичен типу А. Однако, если это невозможно, для типов Р и А приемлемы отличия, перечисленные ниже. • Если в объявлении исходного параметра присутствует описатель ссылки, в типе, который подставляется вместо параметра />, может быть больше квалификаторов const и volatile, чем в типе Л. • Если тип А является обычным указателем или указателем на член класса, допустимо такое его преобразование к заменяемому им типу Р, при котором к типу А добавляется квалификатор const и/или volatile. • Если вывод относится не к шаблону оператора преобразования типов, подставляемый вместо параметра Р тип может быть базовым классом типа А или указателем на базовый класс для того класса, на который указывает тип А. Например: template<typename T> class B<T> { }; template<typename T> class D : В<Т> { }; template<typename T> void f(B<T>*); void g(D<long> dl) { f(&dl); // Вывод завершится успешно, если // вместо Т подставить long } Ослабление требований к совпадению типов допускается только в том случае, если не удалось добиться полного соответствия. Однако вывод будет успешным лишь тогда, когда параметру Р с учетом возможностей, предоставляемых допустимыми преобразованиями типов, соответствует только один тип А. 11.5. Параметры шаблона класса Вывод аргументов шаблонов возможен только для шаблонов функций и функций- членов. В частности, аргументы шаблонов классов не выводятся из аргументов, применяемых при вызове одного из конструкторов этого класса. template<typename T> class S (
200 Глава 11. Вывод аргументов шаблонов public: S(T b) : a(b) { } private: T a; }; S x(12); // ОШИБКА: параметр Т шаблона класса не выводится // из аргумента конструктора 12 11.6. Аргументы функции по умолчанию Как и в обычных функциях, в шаблонах функций можно задавать аргументы, которые по умолчанию будут подставляться в оператор вызова функции. template<typename T> void init(T* loc, Т const& val = TO) { *loc = val; } Как видно из этого примера, аргумент функции, подставляемый в оператор вызова по умолчанию, может зависеть от параметра шаблона. Такой зависимый аргумент по умолчанию инстанцируется только в том случае, когда никакой другой аргумент явно не указан. Исходя из этого принципа, приведенный ниже пример является корректным. class S { public: S(int, int); }; S s(0, 0); int main() { init(&s, S(7,42)); // T() для случая Т = S является // некорректным выражением, однако // из-за явного указания аргумента //Т() не инстанцируется } Даже если аргумент по умолчанию не является зависимым от параметра типа шаблона, он не может использоваться для вывода аргументов шаблона. Это означает, что приведенный ниже фрагмент кода в C++ недопустим. template<typename T> void f (T x = 42) {
11.7. Метод Бартона-Нэкмана 201 } int main () { f<int>-() ; // Все в порядке: Т = int f(); // ОШИБКА: Т невозможно вывести из // аргумента по умолчанию } 11.7. Метод Бартона-Нэкмана В 1994 году Джон Бартон (John J. Barton) и Ли Нэкман (Lee R. Nackman) представили метод применения шаблонов, названный ими ограниченным расширением шаблонов (restricted template expansion). Частично причиной развития этого метода послужил тот факт, что в то время шаблоны функций нельзя было перегружать , а пространства имен в большинстве компиляторов были недоступны. Чтобы проиллюстрировать указанный метод, предположим, что у нас есть шаблон класса Array, в котором требуется определить оператор равенства ==. Одна из возможностей — объявить этот оператор членом класса. Однако недостаток этого подхода состоит в том, что первый аргумент (связанный с указателем this) подчиняется правилам преобразования типов, отличным от тех, которые применимы ко второму аргументу. Поскольку удобнее, чтобы оператор == был симметричным относительно своих аргументов, лучше объявить его как функцию в области видимости пространства имен. Общая схема такого подхода к реализации оператора == может иметь следующий вид: template<typename T> class Array { public: }; template<typename T> bool operator == (Array<T> const& a, Array<T> const& b) { } Однако если перегрузка шаблонов функций не допускается, возникает проблема: в этом пространстве имен других шаблонов операторов == объявлять нельзя, а ведь они могут понадобиться для других шаблонов классов. Бартону и Нэкману удалось решить эту проблему путем определения в классе оператора равенства в виде обычной функции-друга. Возможно, вам стоит прочитать раздел 12.2, стр. 208, чтобы понять, как в современном C++ работает перегрузка шаблонов функций.
202 Глава 11. Вывод аргументов шаблонов template<typename T> class Array { public: friend bool operator == (Array<T> const& a, Array<T> const& b) { return ArraysAreEqual(a, b); } >; Предположим, что эта версия шаблона Array инстанцируется для типа float. Тогда в процессе инстанцирования объявляется функция-друг, с помощью которой реализован оператор равенства. Заметим, что сама по себе эта функция не есть результат инстанцирования шаблона функции. Это обычная функция (а не шаблон), введенная в глобальную область видимости, которая является побочным эффектом процесса инстанцирования. Поскольку это нешаблонная функция, ее можно перегружать с другими объявлениями оператора ==, причем для этого не используется возможность перегрузки шаблонов функций. Бартон и Нэкман дали этому методу название ограниченное расширение шаблонов, поскольку в нем не используется шаблон operator== (Т, Т), применимый для всех типов Т (другими словами, неограниченное расширение). Поскольку оператор operator== (Array<T>const&, Array<T>const&) определен в теле класса, он автоматически является встраиваемой функцией, и поэтому мы решили делегировать его реализацию шаблону функции ArraysAreEqual, которая не обязательно должна быть встроенной и которая вряд ли будет конфликтовать с другим шаблоном с таким же именем. В наше время те цели, для которых был придуман метод Бартона-Нэкмана, достижимы и без него, однако это не снижает интерес к данному методу, поскольку он позволяет генерировать в ходе инстанцирования шаблона класса функции, не являющиеся шаблонами. Поскольку эти функции не генерируются из шаблонов, для них не требуется вьюод аргументов шаблонов; к ним применимы обычные правила разрешения перегрузки (см. приложение Б, "Разрешение перегрузки"). Теоретически это может означать, что при проверке соответствия объявленных типов формальных параметров фактическим типам аргументов, с которыми вызывается функция, допускается неявное преобразование этих типов. На самом деле это преимущество незначительно, поскольку в стандартной современной реализации языка C++ (отличающейся от той, которой Бартон и Нэкман пользовались при разработке своей идеи) введенные в глобальную область видимости функции-друзья не видны безоговорочно за пределами своей исходной области видимости. Они видны только после поиска имен, зависящего от аргумента. Это означает, что аргументы, с которыми вызывается функция, уже должны быть ассоциированы с классом, другом которого является данная функция. Если же аргументы функции принадлежат типу, не имеющему отношения к классу, другом которого является эта функция, но такому, который можно в него преобразовать, то такая функция-друг найдена компилятором не будет.
11.8. Заключение 203 class S { }; template<typename T> class Wrapper {. private: T object; public: Wrapper (T obj) : object(obj) { // Неявное преобразование Т к типу Wrapper<T> } friend void f(Wrapper<T> constfc a) { } }; int main() { S s; Wrapper<S> w(s); f(w); // Правильно: класс Wrapper<S> связан с w f(s); // ОШИБКА: класс Wrapper<S> не связан с s } В данном примере вызов функции f (w) корректен, поскольку функция f () является другом класса Wrapper<S>, с которым связана переменная w4. Однако при вызове f (s) объявление функции-друга f (Wrapper<S> const&) невидимо, поскольку класс Wrapper<S>, в котором определена функция f (), не ассоциирован с аргументом s типа S. Поэтому, несмотря на допустимость неявного преобразования типа S к типу Wrapper<S> (с помощью конструктора класса Wrapper<S>), такое преобразование не рассматривается. Таким образом, определяя функцию-друга для шаблона класса, мы получаем незначительное преимущество по сравнению с определением ее в качестве обычного шаблона функции* 11.8. Заключение Вывод аргументов шаблонов для шаблонов функций был изначально заложен в язык программирования C++. Альтернативный подход с явно задаваемыми аргументами шаблона начал применяться в C++ существенно позже. Многие специалисты по C++ считают возможность введения функции-друга в глобальную область видимости вредной, поскольку при этом программы становятся более чувствительными к порядку инстанцирования. Одним из активных сторонников этой точки зрения был Билл Гиббоне (Bill Gibbons), работавший в то время над компилятором Заметим, что эта переменная также ассоциирована с классом S, поскольку этот класс представляет собой аргумент шаблона типа переменной w.
204 Глава 11. Вывод аргументов шаблонов Taligent, поскольку устранение зависимости от порядка инстанцирования обеспечивало возможность новых интересных сред разработки C++, для которых предполагалось использовать компилятор Taligent. Однако для работы метода Бартона-Нэкмана требовалось, чтобы возможность введения функции-друга в глобальную область видимости была сохранена в языке в ее текущем (ослабленном) виде. Интересно отметить, что многие слышали о методе Бартона-Нэкмана, но мало кто связывает этот термин с описанной здесь методикой. В результате в литературе можно найти описание многих других методов, использующих функции-друзья и шаблоны, которые совершенно неверно называют методом Бартона-Нэкмана (см., например, раздел 16.5, стр. 324).
Глава 12 Специализация и перегрузка Сейчас вы уже знаете, как шаблоны C++ обеспечивают расширение обобщенного определения в семейство связанных классов или функций. Хотя это и мощный механизм, существует много ситуаций, в которых при замене параметров шаблона обобщенная форма работы далека от оптимальной. Язык C++ кое в чем уникален, он поддерживает обобщенное программирование, поскольку обладает богатым набором возможностей, позволяющих осуществлять прозрачную подмену обобщенного определения более специализированными. В этой главе представлены два механизма языка C++, которые позволяют реализовать полезные отступления от обобщенного подхода: специализация шаблона и перегрузка шаблонов функций. 12.1. Когда обобщенный код не совсем хорош Рассмотрим приведенный ниже пример. template<typename T> class Array { private: Т* data; public: Array(Array<T> constfc); Array<T>& operator = (Array<T> const&); void exchange__with(Array<T>* b) { T* tmp = data; data = b->data; b->data = tmp; } T& operator[] (size.tk) { return data[k]; } ) r
206 Глава 12. Специализация и перегрузка template<typename T> inline void exchange(T* а, Т* b) { Т tmp(*a); *а = *Ь; *b = tmp; } Для простых типов обобщенная реализация функции exchange () работает хорошо. Однако дня типов со сложными операциями копирования обобщенная реализация может быть значительно более ресурсоемкой (в аспекте использования как машинного времени, так и памяти), чем реализация, настроенная под конкретную структуру данных. В нашем примере обобщенная реализация требует одного вызова конструктора копирования шаблона Аггау<Т> и двух вьповов его оператора копирующего присвоения. Для больших структур данных создание таких копий часто сопровождается копированием относительно больших объемов памяти. Однако функциональность exchange () часто может заменяться просто обменом указателями, подобно тому, как это делается в функции-члене exchange_wi th (). 12.1.1. Прозрачная настройка В предыдущем примере функция-член exchange_with() обеспечивала эффективную альтернативу обобщенной функции exchange (). Тем не менее по ряду причин использование другой функции неудобно. 1. Пользователи класса Array должны помнить о дополнительном интерфейсе и по возможности аккуратно им пользоваться. 2. Обобщенные алгоритмы могут не уметь отличать различные возможные варианты действий. template<typename T> void generic_algorithm(T* x, T* у) { exchange(х, у) ; // Каким образом выбрать // правильный алгоритм? } По этим соображениям шаблоны C++ обеспечивают прозрачные способы настройки шаблонов функций и классов. Для шаблонов функций это достигается через механизм перегрузки. Например, можно написать перегруженный набор шаблонов функций quick__exchange () как показано ниже. template<typename T> inline void quick_exchange(T* a, T* b) // (1) { Т tmp(*a); *а = *b;
12.1. Когда обобщенный код не совсем хорош 207 *b = tmp; } template<typename T> inline void quick__exchange(Array<T>* a, Array<T>* b) // (2) { a->exchange_with(b) ; } ' . void demo(Array<int>* pi, Array<int>* p2) { int x, y; ,quick_exchange(&x, &y) ; // использует (1) quick_exchange(pl, p2); // использует (2) } Первый вызов quick_exchange () имеет два аргумента типа int*, поэтому вывод аргументов выполняется успешно только для первого шаблона (объявленного в точке (1)), когда тип Т заменяется типом int. Поэтому не возникает сомнений относительно того, какую функцию нужно вызвать. Второй же вызов соответствует обоим шаблонам: жизнеспособные функции для вызова quick_exchange(pl, p2) получаются как заменой Аг- ray<int> на Т в первом шаблоне, так и заменой int во втором шаблоне. Кроме того, обе замены дают функции с типами параметров, которые точно соответствуют типам аргументов во втором вызове. Обычно это позволяет заключить, что вызов неоднозначен, однако (как выяснится позже) язык C++ считает второй шаблон "более специализированным", чем первый. При прочих равных условиях разрешение перегрузки отдает предпочтение более специализированному шаблону и поэтому выбирает шаблон из точки (2). 12.1.2. Семантическая прозрачность Использование перегрузки, как было показано в предыдущем разделе, очень полезно при достижении прозрачной настройки процесса инстанцирования. При этом важно понимать, что такая "прозрачность" существенно зависит от деталей реализации. Чтобы проиллюстрировать это, рассмотрим реализацию нашей функции quick__exchange (). Хотя и обобщенный алгоритм, и алгоритм, настроенный для типов Аггау<Т>, заканчиваются обменом значений, на которые указывают указатели, побочные эффекты этих операций существенно отличаются. Яркой иллюстрацией тому может служить код, который сравнивает обмен структурных объектов с обменом шаблонов Аггау<Т>. struct S { int х; > si, S2; void distinguish(Array<int> al, Array<int> a2) { int* P = &al[0];
208 Глава 12. Специализация и перегрузка int* q = &sl.x; al[0] = sl.x = 1; a2[0] = s2.x = 2; quick_exchange(&al, &a2); // после этого *р == 1 // (все еще) quick_exchange(&sl, &s2); // после этого *q == 2 } Этот пример показывает, что после вызова quick_exchange () указатель р на первый массив Array становится указателем на второй массив. Однако указатель на объект si, не являющийся массивом, продолжает указывать в структуру si даже после выполнения операции обмена. Единственное изменение — поменялись местами значения, на которые указывают указатели. Это весьма существенное отличие, которое может сбивать с толку пользователей шаблона. Применение префикса quick_ позволяет привлечь внимание к тому, что данная реализация представляет собой сокращенный вариант нужной операции. Однако первоначальный обобщенный шаблон exchange () может при этом содержать оптимизацию для шаблонов Аггау<Т>. template<typename T> void exchange(Array<T>* a, Array<T>* b) T* p = &a[0]; fl lb" К * / 6 ^t/tf/J *+< T* q = &b[0]; ZO^0***** for (size_t k = a->size(); —k!= 0; ) { ? ^ exchange(p++, q++) ; ) > PAW- } Преимущество этой версии обобщенного кода заключается в том, что при этом не требуется создавать потенциально большой временный массив Аггау<Т>. Шаблон exchange () вызывается рекурсивно, чем достигается хорошая производительность даже для таких типов, как Array<Array<char> >. Отметим также, что более специализированная версия шаблона не объявляется встроенной, поскольку выполняет значительный объем работы. В то же время первоначальная обобщенная реализация является встроенной, поскольку выполняет только несколько операций (каждая из которых потенциально ресурсоемка). 12.2. Перегрузка шаблонов функций В предыдущем разделе было показано, что возможно сосуществование двух шаблонов функций с одним и тем же именем, даже если они могут быть инстанцированы с параметрами идентичных типов. Приведем еще один простой пример этого. // details/funcoverload.hpp template<typename T> int f(T)
12.2. Перегрузка шаблонов функций 209 { return 1 ; } tempiate<typename T> int f(T*) { return 2; } Когда тип Т заменяется типом int* в первом шаблоне, получается функция, у которой есть точно такие же типы параметров (и возвращаемых значений), что и у функции, получаемой при замене типа int типом Т во втором шаблоне. Сосуществовать могут не только эти шаблоны, но и их экземпляры, даже если у них идентичны типы параметров и возвращаемых значений. Приведенный ниже пример демонстрирует, как можно вызвать две такие сгенерированные функции с помощью синтаксиса явного аргумента шаблона (в предположении предыдущих объявлений шаблона). // details/funcoverload.cpp ttinclude <iostream> #include "funcoverload.hpp" int main() { std::cout << f<int*>((int*)0) << std::endl; std::cout << f<int>((int*)0) << std::endl; } Результатом выполнения этой программы будет следующий вывод: 1 2 Чтобы объяснить работу программы, детально проанализируем вызов f <int*> ((int*) 0)} Синтаксис f <int*> обозначает, что первый параметр шаблона f нужно заменить значением типа int* без использования вывода аргумента шаблона. В этом случае существует более одного шаблона f и поэтому создается набор перегрузки, содержащий две функции, сгенерированные из шаблонов: f<int*> (int*) (сгенерированная из первого шаблона) и f < int * > (int * *) (сгенерированная из второго шаблона). Аргумент вызова (int *) 0 имеет тип int*, что соответствует только функции, сгенерированной из первого шаблона. Следовательно, это и есть функция, которая будет вызвана в конечном итоге. Подобный анализ можно сделать и для второго вызова. Заметим, что выражение 0 — это целое число, а не константный нулевой указатель. Оно становится константным нулевым указателем только после специального неявного преобразования, однако это преобразование не учитывается при выводе аргумента шаблона.
210 Глава 12. Специализация и перегрузка 12.2.1. Сигнатуры Две функции могут сосуществовать в программе, если у них разные сигнатуры. Оп- ределим сигнатуру как приведенную ниже информацию . 1. Не полностью квалифицированное имя функции (или имя шаблона функции, из которого она сгенерирована). 2. Область видимости класса или пространства имен (и, если это имя имеет внутреннее связывание, единица трансляции), в котором объявлено имя. 3. Классификация функции как const, volatile или const volatile (если это функция-член с данным спецификатором). 4. Типы параметров функции (перед подстановкой параметров шаблона, если функция генерируется из шаблона функции). 5. Если функция генерируется из шаблона функции, то тип ее возвращаемого значения. 6. Параметры и аргументы шаблона, если функция генерируется из шаблона функции. Это означает, что в одной и той же программе могут сосуществовать следующие шаблоны и их экземпляры: template<typename Tl, typename T2> void fl(Tl, T2); tempiate<typename Tl, typename T2> void fl(T2, Tl); template<typename T> long f2(T); template<typename T> char f2(T); Однако их не всегда можно использовать, если они объявлены в одной области видимости, поскольку при их инстанцировании возникает неоднозначность перегрузки. #include <iostream> template<typename Tl , typename T2> void fl(Tl, T2) { std::cout « "fl(Tl, T2)\n"; } template<typename Tl, typename T2> void fl(T2, Tl) { Это определение отличается от того, которое дано в стандарте C++, однако следствия у них эквиваленты.
12.2. Перегрузка шаблонов функций 211 std::cout « "fl(T2, Tl)\n"; } // Пока все хорошо int main () { fl<char,char>('a','b'); // Ошибка: неоднозначность } Здесь функция fl<Tl=char, T2=char>(Tl,T2) может сосуществовать с функцией fl<Tl=char, T2=char>(T2,Tl), однако разрешение перегрузки никогда не отдаст предпочтения одной из них. Если эти шаблоны появляются в различных единицах трансляции, эти два экземпляра действительно могут существовать в одной и той же программе (при этом компоновщик не должен жаловаться на двойное определение, поскольку их сигнатуры различны). // Единица трансляции 1: #include <iostream> template<typename Tl, typename T2> void fl(Tl, T2) { std::cout « "f1(Tl,T2)\n"; } void g() { fl<char,char>('a','b'); } // Единица трансляции 2: #include <iostream> template<typename Tl, typename T2> void fl(T2, Tl) { std::cout « "f1(T2,T1)\n"; } extern void g(); // Определена в единице трансляции (1) int main() { fl<char,char>('a','b'); g(); }
212 Глава 12. Специализация и перегрузка Эта программа работает и выдает следующее: fl(T2/Tl) fl(Tl,T2) 12.2.2. Частичное упорядочение перегруженных шаблонов функций Вернемся к рассмотренному ранее примеру. #include <iostream> template<typename T> int f(T) { return 1; } template<typename T> int f(T*) { return 2; } int main() { std::cout « f<int*>{(int*)0) « std::endl; std::cout « f<irit> ( (int*) 0) « std::endl; } После подстановки списков аргументов шаблонов (<int*> и <int>) разрешение перегрузки заканчивается выбором правильной вызываемой функции. Однако выбор функции происходит даже в том случае, если аргументы шаблона явно не указываются. В этом случае вступает в игру вывод аргумента шаблона. Чтобы обсудить этот механизм, несколько модифицируем функцию main () из предыдущего примера. #include <iostream> template<typename T> int f(T) { return 1; } template<typename T> int f(T*) { return 2;
12.2. Перегрузка шаблонов функций 213 } int main () { std::cout « f(0) « std::endl; std::cout « f((int*)0) « std::endl; } Рассмотрим первый вызов (f (0)): здесь int — тип аргумента, который соответствует типу параметра первого шаблона, если заменить Т на int. Однако тип параметра второго шаблона — это всегда указатель, поэтому после вывода кандидатом для вызова будет только экземпляр, сгенерированный из первого шаблона. В этом случае разрешение перегрузки тривиально. Второй вызов (f ( (int *) 0)) более интересен: осуществить вывод аргумента удается для обоих шаблонов, что дает функции f <int *> (int *) и f <int> (int *). В аспекте традиционного разрешения перегрузки обе функции одинаково хороши для вызова с аргументом int*, что соответствует неоднозначности вызова (см. приложение Б, "Разрешение перегрузки"). Однако в таких случаях вступает в игру дополнительный критерий перегрузки. Выбирается функция, сгенерированная из "более специализированного" шаблона. Здесь, как вы скоро увидите, второй шаблон считается более специализированным, а потому результатом работы этой программы вновь будет 1 2 12.2.3. Правила формального упорядочения В нашем последнем примере интуитивно вполне понятно, что второй шаблон "более специальный", чем первый, поскольку первый может быть подстроен почти под любой тип аргумента, тогда как второй разрешает только типы-указатели. Однако другие примеры могут оказаться не столь очевидными. Далее описана точная процедура определения того, является ли один шаблон, участвующий в наборе перегрузки, более специализированным, чем другой. Отметим, однако, что это правила лишь частичного упорядочения: возможна ситуация, когда ни один из шаблонов не будет считаться более специализированным, чем другой. Если разрешение перегрузки должно выбирать между такими шаблонами, решение принято не будет и в программе возникнет ошибка неоднозначности. Предположим, сравниваются два шаблона функций со сходными именами f ti и f t2, которые кажутся жизнеспособными для данного вызова функции. Параметры вызова функции, которые используют аргументы по умолчанию или многоточия, игнорируются. Затем создаются два искусственных списка типов аргументов (а для шаблона функции преобразования типов — возвращаемого типа) путем подстановки каждого параметра шаблона. 1. Заменим каждый параметр типа шаблона уникальным искусственным типом. 2. Заменим каждый шаблонный параметр шаблона уникальным искусственным шаблоном класса.
214 Глава 12. Специализация и перегрузка 3. Заменим каждый шаблонный параметр, не являющийся типом, уникальным искусственным значением соответствующего типа. Если вывод аргумента второго шаблона из первого синтезированного списка типов аргументов происходит успешно при точном соответствии, но не наоборот, то говорят, что первый шаблон является более специализированным, чем второй. Если вывод аргумента первого шаблона для второго синтезированного списка типов аргументов происходит успешно при точном соответствии, но не наоборот, то говорят, что второй шаблон является более специализированным, чем первый. В ином случае (если нет ни одного успешного вывода или же оба вывода успешны) упорядочения шаблонов не происходит. Попробуем применить этот подход к двум шаблонам в предыдущем примере. Для этих шаблонов синтезируется два списка типов аргументов путем замены шаблонных параметров описанным выше способом: (А1) и (А2 *) (где А1 и А2 — уникальные искусственные типы). Очевидно, что вывод первого шаблона для второго списка типов аргументов происходит успешно при замене А2* на Т. Однако тип Т* из второго шаблона невозможно сделать соответствующим типу А1 из первого списка, который не является типом указателя. Следовательно, формально можно заключить, что второй шаблон более специализирован, чем первый. Наконец, рассмотрим более сложный пример с использованием нескольких параметров функций. template<typename T> void t(T*, T const* =0, ...); template<typename T> void t(T const*, T*, T* = 0); void example(int* p) { t(p, p); } Прежде всего, поскольку реальный вызов не использует параметр многоточия для первого шаблона, а последний параметр второго шаблона покрывается аргументом по умолчанию, эти аргументы при частичном упорядочении игнорируются. Отметим, что аргумент первого шаблона по умолчанию не используется. Поэтому соответствующий параметр участвует в упорядочении. Созданные списки типов аргументов— это (А1*,А1 const*) и (A2l const*,А2*)- Вывод аргументов шаблона (Al*, Al const*) для второго шаблона успешен при замене Т на Al const, однако результирующее соответствие не точное, поскольку для вызова t<Al const>(Al const*, Al const*, Al const* = 0) с аргументами (Al*/ Al const*) требуется дополнительное уточнение типов. Точно так же нельзя найти точное соответствие при выводе аргументов шаблона для первого шаблона из списка типов аргументов (А2 const*, А2 *). Следовательно, между двумя шаблонами нет отношения упорядочения и вызов неоднозначен.
12.3. Явная специализация 215 Формальные правила упорядочения обычно обеспечивают возможность очевидного выбора шаблонов функций. Тем Не менее можно привести множество примеров, когда интуитивно очевидный выбор оказывается невозможным. Вероятно, данные правила упорядочения в будущем могут быть пересмотрены с тем, чтобы такие ситуации стали разрешимыми. 12.2.4. Шаблоны и нешаблоны Шаблоны функций можно перегружать нешаблонными функциями. При прочих равных условиях при выборе реальной функции вызова нешаблонная функция предпочтительнее. Приведенный ниже пример иллюстрирует это. // details/nontmpl.cpp #include <string> #include <iostream> template<typename T> std::string f(T) return "Template"; std::string f(int&) return "Nontemplate"; int main() { int x = 7; std::cout << f(x) « std::endl; Результат выполнения программы: Nontemplate 12.3. Явная специализация Возможность перегружать шаблоны функций в сочетании с правилами частичного упорядочения при выборе обеспечивающего наилучшее соответствие шаблона функции позволяет Добавлять к обобщенной реализации специализированные шаблоны для повышения эффективности кода. Однако перегружать шаблоны классов нельзя. Поэтому для обеспечения прозрачной настройки шаблонов классов используется другой механизм — явная специализация. Стандартный термин явная специализация означает свойство языка, известное как полная специализация. Оно обеспечивает реализацию шаблона с полностью замененными шаблонными
216 Глава 12. Специализация и перегрузка параметрами, когда никаких неизвестных шаблонных параметров не остается. Шаблоны классов и шаблоны функций могут быть полностью специализированными, а члены шаблонов классов — определенными за пределами тела определения класса (т.е. функции-члены, вложенные классы и статические данные-члены). В одном из следующих разделов рассматривается частичная специализация. Она напоминает полную специализацию, но вместо полной замены шаблонных параметров в ней остается некоторая параметризация. Полная и частичная специализации одинаково "явно" присутствуют в нашем исходном коде, поэтому при обсуждении мы избегаем термина явная специализация. Ни полная, ни частичная специализация не добавляют полностью новый шаблон или его экземпляр. Вместо этого данные конструкции предоставляют возможность альтернативного определения для экземпляров, которые уже неявно определены в обобщенном {неспециализированном) шаблоне. Это довольно важное концептуальное отличие от перегрузки шаблонов. 12.3.1. Полная специализация шаблона класса Полная специализация вводится последовательностью трех лексем: template, < и > . Кроме того, после объявления имени класса идут аргументы шаблона, для которого объявляется специализация. Это проиллюстрировано ниже. template<typename T> class S { public: void info() { std::cout << "generic (S<T>::info())\n"; } }; templateo class S<void> { public: void msg() { std::cout << "fully specialized (S<void>::msg())\n"; } }; Обратите внимание, что реализация полной специализации не требует какой-либо связи с обобщенным определением. Это позволяет создавать функции-члены с различными именами (info и msg). Связь между ними определяется исключительно именем шаблона класса. Список определенных аргументов шаблона должен соответствовать списку параметров шаблона. Например, некорректно использовать значение, не являющееся типом, Тот же префикс требуется и при объявлении полной специализации шаблона функции. Ранние версии языка C++ не включали этот префикс, однако добавление шаблонов-членов потребовало дополнительного синтаксиса для разрешения неоднозначности в сложных случаях специализации.
12.3. Явная специализация 217 вместо шаблонного параметра типа. Указывать аргументы для параметров со значениями по умолчанию необязательно. template<typename T> class Types { public: typedef int I; }; template<typename T, typename U = typename Types<T>::I> class S; // (1) templateo class S<void>,{ // (2) public: void f(); }; templateo class S<char, char>; // (3) templateo class S<char, 0>; // Ошибка: О не может // заменить U int main () { S<int>* S<int> S<void>* S<void,int> S<void,char> S<char,char> Pi; el; pv; sv; e2; e3; // // // // // // // // // // // } templateo class S<char, char> {// Определение для (З) Данный пример также показывает, что объявления полной специализации (и шаблонов) не обязательно должны быть определениями. Однако, если объявлена полная специализация, для данного набора аргументов шаблона обобщенное определение никогда не используется. Следовательно, если определение необходимо, но его нет, в программе содержится ошибка. Для специализации шаблонов класса иногда полезно предваритель- ОК: использует (1) , определение не требуется Ошибка: использует (1), но определения нет ОК: использует (2) ОК: использует (2), определение есть Ошибка: использует (1), но определения нет Ошибка: использует (3), но определения нет
218 Глава 12. Специализация и перегрузка ное объявление типов, что позволяет создавать взаимно зависимые типы. Объявление полной специализации идентично объявлению обычного класса (это не шаблонное объявление). Все, что их отличает, — это синтаксис и тот факт, что объявление должно соответствовать предыдущему объявлению шаблбна. Поскольку это не объявление шаблона, члены полной специализации шаблона класса могут быть определены с помощью обычного синтаксиса определения члена вне класса (иными словами, нельзя указывать префикс templateo). template<typename T> class S; templateo class S<char**> { public: void print() const; }; // Перед следующим определением нельзя использовать // префикс templateo void S<char**>::print() { std::cout « "pointer to pointer to char\n"; } Ниже приведен более сложный пример этой концепции. template<typename T> class Outside { public: template<typename U> class Inside { }; }; templateo class Outside<void> { // Нет никакой связи между следующим вложенным // классом и вложенным классом, определенным в // обобщенном шаблоне template<typename U> class Inside { private: static int count; }; } ; // Перед следующим определением нельзя использовать // префикс templateo
12.3. Явная специализация 219 template<typename U> int Outside<void>::Inside<U>::count = 1; Полная специализация — это замена инстанцирования определенного обобщенного . шаблона. При этом некорректно одновременно иметь как явную, так и сгенерированную версии шаблона в одной и той же программе. Попытка использовать их обе в одном и том же файле обычно отслеживается компилятором. template <typename T> class Invalid { }; Invalid<double> xl; // Вызывает инстанцирование // Invalid<double> templateo class Invalid<double>; // Ошибка: Invalid<double> уже // инстанцирован! К сожалению, при использовании в различных единицах трансляции проблема не отслеживается так легко. Следующий некорректный пример кода на C++ состоит их двух файлов. Этот код компилирует и связывает несколько реализаций, однако он некорректен и опасен. // Единица трансляции 1: template<typename T> class Danger { public: enum { max = 10 }; }; char buffer[Danger<void>::max]; // Использует обобщенное // значение extern void clear(char const*); int main() { clear(buffer); } // Единица трансляции 2: template<typename T> class Danger; templateo class Danger<void> {
220 Глава 12. Специализация и перегрузка public: enum { max = 100 }; }; void clear(char const* buf) { // Несоответствие границ массива! for (int k = 0; k < Danger<void>::max; ++k) { buf[k]= '\0'; } } Этот пример был придуман специально, чтобы показать, насколько необходимо следить за тем, чтобы объявление специализации было видно всем пользователям обобщенного шаблона. Практически это означает, что объявление специализации должно идти после объявления шаблона в его заголовочном файле. Если обобщенная реализация берет начало из внешнего источника (такого, что соответствующие заголовочные файлы не должны изменяться), то желательно, хотя и не обязательно, создать заголовочный файл, включающий обобщенный шаблон с последующим объявлением специализаций, чтобы избежать таких труднообнаруживае- мых ошибок. В целом лучше избегать специализации шаблона, происходящего из внешнего источника, если не указано, что он для этого предназначен. 12.3.2. Полная специализация шаблона функции Синтаксис и принципы (явной) полной специализации шаблона функции во многом такие же, как и в случае полной специализации шаблона класса. Однако здесь вступают в игру перегрузка и вывод аргумента. При объявлении полной специализации можно пропускать явные аргументы шаблона, если этот шаблон можно определить с помощью вывода аргумента (используя в качестве типов аргументов типы параметров, указанные в объявлении) и частичного упорядочения. template<typename T> int f(T) // (1) { return l; } template<typename T> int f(T*) // (2) { return 2; } templateo int f(int) // OK: специализация (1) { return 3;
12.3. Явная специализация 221 } templateo int f(int*) // OK: специализация (2) { return 4; } Полная специализация шаблона функции не может включать значения аргумента по умолчанию. Однако любые аргументы по умолчанию, указанные для шаблона, подвергаемого специализации, остаются применимыми и для явной специализации. template<typename T> int f (Т, Т х = 42) return x; templateo int f(int, int =35) // ОШИБКА! return 0; template<typename T> int g(T, T x = 42) return x; templateo int g(int, int y) return y/2; int main(-) std::cout « g(0) « std::endl; // Программа должна // вывести 21 Полная специализация во многом подобна обычному объявлению (точнее, обычному повторному объявлению). В частности, она не объявляет шаблон и, следовательно, в программе Должно быть только одно определение невстраиваемой полной специализации шаблона функции. Однако необходимо следить за тем, чтобы объявление полной специализации следовало после шаблона, что предотвратит попытки использования функции, сгенерированной из шаблона. Поэтому объявления шаблона g в предыдущем примере лучше размещать в двух файлах. Файл интерфейса может выглядеть, как показано ниже. #ifndef TEMPLATE__G_HPP #define TEMPLATE__G_HPP
222 Глава 12. Специализация и перегрузка // Объявление шаблона следует поместить //в заголовочном файле: template<typename T> int g(T, T x = 42) { return x; } // Объявление специализации запрещает инстанцирование // шаблона; определения здесь быть не должно //во избежание ошибки многократного определения templateo int g(int, int у); #endif // TEMPLATE_G_HPP Соответствующий файл реализации может быть таким: #include "template_g.hpp" templateo int g(int, int y) { return y/2; } В качестве альтернативы специализация может быть встраиваемой; в этом случае ее объявление может (и должно) быть помещено в заголовочном файле. 12.3.3. Полная специализация члена Полностью специализироваться могут не только шаблоны членов, но и обычные статические данные-члены и функции-члены шаблонов класса. Синтаксис требует наличия префикса templateo для каждого шаблона класса. Если специализируется шаблон члена, то для указания этого необходимо добавить префикс templateo. Чтобы проиллюстрировать это, представим, что у нас есть приведенные ниже объявления. template<typename T> . class Outer { // (1) public:' template<typename U> class Inner { // (2) private: static int count; // (3) }; static int code; // (4) void print() const { // (5) std::cout « "generic"; } };
12.3. Явная специализация 223 template<typename T> int Outer<T>::code =6; // (6) template<typename T> template<typename U> int Outer<T>::Inner<U>::count =7; // (7) templateo class Outer<bool> { // (8) public: template<typename U> class Inner { // (9), private: static int count; // (10) }; void print() const { // (11) } }; Обычные члены code в точке (4) и print () в точке (5) обобщенного шаблона Outer (1) имеют единый включающий шаблон класса и, следовательно, требуют одного префикса templateo для полной специализации для конкретного набора аргументов шаблона. templateo int Outer<void>::code = 12; templateo void Outer<void>::print() { std::cout « "Outer<void>"; } Эти определения используются поверх обобщенных в точках (4) и (5) для класса Outer<void>, однако другие члены класса Outer<void> все еще генерируются из шаблона в точке (1). Заметим, что после этих объявлений он утрачивает силу в плане обеспечения явной специализации для Outer<void>. Как и в случае полной специализации шаблона функции, нам нужен способ объявления специализации обычного члена шаблона класса без указания определения (чтобы избежать многократных определений). Хотя для функций-членов и статических данных-членов обычных классов в C++ не разрешены неопределяющие объявления вне класса, последние будут корректны при специализации членов шаблонов классов. Предыдущие определения могут быть объявлены следующим образом: templateo int Outer<void>::code; templateo void Outer<void>::print();
224 Глава 12. Специализация и перегрузка Внимательный читатель может заметить, что неопределяющее объявление полной специализации Outer<void>: : code имеет точно тот же синтаксис, что и при определении с помощью конструктора по умолчанию. Это действительно так, однако такие объявления всегда интерпретируются как неопределяющие. Таким образом, нет способа определения полной специализации статических данных- членов с типом, который может быть инициализирован с помощью конструктора по умолчанию! class DefaultlnitOnly { public: DefaultlnitOnly() { } private : DefaultlnitOnly(DefaultlnitOnly const&); // Копирование запрещено }; template<typename T> class Statics { private: T sm; }; // Следующий код — это объявление; // для определения синтаксиса не существует templateo DefaultlnitOnly Statics<DefaultInitOnly>::sm; Шаблон члена Outer<T>: : Inner также можно специализировать для данного аргумента шаблона без влияния на другие члены Outer<T>, для которого специализируется шаблон члена. И вновь наличие включающего шаблона приводит к необходимости префикса templateo. В результате получается такой код: templateo template<typename X> class Outer<wchar_t>::Inner { public: static long count; // Тип члена изменен }; templateo template<typename X> long Outer<wchar__t> : :Inner<X>: : count; Шаблон Outer<T>: : Inner также может быть полностью специализированным, однако только для данного экземпляра Outer<T>. Теперь нам нужно два префикса template<>: один из-за включающего класса и один из-за того, что полностью специализируется (внутренний) шаблон.
12.4. Частичная специализация шаблона класса 225 templateo templateo class Outer<char>::Inner<wchar_t> { public: enum { count = 1 }; }; // Приведенный далее код некорректен: // templateo не может следовать за // списком параметров шаблона template<typename X> templateo class Outer<X>::Inner<void>; // ОШИБКА! Сравните это со специализацией шаблона-члена Outer<bool>. Поскольку он уже полностью специализирован, включающего шаблона нет и нам нужен только один префикс templateo. templateo class Outer<bool>::Inner<wchar_t> { public: enum { count = 2 } ; }; 12.4. Частичная специализация шаблона класса Полная специализация шаблона часто полезна, но иногда естественным оказывается желание специализировать шаблон класса для семейства аргументов шаблона, а не только для конкретного набора аргументов. Например, предположим, что у нас есть шаблон класса, реализующий связанный список. template<typename T> class List { // (1) public: void append(T const&); inline size_t length() const; }; Большой проект с использованием этого шаблона может инстанцировать свои члены для многих типов. Для невстраиваемых функций-членов (скажем, List<T>: :append()) это может вызьшать заметный рост объектного кода. Однако с низкоуровневой точки зрения код List<int*>: : append () и код List<void*>:: append () идентичны. Иными словами, все списки указателей используют одну и ту же реализацию. Хотя это нельзя вьфазить кодом C++, можно приблизиться к цели, отметив, что все списки указателей List должны инстан- Цироваться из другого определения шаблона.
226 Глава 12. Специализация и перегрузка template<typename T> class List<T*> { // (2) private: ,- , List<void*> impl; public: void append(T* p) { impl.append(p); } size__t length () const { return impl.length(); } }; В этом контексте исходный шаблон в точке (1) называется первичным шаблоном, а следующее за ним определение— частичной специализацией (поскольку аргументы шаблона, для которых это определение шаблона должно быть использовано, указаны только частично). Синтаксис, который характеризует частичную специализацию, является комбинацией объявления списка параметров шаблона (template<. . . >) и набора явно указанных аргументов шаблона с именем шаблона класса (в нашем примере это <Т*>). Наш код таит в себе проблему, поскольку List<void*> рекурсивно содержит член того же типа List<void*>. Для прерывания рекурсии перед предыдущей частичной специализацией можно ввести полную специализацию. templateo class List<void*> { // (3) void append (void* p); inline size_t length0 const; }; Этот код вполне корректно работает, поскольку полная специализация предпочтительнее частичной. В результате все функции-члены списков указателей List переадресовываются (с помощью встраиваемых функций) реализации списка List<void*>. Это эффективный способ борьбы с так называемым разбуханием кода (в чем часто обвиняют шаблоны C++). Существует ряд ограничений на объявления списков параметров и аргументов частичной специализации. Приведем некоторые из них. 1. Аргументы частичной специализации должны отвечать виду соответствующих параметров первичного шаблона (это могут быть параметры, представляющие собой тип, параметры, не являющиеся типом, или шаблонные параметры).
12.4. Частичная специализация шаблона класса 227 2. Список параметров частичной специализации не может иметь аргументов по умолчанию; вместо них используются аргументы по умолчанию первичного шаблона класса. 3. Аргументы частичной специализации, не являющиеся типами, должны быть либо независимыми значениями, либо простыми параметрами, не являющимися типами. Они не могут быть сложными зависимыми выражениями наподобие 2*N (где N — параметр шаблона). 4. Список аргументов шаблона частичной специализации не должен быть идентичен (без учета переименования) списку параметров первичного шаблона. Приведем пример, иллюстрирующий эти ограничения. template<typename T, int I = 3> class S; // Первичный шаблон template<typename T> class S<int, T>; // ОШИБКА: несоответствие вида // параметра template<typename T = int> class S<T, 10>; // ОШИБКА: не разрешены аргументы // по умолчанию template<int I> class S<int, I*2>; // ОШИБКА: не разрешены выражения, // не являющиеся типами template<typename U, int K> class S<U, K>; // ОШИБКА: нет существенных отличий // от первичного шаблона Любая частичная специализация, как и любая полная специализация, связана с первичным шаблоном. При использовании шаблона сначала всегда ищется первичный шаблон. Кроме того, проверяется соответствие аргументов аргументам связанных специализаций для определения того, какая реализация шаблона должна быть выбрана. Если найдено несколько соответствующих специализаций, из них выбирается наиболее специализированная (в смысле, определенном для перегруженных шаблонов функций). Если ни одну из них нельзя назвать "наиболее специализированной", в программе содержится ошибка неоднозначности. Наконец, следует заметить, что частичная специализация шаблона класса вполне может иметь большее количество параметров, чем первичный шаблон. Рассмотрим снова наш список обобщенного шаблона List (объявленный в точке (1)). Мы уже рассматривали оптимизацию списка указателей. Но у нас может возникнуть желание сделать то же самое и с определенными типами "указатель на член". Ниже приведен код, который оптимизирует список для указателей на указатель на член.
228 Глава 12. Специализация и перегрузка template<t,ypename C> class List<void* C::*> { // (4) public: // Частичная специализация для любого члена // типа указатель на void* // Любой иной тип указателя на указатель на член // будет использовать ее typedef void* С: : *ElementType,- void append(ElementType pm); inline size_t length() const; }; template<typename T, typename C> class List<T* C::*> { // (5) private: List<void* C::*> impl; public: // Частичная специализация для любого типа // указателя на указатель на член, за // исключением члена типа указателя на void*, // который обработан ранее. Заметим, что у этой // частичной специализации два шаблонных параметра, // тогда как у первичного шаблона - только один typedef T* С: :,*ElementType; void append(ElementType pm) { impl.append((void* С::*)pm); } inline size_t length() const { return impl.length(); } }; В дополнение к замечаниям относительно числа шаблонных параметров отметим, что общая реализация, определенная в точке (4), которую используют все остальные объекты (из объявления в точке (5)), сама является частичной специализацией (для случая простого указателя — это полная специализация). Однако очевидно, что специализация в точке (4) более специализирована, чем в точке (5), так что неоднозначности не возникает.
12.5. Заключение 229 12.5. Заключение Полная специализация шаблона была частью механизма шаблонов C++ с самого начала. Перегрузка шаблона функции и частичная специализация шаблона класса появились значительно позже. Компилятор HP aC++ стал первым компилятором, реализовавшим перегрузку шаблона функции, a EDGC++ первым реализовал частичную специализацию шаблона класса. Принципы частичного упорядочения, описанные в этой главе, первоначально были разработаны Стивом Адамчиком (Steve Adamczyk) и Джоном Спайсе- ром (John Spicer) (оба из EDG). Возможность применения специализаций шаблонов для завершения рекурсивного шаблонного определения (как в примере с List<T*>, приведенном в разделе 12.4) известна уже давно. Однако Эрвин Анрух (Erwin Unruh), вероятно, первый заметил, что это приводит к интересной концепции шаблонного метапрограммирования: использование механизма ин- станцирования шаблонов для выполнения нетривиальных вычислений во время компиляции. Данной теме посвящена глава 17, "Метапрограммы". Возникает вполне резонный вопрос: почему частично специализировать можно только шаблоны классов? Причины этого в основном исторические. Можно определить такой же механизм и для шаблонов функций (см. главу 13, "Направления дальнейшего развития"). В ряде аспектов тот же эффект достигается путем перегрузки шаблонов функций, но здесь есть и некоторые тонкие отличия, в основном связанные с тем, что при использовании этого механизма осуществляется поиск только первичного шаблона. Специализации рассматриваются впоследствии, для определения того, какая именно реализация должна использоваться. В отличие от этого все перегруженные шаблоны функций должны вноситься в набор перегрузки для выполнения поиска; при этом они могут находиться в различных пространствах имен или классах. Это несколько увеличивает вероятность непреднамеренной перегрузки имени шаблона. С другой стороны, можно представить возможность перегрузки шаблонов классов, например: // Некорректная перегрузка шаблонов класса template<typename Tl, typename T2> class Pair; template<int N1, int N2> class Pair; Однако насущной необходимости использования такого механизма не видно.
Глава 13 Направления дальнейшего развития Шаблоны языка C++ прошли значительный путь развития— от своего появления в 1988 году и до стандартизации языка C++ в 1998 году (техническая работа была завершена в ноябре 1997 года). Затем в течение нескольких лет определения языка оставались стабильными, но за это время появился ряд новых потребностей, связанных с шаблонами C++. Некоторые из них были вызваны желанием иметь менее противоречивый и более формальный язык. Почему, например, в шаблонах функций не разрешены аргументы шаблона, используемые по умолчанию, если они разрешены в шаблонах классов? Подсказкой для других расширений языка служит все возрастающая сложность программных идиом шаблонов, балансирующая на грани возможностей существующих компиляторов. Ниже описываются некоторые расширения, наиболее часто используемые разработчиками языка C++ и его компиляторов. Многие из этих расширений были подсказаны разработчиками различных библиотек языка C++ (включая стандартную). Нет никаких гарантий, что когда- нибудь они станут частью стандартного языка C++. С другой стороны, некоторые из них уже включены в определенные реализации языка C++ в качестве расширений. 13.1. Коррекция угловых скобок Для начинающих программировать шаблоны довольно часто неожиданностью оказывается то, что между двумя последовательными закрывающими угловыми скобками необходимо вставлять пробел. Например: #include <list> #include <vector> typedef std::vector<std::list<int> > LineTable; // ПРАВИЛЬНО typedef std::vector<std::list<int>> OtherTable; // ОШИБКА Второе объявление typedef содержит ошибку, так как две закрывающие угловые скобки без пробела между ними представляют собой операцию "сдвиг вправо" (>>), которая в данном месте исходного кода не имеет никакого смысла. Ситуация, когда компилятор обнаруживает данную ошибку и молча трактует операцию >> как две закрывающие угловые скобки (эту особенность иногда называют кор-
232 Глава 13. Направления дальнейшего развития рекцией угловых скобок), относительно проста по сравнению со многими другими особенностями синтаксических анализаторов исходного кода на C++. Действительно, многие компиляторы способны распознавать такие ситуации и принимают некорректный код, выдавая при этом только предупреждающее сообщение. Поэтому вполне вероятно, что в будущей версии языка C++ объявление для OtherTable (из предыдущего примера) будет считаться действительным. Тем не менее следует отметить, что существует ряд тонких нюансов, связанных с коррекцией угловых скобок. На самом деле встречаются ситуации, когда операция » является действительной лексемой в списке аргументов шаблона. Это иллюстрирует приведенный ниже пример. template<int N> class Buf; template<typename T> void strange () {} template<int N> void strange () {} int main() { strange<Buf<16»2> >(); // » не является ошибкой } В какой-то степени к рассматриваемой проблеме имеет отношение вопрос о случайном использовании диграфа <:, который эквивалентен квадратной скобке [ (см. раздел 9.3.1, стр. 152). Рассмотрим следующий фрагмент кода: template<typename T> class List; class Marker; List<::Marker>* markers; //ОШИБКА Последняя строка в данном примере трактуется как List [:Marker>* markers;, что вообще не имеет никакого смысла. Однако компилятор мог бы, вероятно, принять во внимание то, что за таким шаблоном, как List, не может следовать левая (открывающая) квадратная скобка, и в данном контексте не трактовать диграф <: как квадратную скобку. 13.2. Менее строгие правила использования ключевого слова typename Некоторые программисты и разработчики языка находят правила применения ключевого слова typename (см. разделы 5.1, стр. 65, и 9.3.2, стр. 154) слишком строгими. Например, в приведенном далее коде в typename Array<T>: : ElementT присутствие этого ключевого слова обязательно, а в typename Array<int>: : ElementT — запрещено. template <typename T> class Array { public: typedef T ElementT;
13.3. Аргументы шаблонов функций по умолчанию 233 }; template <typename T> void clear(typename Array<T>::ElementT& p); // ПРАВИЛЬНО templateo void clear(typename Array<int>::ElementT& p); // ОШИБКА Такие примеры, как этот, могут быть несколько неожиданными, а поскольку в реализации компилятора языка C++ нетрудно игнорировать лишнее ключевое слово, разработчики языка думают о том, чтобы допустить постановку ключевого слова typename перед любым полным именем типа, если только оно уже не дополнено одним из ключевых слов struct, class, union или enum. Кроме того, такое решение, вероятно, прояснило бы вопрос о том, когда допустимы конструкции .template, ->template и : : template (см. раздел 9.3.3, стр. 156). Игнорирование "паразитных" применений ключевых слов typename и template — это, с точки зрения разработчика компилятора, относительно простая задача. Интересно, что есть также ситуации, в которых по существующим правилам применять эти ключевые слова необходимо, хотя реализация языка могла бы обойтись без них. Например, компилятор мог бы понять, что в предыдущем шаблоне функции clear () имя Аггау<Т>:: ElementT не может быть ничем иным, кроме имени типа (в этом месте не допускаются никакие выражения), и поэтому в данной ситуации использование ключевого слова typename могло бы быть необязательным. Поэтому Комитет по стандартизации языка C++ рассматривает также вопрос о внесении в стандарт изменений, способных сократить число ситуаций, в которых необходимо применение ключевых слов typename и template. 13.3. Аргументы шаблонов функций по умолчанию Когда в язык C++ были впервые включены шаблоны, явное задание аргументов шаблонов функций отсутствовало. Аргументы шаблонов функций всегда должны были логически выводиться из выражения вызова. Поэтому казалось, что нет никаких причин вводить используемые по умолчанию аргументы шаблонов функций, так как они все равно перекрывались бы логически выводимыми значениями. Однако впоследствии стало возможно явно задавать те аргументы шаблонов функций, которые не могли быть выведены логическим путем. Следовательно, было бы вполне естественно задавать используемые по умолчанию значения для таких аргументов шаблонов, не выводимых логически. Рассмотрим приведенный ниже пример. template <typename Tl, typename T2 = int> T2 count(Tl constfc x); class Mylnt { };
234 Глава 13. Направления дальнейшего развития void test(Container constfc с) { int i = count(с); Mylnt = count<MyInt>(c); assert(Mylnt == i); } В данном примере ограничение заключается в том, что если какой-нибудь параметр шаблона имеет используемое по умолчанию значение аргумента, то все параметры, следующие за ним, также должны иметь используемые по умолчанию аргументы шаблона. Такое ограничение необходимо для шаблонов классов; в противном случае было бы невозможно задавать в общем виде последующие аргументы. Это иллюстрируется следующим ошибочным кодом: template <typename Tl = int, typename T2> class Bad; Bad<int>* b; // Данное int замещает Tl или Т2? Однако в шаблонах функций завершающие аргументы могут быть выведены логическим путем. Следовательно, нет никаких технических трудностей в том, чтобы переписать данный пример. template <typename Tl = int, typename T2> Tl count(T2 const& x); void test(Container const& c) { int i = count(c); Mylnt = count<MyInt>(c); assert(Mylnt == i); } Во время написания настоящей книги Комитет по стандартизации языка C++ рассматривал вопрос о расширении шаблонов функции в данном направлении. Задним числом программисты также отметили случаи, когда явное задание аргументов шаблонов не используется. template <typename T = double> void f(T const& = TO); int main() { f(l); // ПРАВИЛЬНО: выводится, что Т = int f<long>(2); // ПРАВИЛЬНО: Т = long; вывод не используется f<char>(); // ПРАВИЛЬНО: то же, что и f<char>('\0'); f(); // То же, что и f<double>(0.О); } Здесь аргументу шаблона по умолчанию позволено использовать аргумент функции по умолчанию без явного указания аргументов шаблона.
13.4. Строковые литералы и выражения с плавающей точкой в качестве аргументов... 235 13.4. Строковые литералы и выражения с плавающей точкой в качестве аргументов шаблонов Пожалуй, и для начинающих, и для опытных программистов, пишущих шаблоны, самое удивительное из всех ограничений на аргументы шаблонов, не являющиеся типами, состоит в том, что строковый литерал невозможно использовать в качестве аргумента шаблона. Приведенный ниже пример интуитивно понятен. template <char const* msg> class Diagnoser { public: void print(); }; int main() { Diagnoser<"Surprise!">().print(); } Однако здесь есть ряд потенциальных проблем. В стандартном языке C++ два экземпляра класса Diagnoser будут принадлежать одному и тому же типу тогда и только тогда, когда у них одни и те же аргументы. В данном случае аргумент является указателем, другими словами — адресом. Однако у двух идентичных строковых литералов, расположенных в разных местах исходного кода, не обязательно должен быть один и тот же адрес. Таким образом, можно оказаться в неловкой ситуации, когда классы Diag- noser<"Xl,> и Diagnoser<"X"> будут представлять два разных несовместимых типа! (Обратите внимание, что "X" имеет тип char const [2], но когда он передается в качестве аргумента шаблона, его тип сводится к char const *.) Исходя из этих (и других, связанных с ними) соображений, в стандартном языке C++ запрещено использовать строковые литералы в качестве аргументов шаблонов. Однако в некоторых реализациях такая возможность существует в виде расширения. Это обеспечивается использованием содержимого строковых литералов во внутреннем представлении экземпляра шаблона. Хотя это вполне осуществимо, некоторые толкователи языка C++ полагают, что не являющийся типом параметр шаблона, который может замещаться строковым литералом, должен объявляться иначе, чем параметр, который может замещаться адресом. Однако на момент написания книги синтаксис такого объявления, который получил бы широкую поддержку, отсутствовал. Кроме того, следует отметить, что в данном вопросе кроется один дополнительный технический недостаток. Рассмотрим следующие объявления шаблонов и предположим, нто язык расширен так, чтобы строковые литералы могли использоваться в качестве аргументов шаблонов.
236 Глава 13. Направления дальнейшего развития template <char const* str> class Bracket { public: static char const* address() const; static char const* bytes() const; }; template <char const* str> char const* Bracket<T>: :address () const { return str; } template <char const* str> char const* Bracket<T>::bytes() const { return str; } В этом коде две функции-члена идентичны во всем, кроме своих имен, — ситуация не такая уж и редкая. Предположим, что некоторая реализация инстанцирует Bracket<"X"> с помощью процесса, подобного макрорасширению: тогда, если эти две функции-члена инстанцируются в разных программных модулях, они могут возвращать разные значения. Интересно, что тестирование некоторых компиляторов языка C++, имеющих данное расширение, показало, что они обладают таким удивительным недостатком. С этим вопросом связан и вопрос о возможности использования литералов с плавающей точкой (и простых константных выражений с плавающей точкой) в качестве аргументов шаблонов. template <double Ratio> class Converter { public: static double convert(double val) const { return val*Ratio; } } ; typedef Converter<0.0254> InchToMeter; Эта возможность также имеется в некоторых реализациях языка C++, и она не представляет собой серьезной технической задачи (в отличие от использования строковых литералов в качестве аргументов).
13.5. Менее строгие правила соответствия для шаблонных параметров шаблона 237 13.5. Менее строгие правила соответствия для шаблонных параметров шаблона Шаблон, используемый для подстановки вместо шаблонного параметра шаблона, должен точно соответствовать списку параметров данного шаблонного параметра. Как видно из следующего примера, иногда это может приводить к удивительным последствиям. #include <list> // Содержит объявление: // namespace std { // template <typename T, // typename Allocator = allocator<T> > // class list; // } template< typename Tl, typename T2, template<typename> class Containers // Ожидается, что Container - шаблон // только с одним параметром class Relation { public: private: Container<Tl> doml; Container<T2> dom2; }; int main() { Relation<int, double, std::list> rel; // ОШИБКА: std::list имеет более // одного параметра шаблона } Эта программа ошибочна, поскольку в качестве параметра Container ожидается шаблон с одним параметром, тогда как std:: list содержит параметр allocator в дополнение к параметру, определяющему тип элемента. Однако, поскольку std: :list имеет Для параметра allocator значение по умолчанию, можно было бы установить, что Container соответствует std:: list и что каждое инстанцирование класса Container использует аргумент шаблона по умолчанию из std: : list (см. раздел 8.3.4, стр. 135). Аргументом в пользу необходимости сохранять статус-кво (т.е. несоответствие) является то, что для определения соответствия типов функций применяется такое же правило. Однако в этом случае используемые по умолчанию аргументы не всегда могут быть определены, так как значение указателя на функцию обычно не устанавливается до вре-
238 Глава 13. Направления дальнейшего развития мени выполнения. В противоположность этому "указателей на шаблоны" не существует и вся необходимая информация доступна во время компиляции. В некоторых компиляторах языка C++ менее строгое правило соответствия уже реализовано в виде расширения. Этот вопрос связан также с вопросом шаблонных typedef (они рассматриваются в следующем разделе). Действительно, попробуем заменить определение функции main () из предыдущего примера следующим: template <typename T> typedef list<T> MyList; int main() { Relation<int, double, MyList> rel; } Конструкция typedef вводит новый шаблон, который теперь точно соответствует классу Container в плане списка параметров. Как трактовать данный пример: в пользу менее строгого правила соответствия или против него — это, конечно, вопрос спорный. Данный вопрос поднимался перед Комитетом по стандартизации языка C++, но в настоящее время Комитет не склонен вводить менее строгое правило соответствия. 13.6. typedef-шаблоны Шаблоны классов часто комбинируются довольно сложными способами для получения других параметризованных типов. Когда такие параметризованные типы часто повторяются в исходном коде, возникает естественное желание ввести для них сокращенную запись, так же как конструкции typedef дают сокращенную запись синонимов не- параметризованных типов. Поэтому разработчики языка C++ думают о введении конструкции, которая могла бы выглядеть следующим образом: template <typename T> typedef vector<list<T> > Table; После данного объявления Table будет новым шаблоном, который может быть ин- станцирован и стать определением конкретного типа. Такой шаблон называется typedef- шаблоном (в отличие от шаблона класса или шаблона функции). Например: Table<int> t; // t имеет тип vector<list<int> > В настоящее время отсутствие typedef-шаблонов обходят путем использования конструкций typedef, являющихся членами шаблонов классов. В нашем примере можно было бы сделать так: template <typename T> class Table { public:
13.7. Частичная специализация шаблонов функций 239 typedef vector<list<T> > Type; }; Table<int>::Type t; //t имеет тип vector<list<int> > Поскольку шаблоны typedef являются полноценными, они могут специализироваться во многом подобно шаблонам классов. // Первичный шаблон typedef: template<typename T> typedef T Opaque; // Частичная специализация: tempiate<typename T> typedef void* Opaque<T*>; // Полная специализация: templateo typedef bool Opaque<void>; Шаблоны typedef не так уж просты. Например, непонятно, как бы они могли участвовать в процессе вывода. void candidate(long); template<typename T> typedef T DT; template<typename T> void candidate(DT<T>); int main() { candidate(42); // Какую именно функцию // candidate() следует вызвать? } Неясно, будет ли успешным вывод в данном случае. Безусловно, невозможен вывод с конструкциями typedef произвольной структуры. 13.7. Частичная специализация шаблонов функций В главе 12, "Специализация и перегрузка", отмечалось, что шаблоны классов можно частично специализировать, тогда как шаблоны функций просто перегружают. Эти два механизма различаются между собой. ' При частичной специализации не создается полностью новый шаблон: это просто Расширение существующего (первичного) шаблона. Когда выбирается шаблон класса, сначала рассматриваются только первичные шаблоны. Если после выбора первичного Шаблона оказывается, что имеется его частичная специализация с аргументами шаблона, соответствующими данному инстанцированию, его определение (или, другими словами, его тело) инстанцируется вместо определения первичного шаблона (при полной специализации шаблона все выполняется точно так же).
240 Глава 13. Направления дальнейшего развития Перегруженные шаблоны функций, напротив, являются отдельными шаблонами, полностью независимыми друг от друга. Когда компилятор решает, какой именно шаблон инстанцировать, он рассматривает все перегруженные шаблоны и выбирает наиболее подходящий. На первый взгляд такой подход кажется вполне адекватным, однако на практике существует ряд ограничений. • Можно специализировать шаблоны-члены класса без изменения определения этого класса. Однако добавление перегруженного члена требует изменения в определении класса. Во многих случаях этот вариант невозможен, так как у программиста может не быть на это прав. Более того, существующий стандарт языка C++ не позволяет программистам добавлять новые шаблоны в пространство имен std, но позволяет специализировать шаблоны из этого пространства имен. • Чтобы перегрузка шаблонов функций была возможна, параметры этих функций должны различаться каким-то существенным образом. Возьмем шаблон функции R convert (T const&), где R и Т— параметры шаблона. Этот шаблон вполне можно специализировать для R = void, но с помощью перегрузки этого сделать нельзя. • Код, который корректен для неперегруженной функции, может перестать быть корректным, когда эта функция перегружается. В частности, если есть два шаблона функций f (Т) и g (Т) (где Т — параметр шаблона), выражение g (&f <int>) корректно только в случае, если функция f не перегружена (в противном случае будет невозможно решить, какая именно функция f имеется в виду). • Дружественные объявления касаются определенного шаблона функции или ин- станцирования определенного шаблона функции. Перегруженная версия шаблона функции не будет автоматически иметь привилегии, которыми обладает исходный шаблон. В целом этот список представляет собой убедительный аргумент в пользу частичной специализации шаблонов функций. Естественной формой записи частичной специализации шаблонов функций является обобщение такой записи для шаблона класса. template <typename T> Т const& max(T const&, T constfc); // Первичный шаблон template <typename T> о T*const& max<T*>(T*const&,T*const&); // Частичная специализация Некоторых разработчиков языка беспокоит взаимодействие такого подхода к частичной специализации и перегрузки шаблонов функций. Например: template <typename- T> void add(T& x, int i); // Первичный шаблон template <typename Tl, typename T2>
13.8. Оператор typeof 241 void add(Tl a, T2 b); // Перегруженный первичный шаблон template <typename T> void add<T*>(T*&, int); // Специализация какого первичного шаблона? Однако мы полагаем, что такие ошибки не оказывают значительного влияния на полезность данной функциональной возможности. На момент написания книги это расширение находилось на рассмотрении Комитета по стандартизации языка C++. 13.8. Оператор typeof При написании шаблонов часто полезно иметь возможность указать тип выражения, зависящего от шаблона. Наглядным примером такой ситуации является объявление арифметической операции для шаблона числового массива, в котором типы операндов различны. Следующий пример должен прояснить данную мысль: template <typename Tl, typename T2> Array<???> operator+(Array<Tl>const& x, Array<T2>const& y); Предположительно эта операция должна создать массив элементов, которые являются результатом сложения соответствующих элементов массивов х и у. Таким образом, результирующий элемент будет иметь тип х [0] +у [0]. К сожалению, в языке C++ отсутствует надежный способ выражения этого типа с помощью Т1 и Т2. В качестве расширения, направленного на решение этого вопроса, в некоторых компиляторах имеется операция typeof. Она напоминает операцию sizeof тем, что позволяет получить из исходного выражения некоторый объект времени компиляции, но в данном случае этот объект может выступать в качестве имени типа. Тогда предыдущий пример можно записать следующим образом: template <typename Tl, typename T2> Array<typeof(Tl()+T2())> operator + (Array<Tl> const& x, Array<T2> const& y); Очень даже неплохо, но не идеально. Действительно, здесь предполагается, что данные типы могут быть инициализированы по умолчанию. Это можно обойти, вводя вспомогательный шаблон. template <typename T> Т makeTO; // Определение не требуется template <typename Tl, typename T2> Array<typeof(makeT<Tl>()+makeT<T2>() ) > operator + (Array<Tl> const& x, Array<T2> const& y);
242 Глава 13. Направления дальнейшего развития В аргументе typeof мы бы предпочли использовать х и у, но не можем этого сделать, так как они не были объявлены в точке расположения конструкции typeof. Радикальное решение этой проблемы заключается в том, чтобы ввести альтернативный синтаксис объявления функции, в котором возвращаемый тип помещается после параметров. // Шаблон функции оператора: template <typename Tl, typename T2> operator + (Array<Tl> constfc x, Array<T2> constfc y) -> Array< typeo f(x+y)>; // Шаблон регулярной функции: template <typename Tl, typename T2> function exp(Array<Tl> const& x, Array<T2> const& y) -> Array<typeof(exp(x,y))> Как видно из этого примера, новый синтаксис для неоператорных функций включает новое ключевое слово, в данном случае— function (чтобы выполнить процесс синтаксического анализа для операторных функций, достаточно ключевого слова operator). Обратите внимание, что операция typeof должна быть операцией времени компиляции. В частности, как видно из следующего примера, операция typeof не принимает во внимание ковариантные возвращаемые типы. class Base { public: virtual Base clone(); }; class Derived : public Base { public: virtual Derived clone(); // Ковариантный возвращаемый тип }; void demo (Base* p, Base* q) { typeof(p->clone()) tmp = p->clone(); ( // tmp всегда будет иметь тип Base } В разделе 15.2.4, стр. 298, показано, как иногда используются классы свойств, чтобы частично компенсировать отсутствие операции typeof. 13.9. Именованные аргументы шаблонов В разделе 16.1, стр. 311, описывается методика, позволяющая предоставлять для определенного параметра аргумент шаблона, не используемый по умолчанию, без необходимости задавать другие аргументы шаблона, для которых используется значение по
13.10. Статические свойства 243 умолчанию. Это интересная методика, но ясно, что она требует значительного объема работы для достижения относительно простого результата. Поэтому вполне естественным представляется создание механизма для именования аргументов шаблонов. Здесь следует отметать, что в процессе стандартизации языка C++ подобное расширение (иногда назьюаемое ключевыми аргументами) предлагалось ранее Роландом Хартингером (Roland Hartinger) (см. раздел 6.5.1 в [34]). Это предложение, хотя и технически вполне обос- новайное, в конечном итоге не было принято по ряду причин. В настоящее время нет никаких оснований полагать, что именованные аргументы шаблонов когда-нибудь попадут в язык. Однако для полноты картины упомянем об одной синтаксической идее, которая бродила в умах некоторых разработчиков. template<typename Т, Move: typename M = defaultMove<T>, Copy: typename С = defaultCopy<T>, Swap: typename S = defaultSwap<T>, Init: typename I = defaultInit<T>, Kill: typename К = defaultKill<T> > class Mutator { }; void test(MatrixList ml) { mySort(ml, Mutator<Matrix, Swap: matrixSwap>) ; } Обратите внимание, как имя аргумента (стоит перед двоеточием) отличается от имени параметра. Это позволяет сохранять практику использования коротких имен параметров, применяемых в данной реализации, и в то же самое время иметь самодокументируемые имена аргументов. Поскольку для некоторых стилей программирования такой подход может быть слишком многословным, можно представить себе также возможность опускать имя аргумента, если оно идентично имени параметра. template<typename T, : typename Move = defaultMove<T>, : typename Copy = defaultCopy<T>/ : typename Swap = defaultSwap<T>, : typename Init = defaultInit<T>/ : typename Kill = defaultKill<T> > class Mutator { } ; 13.10. Статические свойства В главах 15, "Классы свойств и стратегий", и 19, "Классификация типов", рассматриваются различные способы классификации типов во время компиляции. Такие характе-
244 Глава 13. Направления дальнейшего развития ристики полезны при выборе специализаций шаблонов на основе статических свойств типа. (Например, обратите внимание на наш класс CSMtraits в разделе 15.3.2, стр. 305, где делаются црпытки выбора оптимальных или почти оптимальных методов копирования, обмена или пересылки элементов типа аргумента.) Некоторые разработчики языка отмечали, что, если такие "выборы специализации" станут обычным делом, они не должны требовать сложного, определяемого пользователем кода тогда, когда все дело лишь в поиске некоторого свойства, которое и так известно компилятору. Вместо этого в языке должен быть ряд встроенных признаков типа. При наличии такого расширения приведенный ниже код мог быть действительной законченной программой на C++. #include <iostream> int main() { std::cout « std::type<int>::is_bit_copyable « '\n' ; std::cout « std::type<int>::is_union « '\n'; } Хотя для такой конструкции можно разработать собственный синтаксис, его подгонка под синтаксис, который может определяться пользователем, могла бы обеспечить более плавный переход от существующего языка к языку, который включал бы такие (функциональные) возможности. Однако некоторые из статических свойств, которые можно легко обеспечить в компиляторе языка C++, могут быть не реализуемы с помощью традиционных методов классов свойств (например, определения того, является ли некоторый тип объединением). Это аргумент в пользу включения данного элемента в язык. Другой аргумент состоит в том, что такое нововведение может значительно сократить количество памяти и машинного времени, необходимых компилятору для трансляции программ, в которых используются такие свойства. 13.11. Пользовательская диагностика инстанцирования Во многих шаблонах к параметрам неявно предъявляются определенные требования. Когда аргументы инстанцирования такого шаблона не отвечают этим требованиям, либо выдается сообщение об ошибке общего характера, либо созданный экземпляр функционирует неправильно. В первых компиляторах языка C++ сообщения об ошибках общего характера, выдаваемые во время инстанцирования шаблона, зачастую были слишком неясными (за примером обратитесь к разделу 6.6.1, стр. 98). В более поздних компиляторах сообщения об ошибках уже достаточно ясны опытным программистам для того, чтобы быстро найти причину ошибки, но желательно еще больше улучшить ситуацию. Рассмотрим приведенный ниже искусственный пример (его цель — проиллюстрировать, что происходит в реальных библиотеках шаблонов).
13.11. Пользовательская диагностика инстанцирования 245 template <typename T> void clear(T const& p) { *Р = 0; // Предполагается, что Т — это тип указателя } template <typename T> void core(T const& p) { clear(p); } template <typename T> void middle(typename T::Index p) { core(p); } template <typename T> void shell(T const& env) { typename T::Index i; middle<T>(i); } class Client { public: typedef int Index; }; Client main_client; int main() { shell (main_client) ; } Данный пример иллюстрирует типичное расслоение разрабатываемого программного обеспечения на несколько уровней: шаблон функции высокого уровня shell () зависит от такого компонента, как middle (), который, в свою очередь, использует функцию низшего уровня core (). Когда инстанцируется функция shell (), должны инстанци- роваться и все уровни ниже нее. В данном примере ошибка обнаруживается на самом глубоком (низшем) уровне: функция core () инстанцируется с типом int (в результате использования Client: : Index из функции middle ()), и делается попытка разыменования значения этого типа, что является ошибкой. Правильное сообщение об ошибке
246 Глава 13. Направления дальнейшего развития общего характера включает трассировку всех уровней, ведущих к ошибке; однако эта информация может оказаться весьма громоздкой. Часто предлагалась такая альтернатива: вставка в шаблон самого высокого уровня устройства, которое запрещает инстанцирование более низких уровней, если известные требования на этих уровнях не удовлетворяются. Делались разные попытки реализации таких устройств в рамках существующих конструкций языка C++ (например, [3]), но они не всегда эффективны. Поэтому нет ничего удивительного в том, что до данному вопросу были предложены расширения языка. Ясно, что такое расширение можно создавать поверх функциональных средств использования статических свойств, рассмотренных выше. Например, можно представить себе следующую-модификацию шаблона функции shell (): template <typename T> void shell(T const& env) { std::instantiation_error( std::type<T>::has_member_type<"Index">, "T must have an Index member type"); std::instantiation_error( !std: :type<typename T: :Index>: -.dereferencable, "T::Index must be a pointer-like type"); typename T::Index i; middled) ; } Псевдофункция instantiation__error() должна приводить к тому, что компилятор прервет инстанцирование (следовательно, не будет сообщений об ошибках, вызываемых конкретизацией функции middle ()) и выдаст данное сообщение. Этот подход, хотя и вполне осуществимый, имеет ряд недостатков. Например, при таком описании всех свойств некоторого типа код может быстро стать громоздким. Некоторые предлагали разрешить "фиктивный код", который бы служил условием, прерывающим инстанцирование. Приведем один из множества предложенных вариантов (он не вводит новых ключевых слов). template <typename T> void shell (T const& env) { template try { typename T::Index p; *p = 0; } catch "T::Index must be a pointer-like type"; typename T::Index i; middled) ; } Идея заключается в том, что тело оператора template try инстанцируется для проверки, без реальной генерации объектного кода и, если происходит ошибка, выдается последующее сообщение об этой ошибке. К сожалению, такой механизм трудно реализовать.
13.12. Перегруженные шаблоны классов 247 Связано это с тем, что, хотя генерацию кода можно подавить, остаются побочные эффекты, присущие внутренней природе компилятора, избежать влияния которых крайне сложно. Другими словами, эта не очень значительная функциональная возможность, по-видимому, потребовала бы значительной модернизации существующей технологии компиляции. Большинство подобных схем имеют и другие недостатки. Например, многие компиляторы языка C++ могут выводить сообщения об ошибках на разных языках (английском, немецком, японском и т.д.), но переводы на разные языки в исходном коде могли бы оказаться излишними. Более того, если будет прерван процесс настоящего инстанцирования, а предусловие не сформулировано точно, программист может оказаться в гораздо худшей ситуации, чем при получении сообщения об ошибке общего характера (пусть даже и громоздкого). 13.12. Перегруженные шаблоны классов Вполне можно представить, что существует возможность перегружать параметры шаблонов классов. Например, можно представить следующий вариант: template <typename Tl> class Tuple { // Одноэлементный кортеж }; template <typename Tl, typename T2> class Tuple { // Двухэлементный кортеж }; template <typename Tl, typename T2, typename T3> class Tuple { // Трехэлементный кортеж } ; В следующем разделе рассматривается применение такой перегрузки. Эта перегрузка не сводится только к тому, что меняется число параметров шаблонов (такую перегрузку можно эмулировать с помощью частичной специализации, как это сделано в главе 22, "Объекты-функции и обратные вызовы", для FunctionPtr). Может меняться также и вид параметров: template <typename Tl, typename T2> class Pair { // Два поля } ; template <int II, int I2>
248 Глава 13. Направления дальнейшего развития class Pair { // Две целочисленные константы }; Неофициально эта идея обсуждалась некоторыми разработчиками языка, однако официально она пока еще не была представлена на рассмотрение Комитета по стандартизации языка C++. 13.13. Параметры-списки Иногда возникает необходимость передавать список типов как один аргумент шаблона. Обычно этот список преследует одну из двух целей: объявление функции с параметризованным числом параметров или определение структуры типов с параметризованным списком членов. Например, может потребоваться определить шаблон функции, которая находит наибольшее число из произвольного списка чисел. В возможном синтаксисе такого объявления используется лексема "многоточие", обозначающая, что последний параметр шаблона соответствует произвольному числу аргументов. #include <iostream> template <typename T, ... list> T const& max(T constfc, T constfc, list const&) ; int main() { std::cout « max(l, 2, 3, 4) « std::endl; } Возможны разные способы реализации такого шаблона. Вот один, не требующий новых ключевых слов, но требующий добавления нового правила, которое состоит в том, что при перегрузке шаблонов функций предпочтение отдается шаблону функции без параметра-списка. template <typename T> inline Т const& max (T constfc a, T constfc b) { // Обычный максимум двух чисел: return a < b ? b : a; } template <typename Т, ... list> inline T const& max(T const& a, T constfc b, list const& x) { return max(a, max(b,x)); }
13.13. Параметры-списки 249 Давайте рассмотрим по шагам, как выполняется эта работа для случая, когда вызывается функция max (1,2,3,4). Поскольку здесь четыре аргумента, она не соответствует бинарной функции max (), но соответствует второй функции с Т = int и list = int, int. Следовательно, мы вынуждены вызывать шаблон двоичной функции шах () с первым аргументом, равным 1, и вторым аргументом, равным результату вьшолнения функции шах (2,3,4). Это опять не соответствует двоичной операции, и мы вызываем версию с параметром-списком, где Т = int и list = int. На этот раз подвыражение max (b, х) переходит в max (3,4) и рекурсия заканчивается выбором бинарного шаблона. Благодаря возможности перегружать шаблоны функций, этот метод работает довольно хорошо. Конечно, наше обсуждение на охватывает вопрос полностью. Например, следовало бы точно определить, что в данном контексте означает параметр list const&. Иногда желательно ссылаться на отдельные элементы или подмножества списка. Например, для этого можно было бы использовать индексные скобки. Следующий пример демонстрирует, как с помощью этой методики можно было бы построить метапрограмму для подсчета числа элементов в списке. template <typename T> class ListProps { public: enum { length = 1 } ; }; template <... list> class ListProps { public: enum { length = l+ListProps<list[1 ...]>::length }; }; Отсюда видно, что параметры-списки могут быть также полезными для шаблонов классов и их можно было бы соединить с рассмотренным ранее понятием перегрузки класса в целях усовершенствования различных методик метапрограммирования шаблонов. В качестве альтернативы, параметр-список можно использовать для объявления списка полей. template <... list> class Collection { list; }; Удивительно, какое количество важных утилит можно создать на основе этого функционального средства. Чтобы у вас появились новые идеи, предлагаем прочитать книгу Modern C++ Design [1], где отсутствие такой функциональной возможности заменяется обширным метапрограммированием на базе шаблонов и макроопределений.
250 Глава 13. Направления дальнейшего развития 13.14. Управление размещением данных Трудная и довольно распространенная задача, связанная с программированием шаблонов, заключается в объявлении массива байтов, достаточно большого (но не более того), чтобы вмещать объект пока не известного типа Т; другими словами, являющегося параметром шаблона. Одно из применений такого объявления — так называемые размеченные объединения (называемые также вариантными типами или помеченными объединениями). template <... list> class D_Union { public: enum { n__bytes } ; char bytes [n__bytes] ; //в конечном счете будет // содержать элементы одного из типов, // описанных аргументами шаблона }; Константе n_bytes не всегда можно присваивать результат операции sizeof (T), так как для типа Т могут быть более строгие требования к выравниванию, чем для буфера bytes. Существуют различные, полученные опытным путем правила, позволяющие учесть это выравнивание; но они зачастую сложны или имеют излишне вольные исходные посылки. Что здесь действительно желательно, так это возможность определять требования к выравниванию некоторого типа в виде константного выражения и, напротив, возможность выполнять выравнивание типа, поля или переменной в соответствии с этими требованиями. Во многих компиляторах языков С и C++ уже есть операция alignof , которая возвращает требование к выравниванию выражения данного типа. Она почти идентична операции sizeof, за исключением того, что для данного типа вместо размера возвращает требование к его выравниванию. Для выполнения выравнивания какого- либо программного объекта во многих компиляторах уже имеются директивы #pragma или подобные им элементы. Возможным подходом является введение ключевого слова alignof, которое можно было бы использовать как в выражениях (для получения требования к выравниванию), так и в объявлениях (для выполнения выравнивания). template <typename T> class Alignment { public: enum { max = alignof(T) }; >; template <... list> class Alignment { public: enum { max = alignof( list[0] > Alignment<list[1 ...]>::max ? alignof(list[0])
13.15. Вывод на основе инициализатора 251 : Alignment<list[1 ...]>::max; } }; // Для определения типа самого большого размера в данном // списке типов можно аналогичным образом создать набор // шаблонов Size template <... list> class Variant { public: char buffer[Size<list>::max] alignof(Alignment<list>::max; }; 13.15. Вывод на основе инициализатора Часто говорят, что "программисты ленивы", и иногда это относится к нашему желанию иметь компактные записи программ. Рассмотрим с этой точки зрения следующее объявление: std::map<std::string, std::list<int> >* diet = new std::map<std::string, std::list<int> >; Оно многословно и на практике мы, вероятно (а скорее всего, наверняка), ввели бы для данного типа синоним с использованием конструкции typedef. Однако в этом объявлении есть что-то лишнее: мы определяем тип diet, но он все равно неявно присутствует в типе инициализатора. Разве не элегантнее было бы записать эквивалентное объявление только с одним определением типа? Например: del diet = new std::map<std::string, std::list<int> >; В этом последнем объявлении тип переменной выводится из типа ее инициализатора. Чтобы это объявление отличалось от обычной операции присвоения, необходимо ключевое слово (в данном случае del, но предлагались также var, let и даже auto). Этот вопрос касается не только шаблонов. Оказывается, что в действительности такая конструкция была принята в одной из самых первых версий компилятора Cfront (в 1982 году, до того как появились шаблоны). Однако именно многословность объявлений многих типов в шаблонах делает потребность в этой функциональной возможности более настоятельной. Можно также представить частичный вывод, при котором логически должны выводиться только аргументы шаблона. std::list<> index = create_index(); Другой вариант состоит в том, чтобы логически выводить аргументы шаблона из аргументов конструктора. template <typename T> class Complex { public:
252 Глава 13. Направления дальнейшего развития Complex(T constfc re, T const& im); }; Complexo z(1.0, 3.0); //здесь выводится, что Т = double Точные спецификации (определения) для этого вида вывода усложняются тем, что возможны перегруженные конструкторы и шаблоны конструкторов. Предположим, например, что наш шаблон Complex содержит шаблон конструктора в дополнение к обычному конструктору копирования. template <typename T> class Complex { public: Complex(Complex<T> const&); template <typename T2> Complex(Complex<T2> constfc); }; Complex<double> j(0.0, 1.0); Complexo z = j; //Какой конструктор имеется в виду? Вероятно, в последней инициализации имелся в виду стандартный конструктор копирования; следовательно, переменная z должна иметь тот же тип, что и j. Однако делать из этого неявное правило игнорирования шаблонов конструкторов было бы чересчур смело. 13.16. Функциональные выражения В главе 22, "Объекты-функции и обратные вызовы", проиллюстрирован тот факт, что часто бывает удобно передавать небольшие функции (или функторы) в другие функции в качестве параметров. В главе 17, "Метапрограммы", также упоминается, что методики создания шаблонов выражений можно использовать для построения небольших функторов в кратком виде, без явных объявлений (см. раздел 18.3, стр. 364). Например, может потребоваться вызывать особую функцию-член для каждого элемента стандартного вектора, чтобы его инициализировать. class BigValue { public: void init(); }; class Init { public: void operator() (BigValuefc v) const { v.init(); }
13.16. Функциональные выражения 253 }; void compute (std::vector<BegValue>& vec) { std::for_each(vec.begin(), vec.end(), Init()) ; } Необходимость определять для этого отдельный класс Init ведет к громоздкому коду. Вместо этого можно представить себе возможность записывать (неименованные) тела функций как часть некоторого выражения. class BigValue { public: void init(); }; void compute (std::vector<BigValue>& vec) { std::for_each(vec.begin(), vec.end(), $(BigValue&) { $l.init(); }); } Идея заключается в том, чтобы ввести функциональное выражение со специальной лексемой $, за которой следуют типы параметров в круглых скобках и тело выражения в фигурных скобках. Внутри такой конструкции к параметрам можно обращаться посредством специальной записи $п, где п — это константа, указывающая номер параметра. Эта форма записи похожа на так называемые лямбда-выражения (или лямбда- функции) и замыкания из других языков программирования. Однако возможны и другие решения. Например, одним из решений могло бы быть использование анонимных внутренних классов, которые можно увидеть в языке Java. class BigValue { public: void init(); } ; void compute (std::vector<BigValue>& vec) { std::for_each(vec.begin(), vec.end(), class { public: void operator() (BigValue& v) const { v.init(); }
254 Глава 13. Направления дальнейшего развития ); } Хотя конструкции такого рода регулярно появляются в среде разработчиков языка, конкретных предложений очень мало. Вероятно, это следствие того, что проектирование такого расширения — задача гораздо более сложная, чем можно предположить на основании наших примеров. Среди вопросов, которые должны быть решены, вопросы о спецификации возвращаемого типа и правилах, определяющих, какие программные элементы доступны в теле функционального выражения. Например, разрешен ли доступ к локальным переменным в окружающих функциях? Можно было бы также представить, что функциональные выражения — это шаблоны, в которых типы параметров логически выводятся из конкретного применения функционального выражения. Такой подход может сделать предыдущий пример еще более кратким (позволяя совсем опустить список параметров), но он ставит и новые задачи, касающиеся системы логического вывода аргументов шаблона. Совершенно неясно, будет ли когда-нибудь в язык C++ включено такое понятие, как функциональное выражение. Однако библиотека Lambda Library авторов Яакко Ярви (Jaakko 1дга) и Гэри Пауэлла (Gary Powell) [20] является большим шагом на пути обеспечения желаемых функциональных возможностей, хотя и за счет значительных ресурсов компилятора. 13.17. Заключение Сейчас, когда компиляторы языка C++ только становятся совместимыми в своей основе со стандартом 1998 года (С++98), пожалуй, преждевременно говорить о дальнейшем расширении языка. Однако частично именно благодаря тому, что эта совместимость постепенно достигается, мы (сообщество программистов, пишущих на C++) приобретаем способность ясно понимать истинные недостатки языка C++ (и шаблонов в частности). Идя навстречу новым потребностям программистов, пишущих на C++, Комитет по стандартам языка C++ (известный как ISO WG21/ANSIJ16 или просто WG21/J16) начал исследовать пути к новому стандарту: С++0х. После того как этот стандарт был предварительно представлен на заседании в Копенгагене в апреле 2001 года, комитет WG21/J16 начал рассматривать конкретные предложения по расширению библиотеки. В действительности намерение комитета состоит в том, чтобы попытаться, насколько это возможно, ограничить расширения стандартной библиотеки языка C++. Однако ясно, что некоторые из этих расширений потребуют изменений в самом ядре языка. Мы ожидаем, что многие из этих необходимых модификаций будут иметь отношение к шаблонам языка C++, точно так же, как введение стандартной библиотеки шаблонов (STL) в стандартную библиотеку языка C++ стимулировало развитие шаблонов в 1990-х годах. Наконец, ожидается, что в стандарте С++0х будет уделено внимание некоторым "затруднительным моментам", присущим стандарту С++98. Есть надежда, что это повысит доступность языка C++. В настоящей главе рассматривались некоторые расширения в этом направлении.
Часть III Шаблоны и конструирование Программы обычно создаются с использованием конструкций, которые довольно хорошо отражают возможности механизмов, предлагаемых выбранным языком программирования. Поскольку шаблоны являются совершенно новым механизмом языка, неудивительно, если они будут вызывать к жизни новые элементы дизайна. Данная часть книги посвящена изучению именно этих элементов. Шаблоны отличаются от более традиционных конструкций языка тем, что позволяют параметризовать типы и константы нашего кода. Их комбинирование с частичной специализацией и рекурсивным инстанцированием приводит к необычайному увеличению выразительности и силы языка, что будет проиллюстрировано в следующих главах с помощью большого числа методов дизайна, в частности: • обобщенного программирования; • классов свойств; • классов стратегий; • метапрограммирования; • шаблонов выражений. При этом ставилась цель не только перечислить различные известные элементы конструирования, но и показать принципы, которые лежат в основе новых методов.
Глава 14 Полиморфные возможности шаблонов Полиморфизм представляет собой способность связывать различные специфические виды поведения с помощью единой общей записи1. Кроме того, полиморфизм является краеугольным камнем парадигмы объектно-ориентированного программирования, которая в C++ поддерживается главным образом через наследование свойств классов и виртуальные функции. Поскольку этот механизм (по крайней мере частично) работает во время выполнения программы, можно употребить термин динамический полиморфизм. Обычно так говорят, когда речь идет об обычном полиморфизме в C++. Однако шаблоны также позволяют связывать различные специфические виды поведения единой общей записью, но это связывание обрабатывается, как правило, в процессе компиляции, так что в этом случае следует говорить о статическом полиморфизме. В данной главе приводится обзор обоих вариантов полиморфизма и обсуждается вопрос о том, какой из них соответствует той или иной конкретной ситуации. 14.L Динамический полиморфизм Исторически сложилось так, что язык C++ начался с поддержки полиморфизма толь- ко посредством наследования, объединенного с виртуальными функциями . В этом контексте искусство полиморфного дизайна состоит в идентификации общего набора возможностей среди связанных типов объектов и объявлении их в качестве интерфейсов виртуальных функций в общем базовом классе. Наглядным примером этого подхода к конструированию является приложение, которое управляет построением геометрических фигур с возможностью их воспроизведения определенным способом (например, на экране). В таком приложении можно указать так Полиморфизм буквально означает условие существования нескольких форм или видов (от греческого polymorphos). Строго говоря, макросы также могут рассматриваться, как ранняя форма статического полиморфизма. Однако они остаются за пределами данного обсуждения, поскольку обычно никак не связаны с другими механизмами языка.
258 Глава 14. Полиморфные возможности шаблонов определенным способом (например, на экране). В таком приложении можно указать так называемый абстрактный базовый класс (abstract base class — ABC) GeoOb j, который объявляет общие операции и свойства, применимые к геометрическим объектам вообще. Каждый конкретный класс для конкретных геометрических объектов будет затем порождаться из абстрактного базового класса GeoOb j (рис. 14.1). Рис. 14.1. Полиморфизм, реализованный через наследование // poly/dynahier.hpp #include "coord.hpp" // Общий абстрактный базовый класс GeoObj // для геометрических объектов class GeoObj { public: // Черчение геометрического объекта virtual void draw() const = 0; // Координаты центра тяжести геометрического объекта virtual Coord center_of_gravity() const = 0; }; // Конкретный класс геометрического объекта Circle // - производный от базового класса GeoObj class Circle : public GeoObj { public: virtual void draw() const; virtual Coord center_of_gravity() const; }; // Конкретный класс геометрического объекта Line // - производный от базового класса GeoObj class Line : public GeoObj { public: virtual void draw() const; virtual Coord center_of_gravity() const;
14.1. Динамический полиморфизм 259 }; После создания конкретных объектов код пользователя может управлять этими объектами через ссылки или указатели на базовый класс, который дает возможность задействовать механизм диспетчеризации виртуальных функций. В результате вызова виртуальной функции-члена посредством указателя или ссылки на подобъект базового класса происходит вызов соответствующего члена объекта, на который осуществлялась ссылка. В нашем примере конкретный код может выглядеть, как показано ниже. // poly/dynapoly.cpp #include "dynahier.hpp" #include <vector> // Черчение любого объекта GeoObj void myDraw(GeoObj constfc obj) { // Вызов draw() в соответствии с типом объекта ob j . draw () ; } // Расстояние между центрами тяжести двух объектов GeoObj Coord distance(GeoObj const& xl, GeoObj constfc x2) { Coord с = xl.center__of_gravity() - x2.center_of„gravity(); return c.abs(); // Возврат абсолютного значения } // Черчение неоднородного набора объектов GeoObj void drawElems(std::vector<GeoObj*> constfc elems) { for (unsigned i = 0; i < elems.size(); ++i) { // Вызов draw() в соответствии с типом элемента elems[i]->draw(); } } int main() { Line 1; Circle c, cl, c2; myDraw(l); // myDraw(GeoObj&) => Line::draw() myDraw(c); // myDraw(GeoObj&) => Circle::draw() distance(cl,c2); // distance(GeoObj&,GeoObj&) distance(1,c); // distance(GeoObj&,GeoObj&)
260 Глава 14. Полиморфные возможности шаблонов std::vector<GeoObj*> coll; // Неоднородный набор coll.push_back(&l); // Вставка линии coll.push_back(&c); // Вставка круга drawElems(coll); // Черчение набора объектов } Ключевыми элементами полиморфного интерфейса являются функции draw () и center_of_gravity(). Обе функции представляют собой виртуальные функции- члены. В нашем примере продемонстрировано их использование в функциях myDraw (), distance () и drawElems (). Последние три функции записаны с использованием общего базового типа GeoOb j. Вследствие этого в процессе компиляции нельзя определить, какая именно версия — draw() или center_of_gravity () — должна использоваться. Однако в процессе выполнения программы при диспетчеризации вызовов функций определяется полный динамический тип объектов, для которых вызываются виртуальные функции. Следовательно, соответствующая операция выполняется в зависимости от фактического типа геометрического объекта: если myDraw () вызывается для объекта Line, то выражение obj . draw () вызывает функцию Line: : draw (), тогда как для объекта Circle вызывается функция Circle: : draw (). Подобным же образом в вызове distance () функции-члены center_of__gravity () соответствуют переданным в качестве параметров объектам. Очевидно, наиболее впечатляющей возможностью динамического полиморфизма является способность обрабатывать разнородные коллекции объектов. Эта концепция иллюстрируется функцией drawElems (); простое выражение elems[i]->draw() выполняет вызов самых разных функций-членов, в зависимости от типа итерируемого элемента. 14.2. Статический полиморфизм Шаблоны также могут использоваться для реализации полиморфизма. Однако они не зависят от фактора общего поведения, свойственного базовым классам. Вместо этого общность подразумевает поддержку операций с использованием общего синтаксиса (т.е. соответствующие функции имеют одни и те же имена). Конкретные классы при этом определяются независимо друг от друга (рис. 14.2), а сам полиморфизм проявляется при инстанцировании шаблонов с конкретными классами. Рис. 14.2. Полиморфизм, реализованный через шаблоны Circle Line Rectangle
14.2. Статический полиморфизм 261 Например, функцию myDraw () void myDraw(GeoObj constfc obj) // GeoObj - абстрактный базовый класс { obj.draw(); } можно переписать следующим образом: template <typename GeoObj> void myDraw(GeoObj constfc obj) // GeoObj - параметр шаблона { obj.draw(); } Сравнивая обе реализации функции myDraw (), можно видеть, что основное различие состоит в указании GeoObj в качестве параметра шаблона вместо общего базового класса. Имеются, однако, и более существенные различия. Например, при использовании динамического полиморфизма в процессе выполнения у нас была только одна функция myDraw (), тогда как, применяя шаблон, мы имеем различные функции, такие, как myDraw<Line> () и myDraw<Circle> (). Можно попытаться переписать весь пример из предыдущего раздела с использованием статического полиморфизма. При этом вместо иерархии геометрических классов у нас появится несколько индивидуальных геометрических классов. // poly/statichier.hpp #include "coord.hpp" // Конкретный класс Circle // - не порождается из какого-либо класса class Circle { public: void draw() const; Coord center_of_cjravity() const; }; // Конкретный класс Line // - не порождается из какого-либо класса class Line { public: void draw() const; Coord center_of_gravity() const; b
262 Глава 14. Полиморфные возможности шаблонов Применение этих классов теперь выглядит, как показано ниже. // poly/staticpoly.cpp #include "statichier.hpp1* #include <vector> // Черчение любого объекта GeoObj template <typename GeoObj> void myDraw(GeoObj const& obj) { obj.drawO; // Вызов draw() соответствующего объекта } // Расстояние между центрами тяжести двух объектов GeoObj template <typename GeoObjl/ typename GeoObj2> Coord distance(GeoObjl constfc xl, Geo0bj2 constfc x2) { Coord с = xl.center_of_gravitY() - x2.center_of_gravity(); return c.abs(); // Возврат абсолютного значения } // Черчение однородной коллекции объектов GeoObj template <typename GeoObj> void drawElems(std::vector<GeoObj> constfc elems) { for (unsigned i = 0; i < elems.size(); ++i) { // Вызов draw() в соответствии с типом элемента elems[i].draw(); } } int main() { Line 1; Circle c, cl# c2; myDraw(1) ; // myDraw<Line>(GeoObj &) => Line::draw() myDraw(c); // myDraw<Circle>(GeoObj&) => Circle::draw() distance(cl,c2); // distance<Circle,Circle>(GeoObj1&,GeoObj2&) distance(1,с);
14.3. Сравнение динамического и статического полиморфизма 263 // distance<Line,Circle>(GeoObj1&,GeoObj 2&) // std::vector<GeoObj*> coll; // ОШИБКА: неоднородная коллекция невозможна std::vector<Line> coll; // Однородная коллекция coll.push_back(l); // Вставка линии drawElems(coll); // Черчение всех линий } Тип GeoObj больше не может использоваться в качестве конкретного параметра типа как для функции distance (), так и в функции myDraw (). Вместо этого в функции distance () предусмотрены два параметра шаблона — GeoObj 1 и GeoObj 2. Два разных параметра шаблона позволяют вычислять расстояние между разными типами геометрических объектов: distanced,с); // distance<Line,Circle>(GeoObj1&,GeoObj2&) Теперь, однако, разнородные коллекции больше не могут обрабатываться явным образом. Это тот случай, когда статическая часть статического полиморфизма налагает свои ограничения, а именно: все типы должны быть определены в процессе компиляции. Взамен предоставляется возможность легко вводить разные коллекции для различных типов геометрических объектов, к тому же больше не требуется, чтобы коллекция была ограничена указателями, что дает существенные преимущества в аспекте производительности и безопасности типов. 14.3. Сравнение динамического и статического полиморфизма А теперь классифицируем и сравним обе формы полиморфизма. Терминология Динамический и статический полиморфизм обеспечивает поддержку различных идиом языка программирования C++3. • Полиморфизм, реализованный с использованием наследования, является ограниченным (bounded) и динамическим (dynamic). • Термин ограниченный означает, что интерфейсы типов, участвующих в процессе полиморфизма, предопределены дизайном общего базового класса {другими терминами для обозначения данной концепции являются инвазивный (invasive) или интрузивный (intrusive)). С терминологией, касающейся полиморфизма, более детально можно ознакомиться в [12], разделы 6.5-6.7.
264 Глава 14. Полиморфные возможности шаблонов • Термин динамический означает, что связывание интерфейсов происходит в процессе выполнения программы (т.е. динамически). • Полиморфизм, реализованный с использованием шаблонов, является неограниченным (unbounded) и статическим (static). • Термин неограниченный означает, что интерфейсы типов, участвующих в процессе полиморфизма, не предопределены заранее (другими терминами для обозначения данной концепции являются неинвазивный (noninvasive) или неинтрузивный (nonintrusive)). • Термин статический означает, что связывание интерфейсов происходит в процессе компиляции (т.е. статически). Строго говоря, в терминах языка C++ понятия динамический полиморфизм и статический полиморфизм — это сокращенные варианты понятий ограниченный динамический полиморфизм и неограниченный статический полиморфизм. В других языках используются иные комбинации (например, Smalltalk использует термин "неограниченный динамический полиморфизм"). Однако более краткие термины динамический полиморфизм и статический полиморфизм в контексте языка C++ не приводят к возникновению путаницы. Преимущества и недостатки Динамический полиморфизм в C++ обладает рядом преимуществ. • Элегантная обработка разнородных коллекций. • Размер исполняемого кода потенциально меньше (поскольку в данном случае нужна только одна полиморфная функция, тогда как для шаблонов с разными параметрами типов должны быть сгенерированы отдельные экземпляры). • Код полностью компилируем; таким образом, исходные тексты не обязательно должны быть опубликованы (распространение библиотек шаблонов обычно требует распространения исходного кода реализации шаблонов). Приведем преимущества статического полиморфизма в C++. • Легко реализуются коллекции встроенных типов. Общность интерфейса не обязательно должна выражаться через общий базовый класс. • Сгенерированный код выполняется потенциально быстрее (поскольку априори отсутствует необходимость в косвенном обращении через указатели, а невиртуальные функции могут быть встраиваемыми намного чаще). • Могут использоваться конкретные типы, в которых имеются только частичные интерфейсы (только если приложение ограничивается использованием этого частичного интерфейса). Часто статический полиморфизм расценивается как более надежный в плане безопасности типов, чем динамический, поскольку все связывания выполняются в процессе компиляции. Например, опасность того, что в контейнер, реализованный шаблоном, будет вставлен объект неправильного типа, крайне мала; в то же время в контейнере, кото-
14.4. Новые виды шаблонов проектирования 265 рый содержит указатели на общий базовый класс, существует возможность непреднамеренного использования указателей на объекты совершенно иного типа. На практике инстанцирование шаблонов может вызвать определенные неприятности в том случае, когда за идентично выглядящими интерфейсами скрываются разные семантические допущения. Например, неприятные сюрпризы могут произойти тогда, когда шаблон предполагает наличие ассоциативного оператора + у типа, который таким оператором не обладает. Обычно этот вид семантического несоответствия встречается гораздо реже в иерархиях, основанных на наследовании; вероятно, это связано с более явным и точным определением интерфейса. Объединение обеих форм Конечно, можно совместить обе формы наследования. Например, различные виды геометрических объектов можно порождать из общего базового класса, для того чтобы иметь возможность обрабатывать неоднородные коллекции геометрических объектов. Однако одновременно можно использовать и шаблоны в целях написания кода для некоторого отдельного вида геометрического объекта. Комбинация наследования и шаблонов описана в главе 16, "Шаблоны и наследование". В ней рассматривается (помимо прочего), как может быть параметризована виртуальность функции-члена и как можно предоставить дополнительную гибкость статическому полиморфизму, используя основанную на наследовании модель, необычного рекуррентного шаблона {curiously recurring template pattern — CRTP). 14.4. Новые виды шаблонов проектирования Следствием использования новой формы статического полиморфизма являются новые пути реализации шаблонов проектирования . Возьмем, например, шаблон bridge pattern, который играет большую роль в программах на C++. Одна из задач использования этого шаблона проектирования состоит в переключении между различными реализациями интерфейса. Согласно [13], обычно это переключение осуществляется с использованием указателя для обращения к действительной реализации и путем делегирования всех обращений к этому классу (рис. 14.3). Однако если тип реализации известен во время компиляции, то вместо этого можно использовать подход с применением шаблонов (рис. 14.4). Это приведет к большей безопасности типов и позволит избежать использования указателей, что должно способствовать более быстрому выполнению программы. Здесь и далее появляется определенная путаница, связанная с тем, что в русскоязычной литературе в качестве перевода терминов template и pattern принято одно слово — шаблон. Впрочем, из контекста должно быть понятно, идет ли речь о шаблонах C++ (C++ templates) или о шаблонах проектирования (design patterns). — Прим. ред.
266 Глава 14. Полиморфные возможности шаблонов Interface Implementation operatlonA() operationB() body Implementation operatlonA operationB operationC Г ± Implementation A operatlonA operationB operationC "L Implementation В operatlonA operationB operationC Puc. 14.3. Шаблон Bridge pattern, реализованный с использованием наследования -! impi ; Interface - 1 Impl operatlonA() operationB() Implementation A operatlonA operationB operationC Implementation В operatlonA operationB operationC Puc. 14.4. Bridge pattern, реализованный с использованием шаблонов 14.5. Обобщенное программирование Статический полиморфизм порождает концепцию обобщенного программирования (generic programming). Однако единого универсального установившегося определения этого понятия не существует (как не существует и единого установившегося определения понятия объектно-ориентированного программирования). Согласно [12], имеются определения от программирования с обобщенными параметрами (programming with generic parameters) до поиска наиболее абстрактного представления эффективных алгоритмов (finding the most abstract representation of efficient algorithms). Книга резюмирует: Обобщенное программирование— это поддисциплина информатики, которая имеет дело с поиском абстрактных представлений эффективных алгоритмов, структур данных и других понятий программного обеспечения, вместе с организацией их систематики.... Обобщенное программирование сосредоточивает внимание на представлении семейств концепций доменов (стр. 169-170). В контексте C++ обобщенное программирование иногда определяется как программирование с шаблонами (programming with templates), в то время как объектно- ориентированное программирование рассматривается, как программирование с вирту-
14.5. Обобщенное программирование 267 альными функциями {programming with virtual functions). В этом смысле почти любое использование шаблонов в C++ можно рассматривать как пример обобщенного программирования. Однако практикующие программисты часто рассматривают обобщенное программирование, как имеющее дополнительный существенный компонент, а именно: шаблоны должны конструироваться в целях предоставления большого числа полезных комбинаций. Наиболее значительный вклад в этой области принадлежит стандартной библиотеке шаблонов (Standard Template Library — STL), которая позже была адаптирована и включена в стандартную библиотеку C++. STL является основой, которая предоставляет большое количество полезных операций, называемых алгоритмами, для ряда линейных структур данных для хранения коллекции объектов (контейнеров (containers)). И алгоритмы и контейнеры являются шаблонами; однако ключевой момент состоит в том, что алгоритмы не являются функциями-членами контейнеров. Они написаны обобщенным способом, так что их можно использовать любым контейнером (и линейной коллекцией элементов). Для обеспечения такого использования проектировщики STL определили абстрактное понятие итераторов (iterators), которые могут быть предоставлены для любого вида линейной коллекции. По существу, аспекты функционирования контейнера, специфические для данной коллекции, оказались переложенными на функциональность итераторов. Вследствие этого операция наподобие вычисления максимального значения в последовательности может быть выполнена и без знания того, каким образом в этой последовательности хранятся значения. template <class Iterators Iterator max_element(Iterator beg, // Ссылка на начало Iterator end) //и конец коллекции { // Используются определенные операции итератора // для обхода всех элементов коллекции с целью // поиска элемента с максимальным значением и // возврата его позиции посредством итератора } Вместо того чтобы обеспечить полезными операциями, подобными max_element (), каждый из линейных контейнеров, контейнер должен предоставить итератор для обхода всех содержащихся в нем значений, а также функции-члены, необходимые для создания таких итераторов. namespace std { template <class T, ... > class vector { public: typedef ... const_iterator; // Зависящий от реализации итератор // для постоянных векторов
268 Глава 14. Полиморфные возможности шаблонов const_iterator begin() const; // Итератор для,начала коллекции const__iterator end() const; // Итератор для конца коллекции }; template <class T# ... > class list { public: typedef ... const_iterator; // Зависящий от реализации итератор // для постоянных списков const_iterator begin() const; // Итератор для начала коллекции const__iterator end() const; // Итератор для конца коллекции }; } Теперь можно находить максимум любой коллекции, вызывая обобщенную операцию max_element () с указанием начала и конца коллекции в качестве аргументов (здесь опущен специальный случай пустой коллекции). // poly/printmax.cpp #include <vector> #include <list> #include <algorithm> #include <iostream> #include "MyClass.hpp" template <typename T> void print_max(T const& coll) { // Объявление локального итератора коллекции typename T::const_iterator pos; // Вычисление позиции максимального значения pos = std::max_element(coll.begin(),coll.end()); // Вывод значения максимального элемента коллекции // (если таковой имеется) if (pos != coll.endO) {
14.6. Заключение 269 std::cout « *pos « std::endl; } else •{ std::cout « "empty" « std::endl; } int main() { std::vector<MyClass> cl; std::list<MyClass> c2; print_max(cl); print_max(c2) ; } Параметризируя свои операции в терминах итераторов, STL избегает резкого увеличения количества определений операций. Вместо того чтобы реализовать каждую операцию для каждого контейнера, нужно реализовать алгоритм всего лишь один раз, после чего его можно будет использовать для каждого контейнера. Обобщенная связка (generic glue) — это итераторы, которые обеспечиваются контейнерами и используется алгоритмами. Этот метод работоспособен, поскольку итераторы имеют определенный интерфейс, который обеспечивается контейнерами и используется алгоритмами. Этот интерфейс обычно называется концепцией (concept), что обозначает набор ограничений, которым должен удовлетворять шаблон, чтобы вписаться в соответствующую схему. В принципе функциональность STL может быть реализована и с использованием динамического полиморфизма. Однако на практике этот способ имел бы весьма ограниченное использование, поскольку концепция итератора слишком "легковесна" по сравнению с механизмом вызова виртуальных функций. Добавление уровня интерфейса, основанного на виртуальных функциях, скорее всего снизит скорость выполнения наших операций на порядок (а то и более). Обобщенное программирование основано на статическом полиморфизме, при котором разрешение интерфейсов осуществляется в процессе компиляции. Однако, с другой стороны, необходимость разрешения интерфейсов в процессе компиляции требует новых принципов проектирования, которые во многом отличаются от принципов объектно- ориентированного проектирования. Наиболее важные из этих принципов обобщенного проектирования описаны в оставшейся части книги. 14,6. Заключение Контейнерные типы были первым толчком для введения шаблонов в язык программирования C++. До шаблонов наиболее популярным подходом при разработке контейнеров были полиморфные иерархии. В качестве широко известного примера можно привести библиотеку классов Национального института здравоохранения (National Institutes
270 Глава 14. Полиморфные возможности шаблонов of Health Class Library — NIHCL), в которой была расширена иерархия контейнеров Smalltalk (рис. 14.5). Рис. 14.5. Иерархия классов NIHCL Во многом подобно стандартной библиотеке C++, NIHCL поддерживала широкое разнообразие контейнеров и итераторов. Однако реализация библиотеки следовала стилю динамического полиморфизма Smalltalk: для работы с коллекциями разных типов итераторы использовали абстрактный базовый класс Collection. К сожалению, цена такого подхода была весьма высока, это касалось как времени работы, так и используемой памяти. Обычно время работы оказывалось на порядок больше, чем у эквивалентного кода, использующего стандартную библиотеку C++, поскольку большинство операций приводили к виртуальным вызовам (в то время как в стандартной библиотеке C++ многие операции являются встраиваемыми, а в интерфейсах итераторов и контейнеров нет никаких виртуальных функций). Кроме того, поскольку (в отличие от Smalltalk) интерфейсы были ограниченными, встроенные типы должны были быть "обернутыми" в большие полиморфные классы (что и обеспечивала NIHCL), а это, в свою очередь, увеличивало потребность в памяти. Даже в нынешнюю эпоху шаблонов во многих проектах все еще делается неоптимальный выбор при использовании полиморфизма. Очевидно, что существует множество ситуаций, когда следует отдать предпочтение динамическому полиморфизму (в качестве яркого приме-
14.6. Заключение 271 pa можно привести гетерогенные коллекции). Одщако ничуть не меньше задач программирования естественно и эффективно решаются с использованием шаблонов. Использование статического полиморфизма хорошо подходит для кодирования наиболее фундаментальных вычислительных структур; необходимость же выбора общего базового типа приводит к тому, что динамическая полиморфная библиотека обычно хорошо удовлетворяет требованиям конкретной предметной области. Поэтому не должен вызывать никакого удивления тот факт, что STL-часть стандартной библиотеки C++ никогда не включала в свой состав полиморфные контейнеры, но зато содержит богатый набор контейнеров и итераторов, которые используют статический полиморфизм. Средние и большие программы, написанные на C++, обычно работают с обоими видами полиморфизма. Порой даже может возникнуть необходимость их весьма тесной комбинации. Во многих случаях выбор оптимального варианта проектирования в свете нашего обсуждения изначально представляется совершенно ясным, однако спустя некоторое время приходит понимание того, что здесь, как нигде, важна роль долгосрочного планирования с учетом всех возможных путей эволюции разрабатываемого проекта.
Глава 15 Классы свойств и стратегий Шаблоны дают возможность параметризовать классы и функции для различных типов. Кажется весьма заманчивым вводить столько параметров шаблонов, сколько нужно для того, чтобы настроить каждый аспект поведения типа или алгоритма. Таким образом, наши "шаблонизированные" компоненты могли бы быть реализованы так, чтобы удовлетворять любым потребностям пользовательского кода. Однако на практике нежелательно вводить большое количество параметров шаблонов для их максимально возможной параметризации. Необходимость указания всех соответствующих аргументов в пользовательском коде чрезмерно утомительна. К счастью, большинству дополнительных параметров можно назначить приемлемые значения по умолчанию. В ряде случаев дополнительные параметры полностью определяются несколькими основными параметрами и поэтому могут быть вообще опущены. Для других параметров могут быть заданы значения по умолчанию, зависящие от основных параметров, которые, тем не менее, в ряде случаев все же должны заменяться реальными значениями. Некоторые параметры оказываются не зависящими от основных параметров и в этом смысле сами являются основными параметрами. Классы стратегий и классы свойств (или шаблоны классов свойств) являются теми компонентами программирования на языке C++, которые значительно облегчают управление множеством дополнительных параметров, появляющихся при разработке мощных шаблонов. В этой главе приведен ряд ситуаций, в которых они доказывают свою несомненную эффективность, а также демонстрируются различные методы разработки мощных и надежных компонентов для ваших собственных программ. 15.1. Пример: суммирование последовательности Вычисление суммы последовательности значений — довольно тривиальная вычислительная задача. Однако эта простая на вид проблема может служить прекрасным примером использования классов стратегий и классов свойств на разных уровнях.
'274 Глава 15. Классы свойств и стратегий 15.1.1. Фиксированные классы свойств Предположим для начала, что значения, сумму которых необходимо вычислить, хранятся в массиве, и нам заданы указатели на первый суммируемый элемент и на элемент, следующий за последним. Естественно, потребуется написать шаблон, который будет применим для различных типов. Приведенный ниже код может показаться вам очень простым1. // traits/accuml.hpp #ifndef ACCUM_HPP #define ACCUM_HPP template <typename T> inline T accum(T const* beg, T const* end) { T total = T(); // Предполагаем, что ТОсоздает // нулевое значение while (beg != end) { total += *beg; ++beg; } return total; } #endif //ACCUM_HPP Здесь есть только один тонкий момент: как создать нулевое значение корректного типа для начала процесса суммирования. Мы используем здесь выражение ТО, которое должно правильно работать для встроенных числовых типов, таких, как int и float, т.е. для целых чисел и чисел с плавающей точкой (см. раздел 5.5, стр. 78). Рассмотрим теперь код, в котором используется наша функция ас cum (). // traits/accuml.cpp #include "accuml.hpp" #include <iostream> int main() { // Создание массива из пяти целочисленных значений int num[] = { 1, 2, 3, 4, 5}; В большинстве примеров, предлагаемых в этом разделе, ради простоты используются обычные указатели. Ясно, что в серьезной разработке может оказаться предпочтительным использование итераторов (следуя соглашениям стандартной библиотеки C++ [18]). Мы рассмотрим этот аспект несколько позже.
15.1. Пример: суммирование последовательности 275 // Вывод среднего значения std::cout << "the average value of the integer values is " << acQum(&num[0], &num[5]) / 5 « '\nf; // Создание массива символьных значений char name[] = "templates"; int length = sizeof(name)-1; ' II (Попытка) вывода среднего значения символов std::cout « "the average value of the characters in \"" << name << "\" is " << accum(&name[0], fcname[length]) / length « '\n'; } В первой половине этой программы использован оператор accum() для суммирования пяти целочисленных значений. int num[] = { 1, 2, 3, 4, 5}; accum(&num[0] , &num[5]) После этого полученная сумма просто делится на количество значений в массиве, что дает нам целочисленное среднее значение. Вторая половина программы пытается сделать то же самое для всех букв в слове templates (рассматривая символы от а до z как непрерывную последовательность в наборе символов, что справедливо для ASCII, но не для EBCDIC2). По-видимому, результат вычисления должен находиться между значением а и значением z. В настоящее время на большинстве платформ эти значения определяются ASCII-кодами: символ а имеет код 97, а символ z —122. Следовательно, можно предположить, что результат должен находиться где-то между 97 и 122. Однако программа выводит следующее сообщение: the average value of the integer values is 3 the average value of the characters in "templates" is -5 Проблема заключается в том, что наш шаблон был инстанцирован для типа char, у которого оказался слишком маленький диапазон для накопления даже относительно небольших значений. Ясно, что можно было решить эту проблему, введя дополнительный параметр шаблона АссТ, описывающий тип, который используется для переменной total (и соответственно возвращаемый тип). Однако тем самым мы бы добавили дополнительную работу всем пользователям: они были бы вынуждены указывать этот тип при каждом обращении к шаблону; например, рассмотренный ранее код использовал бы следующий вызов функции: EBCDIC (Extended Binary-Coded Decimal Interchange Code)— это расширенный двоично- Десятичный код обмена информацией, который представляет собой набор символов IBM, широко используемый на больших машинах IBM.
276 Глава 15. Классы свойств и стратегий accum<int>(&name[ 0 ] ,fcname[length]) Это не столь существенное ограничение, но и его можно избежать. Альтернативным подходом к применению дополнительного параметра является создание связи между каждым типом Т, для которого вызывается функция accum (), и типом, который будет использоваться для хранения накопленного значения. Эта связь может рассматриваться в качестве характеристики типа Т, и поэтому тип вычисляемой суммы иногда называется свойством (trait) Т. Эта связь может быть закодирована в виде специализации шаблона. // traits/accumtraits2.hpp template<typename T> class AccumulationTraits; templateo class AccumulationTraits<char> { public: typedef int AccT; }; templateo class AccumulationTraits<short> { public: typedef int AccT; }; templateo class AccumulationTraits<int> { public: typedef long AccT; }; templateo class AccumulationTraits<unsigned int> { public: typedef unsigned long AccT; }; templateo class AccumulationTraits<float> { public: typedef double AccT; }; Шаблон AccumulationTraits называется шаблоном свойств (traits template), поскольку он хранит свойство типа своего параметра. (Вообще говоря, в шаблоне свойств может быть как несколько свойств, так и несколько параметров.) В данном случае обоб-
15.1. Пример: суммирование последовательности 277 щенного определения шаблона нет, так как нет хорошего способа для выбора подходящего типа накопления в случае неизвестного исходного типа, Однако можно считать, что таким типом может быть сам тип Т. С учетом сказанного можно переписать наш шаблон accum (), как показано ниже. // traits/accum2.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include "accumtraits2.hpp" template <typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { // Возвращаемый тип является свойством типа элементов typedef typename AccumulationTraits<T>::AccT AccT; АссТ total = АссТ(); // Предполагаем, что АссТ()создает // нулевое значение while(beg != end) { total += *beg; ++beg; } return total; #endif // АССЦМ__НРР Теперь вывод нашей программы выглядит следующим образом: the average value of the integer values is 3 the average value of the characters in "templates" is 108 В целом внесенные изменения не очень впечатляющи, хотя добавлен очень полезный механизм настройки нашего алгоритма. Кроме того, если появятся новые типы, предназначенные для использования с accum (), соответствующий тип АссТ может быть связан с ними посредством простого объявления дополнительной явной специализации класса Accumula- tionTraits. Обратите внимание на то, что эта операция может быть выполнена для любого типа: основных типов, типов, которые объявлены в других библиотеках, и т.д. 15.1.2. Свойства-значения До сих пор речь шла о том, что свойства предоставляют дополнительную информацию о типах, имеющую отношение к данному "главному" типу. В этом разделе показано, что такая дополнительная информация не ограничивается только типами. С типом могут быть связаны константы и другие классы значений.
278 Глава 15. Классы свойств и стратегий Наш исходный шаблон accumO использует конструктор по умолчанию, возвращающий значение для инициализации переменной-результата с тем, чтобы она приняла значение, аналогичное нулевому. АссТ total = АссТО; // Предполагаем, что АссТ() создает // нулевое значение return total; Разумеется, нет никакой гарантии того, что этот код обеспечивает подходящее значение, необходимое для запуска цикла накопления. Ведь тип Т может даже не иметь конструктора по умолчанию. Но и в этом случае классы свойств могут спасти ситуацию. В данном примере можно добавить к нашему классу AccumulationTraits новое свойство-значение (value trait). // traits/accumtraits3.hpp template<typename T> class AccumulationTraits; templateo class AccumulationTraits<char> { public: typedef int AccT; static AccT const zero = 0; }; templateo class AccumulationTraits<short> { public: typedef int AccT; static AccT const zero = 0; }; templateo class AccumulationTraits<int> { public: typedef long AccT; static AccT const zero = 0; }; В представленном фрагменте кода нашим новым свойством является константа, которая может быть вычислена в процессе компиляции. Ниже показано, какой вид принимает при этом функция ас cum (). // traits/асситЗ-hpp #ifndef ACCUM__HPP
15.1. Пример: суммирование последовательности 279 #define АСОЛУЩРР #include naccumtraits3.hpp" template <typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { // Возвращаемый тип является свойством типа элементов typedef typename AccumulationTraits<T>::AccT АссТ; АссТ total = AccumulationTraits<T>::zero; while(beg != end) { . . total += *beg; ++beg; } return total; #endif // ACCUM_HPP В данном коде инициализация переменной для накопления результата остается очень простой: АссТ total = AccumulationTraits<T>::zero; Недостаток этого способа состоит в том, что C++ позволяет инициализировать статический константный член-данные внутри класса, только если он имеет целочисленный или перечислимый тип. Это исключает возможность использования наших собственных классов, а также типов с плавающей точкой. Таким образом, представленная ниже специализация является ошибочной. templateo class AccumulationTraits<float> { public: typedef double AccT; static double const zero = 0.0; // ОШИБКА: не целочисленный тип }; Простейший альтернативный способ состоит в том, чтобы не определять свойство- значение в классе. templateo class AccumulationTraits<float> { public:
280 Глава 15. Классы свойств и стратегий typedef double AccT; static double const zero; }; Инициализатор включается затем в исходный текст и выглядит примерно так: double const AccumulationTraits<float>::zero =0.0; Хотя этот способ вполне работоспособен, он отличается меньшей "прозрачностью" для компиляторов. При работе с пользовательскими файлами компиляторам ничего не известно об определениях в других файлах. В таком случае компилятор не способен, например, воспользоваться тем, что значение zero на самом деле равно 0.0. Следовательно, лучше реализовывать свойства-значения, которые не будут гаранта- рованно целочисленного типа, в виде встраиваемых функций-членов . Например, класс AccumulationTraits можно записать так, как показано ниже. // traits/accumtraits4.hpp template<typename T> class AccumulationTraits; templateo class AccumulationTraits<char> { public: typedef int AccT; static AccT zero() { return 0; } }; templateo class AccumulationTraits<short> { public: typedef int AccT; static AccT zero() { return 0; } }; templateo class AccumulationTraits<int> { public: typedef long AccT; static AccT zero() { Современные компиляторы C++ могут эффективно оптимизировать вызовы простых встраиваемых функций.
15.1. Пример: суммирование последовательности 281 return 0; } }; templateo class AccumulationTraits<unsigned int> { public: typedef unsigned long AccT; static AccT zero() { return 0; } }; templateo class AccumulationTraits<float> { public: typedef double AccT; static AccT zero() { return 0; } }; В коде приложения при этом появляется единственное отличие— использование синтаксиса вызова функции вместо несколько более краткого доступа к статической переменной-члену класса. AccT total = AccumulationTraits<T>::zero(); Ясно, что свойства могут быть чем-то гораздо большим, нежели просто дополнительными типами. В нашем примере они могут играть роль механизма обеспечения функции ас cum () всей необходимой информацией о типе элемента, для которого она вызвана. В этом состоит ключевой момент концепции свойств, а именно: свойства обеспечивают средства настройки конкретных элементов (обычно типов) для обобщенных вычислений. 15.1.3. Параметризованные свойства Использование свойств в ас cum (), показанное в предыдущих разделах, называется фиксированным, поскольку, как только будет определен отдельный класс свойств, в алгоритме его будет нельзя переопределить; хотя бывают ситуации, когда такое переопределение желательно. Например, может оказаться, что набор значений типа float вполне можно суммировать в переменной этого же типа, а не double; к тому же это приведет к некоторому повышению эффективности. В принципе решить эту проблему можно, добавив параметр шаблона со значением по Умолчанию, определяемым нашим шаблоном свойств. Таким образом, в большинстве случаев можно опустить дополнительный аргумент шаблона, а при необходимости легко изменить
282 Глава 15. Классы свойств и стратегий тип переменной-накопителя. Единственная неприятность, сводящая все на нет, заключается в том, что шаблоны функций не могут иметь аргументы шаблона по умолчанию4. Пока что обойдем проблему, представив наш алгоритм в виде класса. Тем самым будет заодно проиллюстрировано, что свойства могут использоваться в шаблонах класса по крайней мере так же легко, как и в шаблонах функций. Недостаток при использовании такого решения заключается в том, что шаблоны классов не могут иметь выводимые шаблонные аргументы, которые в результате должны быть указаны явно. Таким образом, для вызова функции нам придется использовать следующую запись: Accum<char>::accum(&name[ 0 ] , &name[length]) А шаблон для суммирования значений будет выглядеть так, как показано ниже. // traits/accum5.hpp #ifndef ACCUM_HPP #define ACCUM__HPP #include "accumtraits4.hpp" template <typename T, typename AT = AccumulationTraits<T> > class Accum { public: static typename AT::AccT accum(T const*beg, T const*end) { typename AT::AccT total = AT::zero(); while(beg != end) { total += *beg; ++beg; } return total; } }; #endif // ACCUMJiPP Вероятно, большинству пользователей этого шаблона никогда не придется явно указывать второй параметр шаблона, поскольку его значение по умолчанию будет вполне их устраивать. Как часто бывает в таких случаях, можно ввести для упрощения пару удобных в использовании встраиваемых функций. Эта особенность почти наверняка будет учтена при пересмотре стандарта C++, а разработчики компиляторов, вероятно, оснастят свои изделия возможностью использования аргументов шаблонов функций по умолчанию даже раньше, чем будет пересмотрен стандарт (см. раздел 13.3, стр. 233).
15.1. Пример: суммирование последовательности 283 template <typename T> inline typename AccumulationTraits<T>::AccT accum(T const* beg, T const* end) { return Accum<T>: :accum(beg, end); } template <typename Traits, typename T> inline typename Traits::AccT accum(T const* beg, T const* end) { return Accum<T, Traits>::accum(beg, end); } 15.1.4. Стратегии и классы стратегий До сих пор речь шла о накоплении применительно к суммированию. Очевидно, что можно представить и другие виды накопления, а не только суммирование. Например, можно перемножать заданную последовательность значений. Или, если значения представляют собой строки, можно просто конкатенировать эти строки. Даже поиск максимального значения последовательности можно представить как задачу накопления. Во всех этих вариантах единственная операция accumO, которая должна измениться,— это total += *start. Эту операцию можно назвать стратегией (policy) нашего процесса накопления. Класс стратегий, таким образом, является классом, который обеспечивает интерфейс, предназначенный для применения одной или нескольких стратегий в алгоритме . Приведем пример использования такого интерфейса в нашем шаблоне Ac cum. // traits/accum6.hpp #ifndef ACCUMLHPP #define ACCUM_HPP #include "accumtraits4.hppn #include "sumpolicyl.hpp" template <typename T, typename Policy = SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum(T const* beg, T const* end) { Это определение можно обобщить, рассматривая параметр стратегии (policy parameter), который может быть как классом, так и указателем на функцию.
284 Глава 15. Классы свойств и стратегий АссТ total = Traits::zero О; while(beg != end) { Policy::accumulate(total, *beg); ++beg; } return total; } }; #endif //ACCUM_HPP В этом случае класс SumPolicy может выглядеть следующим образом: // traits/sumpolicyl.hpp #ifndef SUMPOLICY__HPP #define SUMPOLICY_HPP class SumPolicy { public: template<typename Tl, typename T2> static void accumulate(T1& total, T2 const& value) { total += value; } }; #endif // SUMPOLICY_HPP В этом примере стратегия имеет вид обычного класса (не шаблона) со статическим шаблоном функции-члена (которая неявно является встраиваемой). Позже будет рассмотрен и другой вариант. Указывая разные стратегии накопления значений, можно вычислять разные вещи. Рассмотрим, например, программу, с помощью которой предполагается определять результат произведения ряда значений. // traits/accum7.cpp #include " ассшпб. hpp" #include <iostream> class MultPolicy { public: template<typename Tl, typename T2> static void accumulate (T1&. total, T2 const& value) { total *= value; } }; int main()
15.1. Пример: суммирование последовательности 285 { // Создание массива из пяти целочисленных значений int num[] ={l, 2, 3, 4, 5}; // Вывод произведения значений std::cout << "the product of the integer values is " << Accum<int,MultPolicy>::accum(&num[0], &num[5]) « 'Xn'; } Однако вывод программы окажется вовсе не тем, который ожидается: the product of the integer values is 0 Проблема вызвана нашим выбором начального значения: хотя значение 0 вполне пригодно при суммировании, оно не годится для умножения (нулевое начальное значение приводит к нулевому конечному результату). Этот пример иллюстрирует взаимодействие разных свойств и стратегий друг с другом, что еще раз подчеркивает, насколько важно быть аккуратным при проектировании шаблонов. В данном случае легко понять, что инициализация цикла накопления — это часть стратегии накопления. Данная стратегия может использовать свойство zero () (но может и не воспользоваться им). Не следует забывать и о других вариантах решения задачи — далеко не все нужно решать только с помощью свойств и стратегий. Например, функция accumulate () стандартной библиотеки C++ получает начальное значение в качестве третьего аргумента функции. 15.1.5. Различие между свойствами и стратегиями Вполне логично предположить, что стратегии представляют собой частный случай свойств. И наоборот, можно утверждать, что свойства—просто закодированные стратегии. Оксфордский словарь [29] дает следующие определения: • свойство... отличительная особенность, характеризующая сущность вещи; • стратегия... любой образ действия, принятый как полезный или целесообразный. На основании этих определений мы вправе ограничить использование термина стратегия классами, которые кодируют определенные действия, слабо связанные с другими аргументами шаблона, с которым это действие связано. Это положение согласуется с положением из [I]6: Стратегии имеют много общего со свойствами, но отличаются от них тем, что в них меньше внимания уделяется типам и больше — поведению. Автор этой книги Александреску (Alexandrescu) играет ключевую роль в мире классов стратегий; им разработан широкий набор основанных на них методов.
286 Глава 15. Классы свойств и стратегий Натан Майерс (Nathan Myers), разработавший метод использования свойств, предложил следующее, более открытое определение [27]: Класс свойств — это класс, используемый вместо параметров шаблона. В качестве класса он объединяет полезные типы и константы; как шаблон, он является средством для обеспечения того "дополнительного уровня косвенности", который решает все проблемы программного обеспечения. Таким образом, мы можем использовать следующие (несколько расплывчатые) определения. • Свойства представляют собой естественные дополнительные свойства параметра шаблона. • Стратегии представляют настраиваемое поведение обобщенных функций и типов (зачастую с некоторыми значениями по умолчанию). Для дальнейшей конкретизации возможных различий между двумя этими концепциями, перечислим ряд замечаний, касающихся свойств. • Свойства могут быть использованы и как фиксированные свойства, т.е. без передачи их шаблону в качестве параметров. • Параметры свойств обычно имеют естественные значения по умолчанию (которые крайне редко переопределяются или попросту не могут быть переопределены). • Параметры свойств имеют тенденцию к сильной зависимости от одного или нескольких основных параметров. • Свойства обычно содержат типы и константы, а не функции-члены. • Свойства имеют тенденцию к агрегации в шаблоны свойств. О классах стратегий также можно сделать несколько замечаний. • Классы стратегий практически всегда передаются в качестве параметров шаблона. • Параметры стратегий не обязательно должны иметь значения по умолчанию и часто явно специализируются (хотя многие обобщенные компоненты обычно настраиваются с использованием стратегий, заданных по умолчанию). • Параметры стратегий обычно слабо связаны с другими параметрами шаблона. • Классы стратегий обычно объединяют функции-члены в единое целое. • Стратегии могут объединяться в обычных классах или в шаблонах классов. Следует отметить, однако, что грань между обоими терминами весьма нечеткая. Например, свойства символов стандартной библиотеки C++ определяют также функциональное поведение, в частности сравнение символов, их перемещение и поиск. Заменяя эти свойства другими, можно определять строковые классы, которые ведут себя иначе, например нечувствительны к регистру символов при использовании того же символьного типа [18]. Таким образом, называясь свойствами, они имеют ряд характеристик, присущих стратегиям.
15.1. Пример: суммирование последовательности 287 15.1.6. Шаблоны членов и шаблонные параметры шаблонов Для реализации стратегии накопления был выбран вариант, в котором SumPolicy и MultPolicy представляли собой обычные классы с шаблонами членов. Другой вариант заключается в конструировании интерфейса класса стратегии с использованием шаблона класса, который затем применяется в качестве шаблонного аргумента шаблона. Например, можно переписать SumPolicy в виде шаблона. // traits/sumpolicy2.hpp #ifhdef SUMPOLICY_HPP #define SUMPOLICY_HPP template <typename Tl, typename T2> class SumPolicy { public: static void accumulate(T1& total, T2 constfc value) { total += value; } }; #endif // SUMPOLICYJHPP Интерфейс класса Accum можно затем адаптировать для использования шаблонного параметра шаблона. // traits/accum8.hpp #ifndef ACCUMLHPP #define ACCUM_HPP #include "accumtraits4.hpp" #include "sumpolicy2.hpp" template <typename T, template<typename,typename>class Policy=SumPolicy, typename Traits = AccumulationTraits<T> > class Accum { public: typedef typename Traits::AccT AccT; static AccT accum(T const*beg, T const*end) { AccT total = Traits::zero(); while(beg != end) { Policy<AccT,T>:accumulate(total, *beg); ++beg; } return total; >
288 Глава 15. Классы свойств и стратегий }; #endif //ACCUM_HPP Такое же преобразование можно применить и к параметру-свойству. (Возможны и другие варианты: например, вместо явной передачи в стратегию типа АссТ может оказаться полезной передача свойств накопления, а стратегия при этом определяет тип результата из параметра свойства.) Главное преимущество использования стратегий посредством шаблонных параметров шаблона— упрощение ситуации, когда класс стратегии содержит статический член- данные с типом, зависящим от параметров шаблона. Слабой стороной подхода с использованием шаблонных параметров шаблона является то, что классы стратегий должны быть написаны как шаблоны, с точным набором параметров шаблона, определяемых интерфейсом. Это, к сожалению, исключает добавление в наши стратегии каких бы то ни было дополнительных параметров шаблона. Например, может потребоваться добавить в SumPolicy параметр, не являющийся типом, например значение типа bool, указывающее, должно ли суммирование осуществляться с помощью оператора += или оператора +. В программе, использующей шаблон члена, можно просто переписать SumPolicy в виде шаблона. // traits/sumpolicy3.hpp #ifndef SUMPOLICY_HPP #define SUMPOLICY_HPP template<bool use_compound_op = true> class SumPolicy { public: template<typename Tl, typename T2> static void accumulate (T1& total, T2 const& value) { total += value; } }; templateo class SumPolicy<false> { public: template<typename Tl, typename T2> static void accumulate (T1& total, T2 constfc value) { total = total + value; } }; #endif // SUMPOLICY_HPP При реализации Ac cum с использованием шаблонного параметра шаблона такая адаптация становится невозможной.
15.1. Пример: суммирование последовательности 289 15.1.7. Комбинирование нескольких стратегий и/или свойств Как показали наши примеры, и свойства и стратегии в принципе допускают применение нескольких параметров шаблона. При этом, однако, их количество должно быть по возможности небольшим, чтобы обеспечить управляемость этой комбинацией. Возникает интересный вопрос: каким образом упорядочить такие множественные параметры? Элементарная политика состоит в упорядочении параметров согласно возрастанию вероятности выбора значения по умолчанию. Обычно это приводит к тому, что параметры свойств следуют за параметрами стратегий, поскольку они чаще переопределяются пользователями (возможно, вы уже заметили использование этого правила в наших примерах). Для тех, кто все же склонен использовать значительное количество параметров, тем самым существенно усложняя код, существует альтернатива, состоящая в задании параметров в любом порядке, не используя значений по умолчанию. Более подробно этот вопрос изложен в разделе 16.1, стр. 311. В главе 13, "Направления дальнейшего развития", обсуждаются некоторые возможные будущие свойства шаблонов, которые способны упростить этот аспект разработки шаблонов. 15.1.8. Накопление с обобщенными итераторами Прежде чем закончить введение в свойства и стратегии, полезно рассмотреть еще одну версию accum(), которая позволяет работать с обобщенными итераторами вместо указателей, что и ожидается от обобщенного промышленного компонента. Интересно, что возможность вызова accum () с указателями при этом остается, поскольку стандартная библиотека C++ обеспечивает использование так называемых свойств итераторов (iterator traits). Для этого можно переписать начальную версию accum () (опускаем при этом последующие усовершенствования). // traits/accumO.hpp #ifndef ACCUM_HPP #define ACCUM_HPP #include <iterator> template <typename Iter> inline typename std::iterator_traits<Iter>::value_type accum(iter start, Iter end) { typedef typename std::iterator_traits<Iter>::value_type VT; VT total = VT(); // Предполагаем, что VT()создает // нулевое значение while (start •= end) {
290 Глава 15. Классы свойств и стратегий total += *start; ++start; } return total; } #endif // ACCUM_HPP Структура iterator_traits инкапсулирует все существенные свойства итератора. Благодаря наличию частичной специализации для указателей эти свойства могут использоваться для.любых обычных указателей. Ниже показано, как стандартная библиотека может реализовать эту поддержку. namespace std { template <typename T> struct iterator_traits<T*> { typedef T value_type; typedef ptrdiff_t difference_type; typedef random_access__iterator_tag iterator_category; typedef T* pointer; typedef T& reference; }; } Однако типа для накопления значений, к которым обращается итератор, здесь нет, так что нам придется разрабатывать свой собственный класс AccumulationTraits. 15.2. Функции типа В первоначальном примере использования свойств показано, что можно задавать поведение, зависящее от типов. Это отличается от того, что мы обычно делаем в программах. В языках программирования С и C++ функции более точно можно назвать функциями значения (value functions): они принимают одни значения в качестве параметров и возвращают другое значение в качестве результата. При работе с шаблонами мы сталкиваемся с функциями типа (type functions), т.е. функциями, которые принимают некоторые аргументы типа и возвращают тип или константу в качестве результата. Весьма полезной встроенной функцией типа является sizeof, которая возвращает константу, указывающую размер (в байтах) данного аргумента типа. Шаблоны классов также могут играть роль функций типа. Параметры функции типа — это параметры шаблона, а результат получается как тип-член или константа-член. Например, оператор sizeof может быть использован с приведенным ниже интерфейсом. // traits/sizeof.срр #include <stddef.h> #include <iostream>
15.2. Функции типа 291 template <typename T> class TypeSize { public: static size_t const value = sizeof(T); } ; int main() { std::cout « "TypeSize<int>:rvalue = " « TypeSize<int>::value « std::endl; } В дальнейшем рассмотрим несколько более универсальных функций типа, которые могут использоваться в качестве свойств. 15.2.1* Определение типа элемента В качестве другого примера предположим, что у нас есть ряд шаблонов контейнеров: vector<T>, list<T> и stack<T>. Нам нужна функция типа, которая для данного типа контейнера возвращает тип его элементов. Этого можно достичь с помощью частичной специализации. // traits/elementtype.cpp #include <vector> #include <list> #include <stack> #include <iostream> #include <typeinfo> template <typename T> class ElementT; // Первичный шаблон template <typename T> class ElementT<std::yector<T> > { // Частичная специализация public: typedef T Type; }; template <typename T> class ElementT<std::list<T> > { // Частичная 'специализация public: typedef T Type; }; template <typename T>
292 Глава 15. Классы свойств и стратегий class ElementT<std::stack<T> > { // Частичная специализация public: typedef T Type; }; template <typename T> void print__element__type (T const & c) { std::cout << "Container of " << typeid(typename ElementT<T>::Type).name() << " elements.\n"; } int mainO { std::stack<bool> s; print__element_type (s) ; } Использование частичной спе!щализации позволяет реализовать эту функцию, не требуя, чтобы в типы контейнера были заложены сведения о ней. Зачастую, однако, функция типа разрабатывается вместе с соответствующими типами, так что ее реализация может быть существенно упрощена. Например, если типы контейнера определяют тип элемента value_type (как это делают стандартные контейнеры), то можно написать следующее: template <typename C> class ElementT { public: typedef typename C::value_type Type; }; Этот код может быть реализацией по умолчанию, что не исключает наличия специализаций для тех типов контейнеров, для которых не задан соответствующий тип элемента value_type. Тем не менее обычно желательно обеспечить возможность определения типов для параметров шаблонов, чтобы к ним было легче обращаться в обобщенном коде. В следующем далее фрагменте кода представлен набросок этой идеи. template <typename Tl, typename T2, ... > class X { public: typedef Tl ... ; typedef T2 ... ; }; В чем заключается польза функции типа? Она позволяет параметризовать шаблон в терминах типа контейнера, не требуя при этом дополнительных параметров для типа элемента и других характеристик. Например, вместо
15.2. Функции типа 293 template <typename T, typename C> Т sum__of_elements (С const& с); (где требуется указание типа элемента в явном виде при помощи синтаксиса sum_of_elements<int> (list)) можно объявить template<typename C> typename ElementT<C>: :Type sum__of_elements (С constfc с); (где тип элемента определяется функцией типа). Обратите внимание, что свойства могут быть реализованы как расширения существующих типов. Таким образом, функции типа можно определять даже для фундаментальных типов и типов из закрытых библиотек. В данном случае тип ElementT назван свойством, поскольку он используется для обращения к свойствам типа данного контейнера С (в общем случае в таком классе может быть собрано несколько свойств). Таким образом, классы свойств не ограничиваются описанием лишь характеристик параметров контейнера, но могут использоваться для описания любого вида "основных параметров". 15.2.2. Определение типов классов С помощью приведенной ниже функции типа можно определить, является ли тип классом. // traits/isclasst.hpp template<typename T> class IsClassT { private: typedef char One; typedef struct {char a[2]; } Two; template<typename C> static One test(int C::*); template<typename C> static Two test(...); public: enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; >; Этот шаблон использует принцип SFINAE, который рассматривается в разделе 8.3.1, стр. 129. Принцип функционирования SFINAE состоит в поиске конструкции типа, которая является недопустимой для типов функций (но не для других типов), или наоборот. Для типов классов можно использовать то, что такая конструкция типа, как указатель на член int С: : *, правомерна только в том случае, если С является классом. Представленная далее программа использует эту функцию типа для проверки того, являются ли некоторые типы и объекты классами. // traits/isclasst.cpp
294 Глава 15. Классы свойств и стратегий #include <iostream> #include "isclasst.hpp" class MyClass { Instruct MyStruct { } ; union MyUnion { >; void myfunc() { } enum E { el } e; // Проверка путем передачи типа в качестве аргумента шаблона template <typename T> void check() { if (IsClassT<T>::Yes) { std::cout « " IsClassT " « std::endl; } else { std::cout « " 'IsClassT " « std::endl; } } // Проверка путем передачи типа в качестве аргумента функции template <typename T> void checkT(T) { check<T>(); } int main() { std::cout « "int: "; check<int>(); std::cout « "MyClass: "; check<MyClass>(); std::cout « "MyStruct:"; MyStruct s;
15.2. Функции типа 295 checkT(s); std::cout « "MyUnion: "; ch,eck<MyUnion> () ; std::cout « "enum: "; checkT(e); std::qout « "myfunc():"; checkT(myfunc); } Программа дает следующий вывод: int: !IsClassT MyClass: IsClassT MyStruct: IsClassT MyUnion: IsClassT enum: !IsClassT myfunc(): !IsClassT 15.2.3. Ссылки и квалификаторы Рассмотрим определение шаблона функции // traits/applyl.hpp template <typename T> void apply(T& arg, void (*func)(T)) { func (arg); } и код, который пытается его использовать: // traits/applyl.cpp • #include <iostream> #include "applyl.hpp" void incr(int& a) { ++a; } void print(int a) { std::cout « a « std::endl; }
296 Глава 15. Классы свойств и стратегий int main () { int x = 7; apply(x, print); apply(x, incr); } Вызов apply (x, print) вполне корректен. При замене Т на int типами параметров apply () являются int& и void(*) (int). Ситуация с вызовом apply (х, incr) не столь проста. Второй параметр требует, чтобы тип Т был заменен типом int&, а это подразумевает, что типом первого параметра является int&&, который недопустим в C++. На самом деле исходный стандарт языка C++ допускал такую замену, но из-за примеров, подобных рассматриваемому, позже технический список опечаток (technical corrigendum [32]) сделал тип Т& с Т, заменяемым типом int&, эквивалентным int& . В тех компиляторах языка C++, где не реализовано более новое правило подстановки ссылок, можно создать функцию типа, которая будет применять "оператор ссылки" тогда и только тогда, когда данный тип не является ссылкой. Можно осуществить и обратную операцию — убрать оператор ссылки (тогда и только тогда, когда тип действительно яв- о ляется ссылкой). После этого можно добавить или удалить спецификатор const . Все это достигается с использованием частичной специализации представленного ниже обобщенного определения. // traits/typeopl.hpp template <typename T> class ТуреОр { // Первичный шаблон public: typedef T ArgT; typedef T BareT; typedef T const ConstT; typedef T & RefT; typedef T & RefBareT; typedef T const & RefConstT; }; Сначала разработаем частичную специализацию для типов с описанием const. // traits/typeop2.hpp template <typenameT> Обратите внимание на то, что мы все равно не можем писать int&&. Это аналогично тому, что Т const позволяет заменять Т на int const, но в явном виде конструкция int const const недопустима. о Обработка спецификаторов volatile и const volatile для краткости опущена, но они обрабатываются аналогично.
15.2. Функции типа 297 class ТуреОр <Т const> { // Частичная специализация для const public: typedef T const ArgT; typedef T BareT; typedef T const ConstT; typedef T const & RefT; typedef T & RefBareT; typedef T const & RefConstT; }; Частичная специализация для ссылок работает также с типами ссылок на const. Следовательно, при необходимости ТуреОр будет применяться рекурсивно, пока не будет получен "голый" тип. И напротив, C++ позволяет применять спецификатор const к параметру шаблона, который заменяется типом, уже являющимся типом const. Таким образом, нам не нужно беспокоиться об устранении описания const при повторном его применении. // traits/typeop3.hpp template <typename T> class ТуреОр <T&> { // Частичная специализация для ссылок public: typedef T & ArgT; typedef typename TypeOp<T>::BareT BareT; typedef T const ConstT; typedef T & RefT; typedef typename TypeOp<T>::BareT & RefBareT; typedef T const & RefConstT; }; Ссылки на тип void не допускаются, однако бывает полезно рассматривать такие типы, как простой void. Приведенная ниже специализация показывает, как это делается. // traits/typeop4.hpp templateo class ТуреОр <void> { // Полная специализация для void public: typedef void ArgT; typedef void BareT; typedef void const ConstT; typedef void RefT; typedef void RefBareT; typedef void RefConstT; }; С учетом этого можно переписать шаблон apply template <typename T> Void apply(typename TypeOp<T>::RefT arg, void (*func)(T))
298 Глава 15. Классы свойств и стратегий { func(arg) ; } и наша демонстрационная программа будет работать как положено. Не забывайте, что тип Т не может быть выведен из первого аргумента, поскольку теперь этот тип присутствует в квалификаторе имени. Таким образом, Т выводится только из второго аргумента и используется для создания типа первого параметра. 15.2.4. Свойства продвижения До сих пор изучались и разрабатывались функции типа, в которых по заданному типу определялись другие связанные типы или константы. В общем случае можно разработать функции типа, зависящие от нескольких аргументов. Один из примеров — так называемые свойства продвиэюения (promotion traits). Для пояснения этой идеи запишем шаблон функции, который позволяет суммировать два контейнера Array. template<typename T> Array<T> operator + (Array<T> const&, Array<T> const&); Поскольку язык позволяет суммировать значения char и int, можно позволить себе выполнение подобных операций смешанного типа по отношению к массивам. Однако при этом возникает вопрос: каким должен быть возвращаемый тип? template<typename Tl, typename T2> Array<???> operator +(Array<Tl> const&, Array<T2> const&); Шаблон свойств продвижения позволяет заменить вопросительные знаки в предыдущей записи следующим образом: template<typename Tl, typename T2> Array<typename Promotion<Tl/ T2>::ResultT> operator + (Array<Tl> const&, Array<T2> const&) ; Или по-другому: template<typename Tl, typename T2> typename Promotion<Array<Tl>, Array<T2> >::ResultT operator + (Array<Tl> const&, Array<T2> const&); Идея состоит в том, чтобы обеспечить большое количество специализаций шаблона Promotion, необходимых для создания функции типа, соответствующей нашим потребностям. Другое применение свойств продвижения мотивировано введением шаблона max () в том случае, когда требуется указать, что максимум двух значений различного типа должен иметь "более мощный тип" (см. раздел 2.3, стр. 35). Для данного шаблона не существует действительно надежного обобщенного определения, так что, может быть, лучше всего оставить первичный шаблон класса без определения. template<typename Tl, typename T2> class Promotion;
15.2. Функции типа 299 Еще одна альтернатива состоит в том, что если один из типов больше другого, то следует вернуть больший тип. Это может быть выполнено с помощью специального шаблона IfThenElse, который использует не являющийся типом параметр шаблона типа bool для выбора одного из двух параметров типа. // traits/ifthenelse.hpp #ifndef IFTHENELSE_HPP #define IFTHENELSEJHPP // Первичный шаблон: возвращает второй или третий // аргумент в зависимости от первого template<bool С, typename Та, typename Tb> class IfThenElse; // Частичная специализация: true возвращает второй аргумент template<typename Та, typename Tb> class IfThenElse<true, Та, Tb> { public: typedef Та ResultT; }; // Частичная специализация: false возвращает третий аргумент tempiate<typename Та, typename Tb> class IfThenElse<false, Та, Tb> { public: typedef Tb ResultT; }; #endif // IFTHENELSE_HPP С учетом этого можно разработать трехвариантный выбор между Tl, T2 и void, в зависимости от размеров типов. // traits/promotel.hpp // Первичный шаблон для продвижения типа template<typename Tl, typename T2> class Promotion { public: typedef typename IfThenElse<(sizeof(Tl)>sizeof(T2)), Tl, typename IfThenElse<(sizeof(Tl)<sizeof(T2)), T2, void >::ResultT >::ResultT ResultT; };
300 Глава 15. Классы свойств и стратегий Эвристика, основанная на размере типа, которая использована в первичном шаблоне, иногда срабатывает, но при этом все же требуется проверка. Если будет выбран неправильный тип, должна быть написана соответствующая специализация, переопределяющая неверный выбор. С другой стороны, если два типа идентичны, то такой тип можно безопасно делать поддерживаемым. Об этом позаботится следующая частичная специализация: // traits/promote2.hpp // Частичная специализация для двух идентичных типов template<typename T> class Promotion<T/T> { public: typedef T ResultT; }; Для продвижения базовых типов необходимо использовать большое количество специализаций. Применение макрокоманды может существенно уменьшить размер исходного кода. // traits/promote3.hpp #define MK_PROMOTION(Tl,T2,Tr) templateo class Promotional, T2> { public: typedef Tr ResultT; }; templateo class Promotion<T2/ Tl> { public: typedef Tr ResultT; }; Затем добавляются продвижения. // traits/promote4.hpp MK_PROMOTION(bool, char, int) MK_PROMOTION(bool, unsigned char, int) MK_PROMOTION(bool, signed char, int) Этот подход относительно прост, но требует перечисления нескольких десятков возможных комбинаций. Существуют и альтернативные методы. Например, шаблоны is- FundaT и IsEnumT могут быть адаптированы для определения типа продвижения для целочисленных типов и типов с плавающей точкой. Продвижение затем требуется специализировать только для результирующих базовых типов (а также для пользовательских типов, как будет показано далее). \ \ \ \ \ \ \ \ \
15.3. Свойства стратегий 301 Как только шаблон Promotion определен для базовых (и при необходимости перечислимых) типов, прочие правила продвижения могут быть выражены через частичную специализацию. Для нашего примера Array это будет выглядеть, как показано ниже. // traits/promotearray.hpp template<typename Tl, typename T2> class Promotion<Array<Tl>, Array<T2> > { public: typedef Array<typename Promotional, T2>: :ResultT> ResultT; }; template<typename T> class Promotion<Array<T>/ Array<T>.> { public: typedef Array<typename Promotion<T/T>::ResultT> ResultT; } ; Эта последняя частичная специализация заслуживает особого внимания. Сначала может показаться, что представленная ранее частичная специализация для идентичных типов (Promotion<T, T>) является вполне пригодной для данного случая. К сожалению, частичная специализация Promotion<Array<Tl>,Array<T2> > является ни более, ни менее специализированной, чем частичная специализация Promotion<T,T> (см. раздел 12.4, стр. 225). Чтобы избежать неоднозначности при выборе шаблона, была добавлена последняя частичная специализация. Она в большей степени специализирована, чем любая из двух предыдущих. Чем больше типов, для которых продвижение имеет смысл, тем больше специализаций и частичных специализаций шаблона Promotion может быть добавлено. 15.3. Свойства стратегий До сих пор наши примеры шаблонов свойств использовались для определения свойств параметров шаблона: какой тип они представляют, какому типу должны предоставить поддержку в операциях смешанного типа и т.д. Такие свойства называются свойствами свойств (property traits). В то же время некоторые свойства определяют, как должны обрабатываться определенные типы. Такие свойства называются свойствами стратегий (policy traits). Это напоминает ранее обсуждавшуюся концепцию классов стратегий (и мы уже обращали внимание на то, что различие между свойствами и стратегиями недостаточно четкое), но свойства стратегий проявляют тенденцию к наличию уникальных свойств, связанных с параметром шаблона, в то время как стратегии обычно не зависят от других параметров шаблона. • Хотя свойства свойств зачастую реализуются, как функции типа, свойства стратегий обычно инкапсулируют стратегию в функциях-членах. В качестве первой иллюстрации
302 Глава 15. Классы свойств и стратегий <и :1 рассмотрим функцию типа, которая определяет стратегию для передачи параметров ' только для чтения. 15.3.1. Типы параметров только для чтения В языках С и C++ аргументы при вызове функции по умолчанию передаются по значению, т.е. значения аргументов, вычисленные вызывающей функцией, копируются в место, контролируемое вызываемой функцией. Большинство программистов знают, что это может приводить к существенным затратам времени и памяти при передаче больших структур и что в этом случае целесообразнее передавать аргументы как ссылки на константные объекты (в языке С — как указатели на константные объекты). Для меньших структур картина не всегда ясна, и с точки зрения эффективности лучший механизм зависит от используемой архитектуры. В большинстве случаев способ передачи аргументов не критичен, но иногда играет роль способ передачи даже маленьких структур. С шаблонами, конечно, ситуация становится несколько более тонкой, ведь априори не известно, насколько большим будет тип, заменяющий параметр шаблона. Кроме того, решение вопроса зависит не только от размера: маленькая структура может иметь ресурсоемкий конструктор копирования. Как указывалось ранее, данную проблему удобно решать, используя шаблон свойств стратегий, являющийся функцией типа: функция отображает тип аргумента Т в оптимальный тип параметра— Т или Т const&. В качестве первого приближения первичный шаблон может использовать передачу аргументов по значению для типов, не превышающих двух указателей, а иначе — передачу в виде ссылки на константный объект. template<typename T> class RParam { public: typedef typename IfThenElse<sizeof(T)<=2*sizeof(void*), T, T const&>::ResultT Type; }; С другой стороны, типы контейнеров, для которых sizeof возвращает маленькое значение, могут включать дорогие конструкторы копирования. Поэтому нам может потребоваться большое количество специализаций и частичных специализаций, как в приведенном ниже примере. template<typename T> class RParam<Array<T> > { public: typedef Array<T> const& Type; }; Возможно, из-за распространенности таких типов будет безопаснее пометить в первичном шаблоне типы, не являющиеся классами, как передаваемые по значению, а затем выборочно добавлять типы классов, для которых это продиктовано соображениями эф-
15.3. Свойства стратегий 303 фективности (для идентификации классов первичный шаблон использует рассмотренный ранее шаблон IsClassTo). // traits/rparam.hpp #ifndef RPARAM_HPP #define RPARAM_HPP #include "ifthenelse.hpp" #incliide "isclasst .hpp" template<typename T> class RParam { public: typedef typename IfThenElse<IsClassT<T>::No, T, T const&>::ResultT Type; }; #endif // RPARAM__HPP Теперь клиенты могут эффективно использовать данную стратегию. Предположим, например, что имеется два класса, причем для одного из них оптимальной является передача по значению. // traits/rparamcls.hpp #include <iostream> # include "rparam.hpp" class MyClassl { public: MyClassl() { } MyClassl(MyClassl const&) { std::cout « "MyClassl copy constructor called\n"; } }; class MyClass2 { public: MyClass2() { } MyClass2(MyClass2 const&) { std::cout « "MyClass2 copy constructor called\n"; } };
304 Глава 15. Классы свойств и стратегий // Передача объектов MyClass2 с RParamo по значению j templateo j class RParam<MyClass2> { public: typedef MyClass2 Type; }; Далее можно объявлять функции, которые используют RParamo для параметров только для чтения, и вызывать эти функции. // traits/rparaml.cpp # include "rparam.hpp" # include "rparamcls.hpp" // Функция, которая разрешает передачу параметров //по значению или по ссылке template <typename Tl, typename T2> void foo(typename RParam<Tl>::Type pi, typename RParam<T2>::Type p2) { } int main() { MyClassl mcl; MyClass2 mc2; foo<MyClassl,MyClass2>(mcl,mc2) } К сожалению, использование RParam не лишено и некоторых весьма существенных отрицательных сторон. Во-первых, объявление функции становится значительно запутаннее. Во-вторых (и это, возможно, еще более неприятно), при вызове функции типа £оо () нельзя использовать вывод аргументов, так как параметры шаблона появляются только в квалификаторах параметров функций. Следовательно, при вызове функции требуется явно указывать параметры шаблона. Громоздкий обходной путь в этом случае состоит в использовании встраиваемого шаблона функции-оболочки, но это решение основано на том предположении, что встраиваемая функция будет оптимизироваться компилятором. // traits/rparam2.cpp # include "rparam.hpp" #include "rparamcls.hpp" // Функция, разрешающая передачу параметра // по значению или по ссылке
15.3. Свойства стратегий 305 template <typename Tl, typename T2> void foo_core(typename RParam<Tl>::Type pi, typename RParam<T2>::Type p2) { // Оболочка для избежания явного указания параметра шаблона template <typename Tl,-typename T2> inline void foo(Tl const& pi, T2 constfc p2) { foo_core<Tl,T2 >(pi,p2); } int main() { MyClassl mcl; MyClass2 mc2/ foo(mcl,mc2); // To же, что и // foo_core<MyClassl,MyClass2>(mcl,mc2) } 15.3.2. Копирование, обмен и перемещение Продолжая тему эффективности, можно ввести шаблон свойств стратегий для выбора наилучшей операции копирования, обмена или перемещения элементов определенного типа. Вероятно, копирование будет осуществляться с помощью копирующего конструктора или оператора копирующего присвоения. Это определенно справедливо для одиночного элемента, но нет также ничего невозможного и в том, что копирование большого количества элементов данного типа может быть выполнено значительно эффективнее, чем посредством повторяющегося вызова конструктора или операции присвоения для этого типа. Аналогично, некоторые типы могут осуществлять обмен или перемещение намного эффективнее, чем в случае обобщенной последовательности классического вида. Т tmp(a); а = Ь; Ь = tmp; Типы контейнеров обычно выпадают из этой категории. Фактически иногда бывает так, что копирование не разрешается, в то время как обмен или перемещение выполняются прекрасно. В главе 20, "Интеллектуальные указатели", описана разработка так называемого интеллектуального указателя (smart pointer), обладающего именно этим свойством. Следовательно, может оказаться полезным собрать все решения в данной области в одном шаблоне свойств. В обобщенном определении необходимо уметь отличать классы от типов, не являющихся классами, чтобы позже не беспокоиться о пользовательском
306 Глава 15. Классы свойств и стратегий /J ! конструкторе копирования и копирующем присвоении. Здесь для выбора одной их двух U реализаций свойств воспользуемся наследованием. // traits/csmtraits.hpp template <typename T> class CSMtraits : public BitOrClassCSM<T/IsClassT<T>::No> { }; Таким образом, реализация полностью делегирована специализациям BitOr- ClassCSMo ("CSM" означает "Copy, Swap, Move"— "копирование, обмен, перемещение"). Второй параметр шаблона указывает, возможно ли безопасное использование побайтового копирования для выполнения различных операций. Обобщенное определение консервативно принимает, что побайтово копировать классы нельзя; но если некоторый тип класса допускает такие операции, то класс CSMtraits можно легко специализировать в целях повышения эффективности. templateo class CSMtraits<MyPODType> : public BitOrClassCSM<MyPODType, true> { }; По умолчанию шаблон BitOrClassCSM состоит из двух частичных специализаций. Первичный шаблон и безопасная частичная специализация (которая не использует побайтовое копирование) выглядят, как показано ниже. // traits/csml.hpp #include <new> #include <cassert> #include <stddef h> #include "rparam.hpp" // Первичный шаблон template<typename T, bool Bitwise> class BitOrClassCSM; // Частичная специализация для // безопасного копирования объектов template<typename T> class BitOrClassCSM<T, false> { public: static void copy(typename RParam<T>::ResultT src, T* dst) { // Копирование одного элемента в другой *dst = src; }
15.3. Свойства стратегий 307 static void copy_n(T const* src, T* dst, size__t n) { // Копирование п элементов в п других элементов for(size_t к = 0; к < n; ++к) { dst[к] = src[к]; } } static void copy_init(typename RParam<T>::ResultT src, void* dst) { // Копирование элемента в // неинициализированную память ::new(dst) T(src); } static void copy_init_ji(T const* src, void* dst, size__t n) { // Копирование п элементов в // неинициализированную память for(size__t к = 0; к < n; ++k) { ::new((void*)((char*)dst + к)) T(src[k]); } ^ ^ T* static void swap(T* a, T* b) { ' I/ Обмен двух элементов T tmp(a); *а = *b; *b = tmp; } static*void swap__n(T* a, T* b, size_t n) { // Осуществление обмена п элементов for(size_t к = 0; к < n; ++k) { T tmp(a[k]); a[k] = b[k]; b[k] = tmp; } } static void move(T* src, T* dst) { // Перемещение одного элемента в другой assert(src != dst); *dst = *src; src->-T(); } static void move_n(T* src, T* dst, size__t n) {
308 Глава 15. Классы свойств и стратегий // Перемещение п элементов в п других assert(src != dst); for(size_t к = 0; к < n; ++к) { dst[к] = src[к]; src[к] .-TO; } } static void move_init (T* src, void* dst) { // Перемещение элемента //в неинициализированную память assert(src != dst); ::new(dst) T(*src); src->~T(); } static void move_JLnit_n(T const* src, void* dst, size_t n) { // Перемещение п элементов //в неинициализированную память assert(src != dst); for(size_t к = 0; к < n; ++k) { ::new((void*)((char*)dst + k)) T(src[k]); src[k] .-TO; } } }; Здесь термин перемещение означает, что значение перенесено из одного места в другое и больше первоначального значения не существует (или, если точнее, первоначальное расположение может быть уничтожено). С другой стороны, операция копирования гарантирует, что и источник, и целевой объект содержат корректные идентичные значения. Не следует путать это с различием между функциями memcpy () и memmove О , которые существуют в стандартной библиотеке С: в этом случае функция memmove () считает, что исходная и целевая области памяти могут перекрываться, а функция memcpy () строится в предположении, что это невозможно. В нашей реализации свойств CSM всегда предполагается, что исходная и целевая области памяти не перекрываются. В промышленную библиотеку, вероятно, следует добавить операцию сдвига, которая перемещает объекты в пределах непрерывной области памяти. Ради простоты изложения она здесь не рассматривается. Все функции-члены нашего шаблона свойств стратегий являются статическими. Это справедливо почти всегда, поскольку функции-члены предназначены для применения к объектам с типом параметров, а не к объектам с типом классов свойств. Другая частичная специализация реализует свойства для типов, которые могут копироваться побайтово.
15.3. Свойства стратегий 309 // traits/csm2.hpp #include <cstring> #include <cassert> #include <stddef.h> #include "csml.hpp" // Частичная специализаций для быстрого // побайтового копирования объектов template <typename T> class BitOrClassCSM<T,true> : public BitOrClassCSM<T,false> { public: static void copy___n(T const* src, T* dst, size__t n) { // Копирование п элементов в п других элементов std::memcpy((void*)dst, (void*)src, n*sizeof(T)); } >; static void copy_init__n(T const* src, void* dst, size_t n) { // Копирование п элементов //в неинициализированную память std::memcpy(dst/ (void*)src/ n*sizeof(T)); static void move_n(T* src, T* dst, size__t n) { // Перемещение п элементов в п других элементов assert(src != dst); std::memcpy((void*)dst, (void*)src, n*sizeof(T)); static void move_init_n(T const* src, void* dst, size_t n) { // Перемещение п элементов //в неинициализированную память assert (src != dst); std::memcpy(dst, (void*)src, n*sizeof(T)); } Здесь использован дополнительный уровень наследования для того, чтобы упростить реализацию свойств для типов, которые могут копироваться побайтово. Конечно, это не единственная возможная реализация; на самом деле для определенных платформ неплохим решением может оказаться использование встроенного ассемблера (например, чтобы воспользоваться аппаратными операциями обмена).
310 Глава 15. Классы свойств и стратегий 15.4. Заключение Натан Майерс (Nathan Myers) был первым, кто формализовал концепцию параметров- свойств. Первоначально он представил их в Комитет по стандартизации C++ в качестве средства, определяющего, каким образом символьные типы данных должны обрабатываться в компонентах стандартных библиотек (например, во входных и выходных потоках). В то время он называл их багажными шаблонами (baggage templates) и отмечал, ^то они содержат свойства. Однако некоторым членам комитета не нравился термин багаж, и вместо этого был поддержан термин свойства (traits), который широко используется в настоящее время. Обычно пользователь вообще не имеет дела со свойствами: заданные по умолчанию классы свойств удовлетворяют распространенным потребностям, а поскольку они заданы по умолчанию, то вообще не появляются в исходном тексте пользователя. Это соображение является дополнительным доводом в пользу длинных описательных имен для заданных по умолчанию шаблонов свойств. Если же пользовательский код адаптирует поведение шаблона, предоставляя пользовательский аргумент свойств, то всегда можно использовать определение синонима типа с помощью конструкции typedef. В нашем рассмотрении шаблоны свойств представляли собой исключительно шаблоны классов. Строго говоря, это не обязательно. Если требуется только одно свойство стратегии, оно может быть обычным шаблоном функции, например: template <typename T, void (*Policy)(T const&, T const&)> class X; Однако первоначальная цель свойств состоит в уменьшении количества вторичных параметров шаблона, чего нельзя достичь, инкапсулируя в параметре шаблона только одно свойство (отсюда, в частности, становится понятно, почему Майерс предлагал определять термином багаж: упакованную коллекцию свойств). Мы еще вернемся к данному вопросу в главе 22, "Объекты-функции и обратные вызовы". Стандартная библиотека определяет шаблон класса std:: char_traits, который используется в качестве параметра свойств стратегии. Чтобы упростить адаптацию алгоритмов для работы с итераторами STL, предусмотрен очень простой шаблон свойств std: : iterator_traits (используемый в интерфейсах стандартных библиотек). Еще один шаблон свойств стандартной библиотеки— std: :numeric__limits. Шаблоны классов std: :unary_function и std: :binary_function попадают в эту же категорию и являются очень простыми функциями типа: они просто определяют имена-члены для типов своих аргументов с помощью конструкции typedef. Наконец, распределение памяти для стандартных типов контейнеров обрабатывается с использованием классов свойств стратегий. Стандартным шаблоном для этого является std:: allocator. Классы стратегий разрабатывались многими программистами. Особо большой вклад в эту область внес Андрей Александреску (Andrei Alexandrescu), благодаря которому термин классы стратегий получил такое широкое распространение, а его книгу Modern C++ Design [1] можно считать наиболее полной работой по данной теме, безусловно, гораздо полнее, чем эта глава.
Глава 16 Шаблоны и наследование Нет никакого повода считать, что шаблоны и наследование взаимодействуют каким-то особым образом. Следует отметить лишь тот факт (см. главу 9, "Имена в шаблонах"), что порождение от зависимых базовых классов требует особой тщательности при использовании неполных имен. Однако некоторые технологии программирования применяют так называемое параметризованное наследование, которое рассматривается в этой главе. 16.1. Именованные аргументы шаблона Зачастую различные методы работы с шаблонами приводят к тому, что шаблон содержит весьма значительное число параметров. Конечно, как правило, многие из них имеют вполне приемлемые значения по умолчанию. Естественный способ определения такого шаблона класса может выглядеть, как показано ниже. template<typename Policyl = DefaultPolicyl, typename Policy2 = DefaultPolicy2, typename Policy3 = DefaultPolicy3, typename Policy4' = DefaultPolicy4> class BreadSlicer { } ; Вероятно, такой шаблон чаще всего будет использоваться с аргументами по умолчанию, с применением синтаксиса BreadSlicero. Однако, если некоторый аргумент имеет значение не по умолчанию, все предшествующие ему аргументы должны быть явно указаны (даже если они используют значения по умолчанию). Понятно, что было бы гораздо лучше использовать конструкцию BreadSlicer <Policy3 = Custom>, чем стандартную, имеющую вид BreadSlicer<Default- Policyl, DefaultPolicy2, Custom>. Далее рассматривается метод, дающий возможность использовать синтаксис, очень похожий на описанный . Обратите внимание, что подобное расширение языка для аргументов вызова функции было предложено (и отклонено) в процессе стандартизации языка C++ еще раньше (более подробно это рассматривается в разделе 13.9, стр. 242).
312 Глава 16. Шаблоны и наследование Наш метод заключается в размещении значений типа по умолчанию в базовом классе и в переопределении некоторых из них в процессе наследования. Вместо непосредственного указания аргументов типа определим их через вспомогательные классы. Например, можно написать BreadSlicer<Policy3__is<Custom> >. Поскольку каждый аргумент шаблона может описьшать любую стратегию, значения по умолчанию не могут быть различными. Иными словами, на верхнем уровне все параметры шаблона эквивалентны. template <typename PolicySetterl = DefkultPolicyArgs, typename PolicySetter2 = DefaultPolicyArgs, typename PolicySetter3 = DefaultPolicyArgs, typename PolicySetter4 = DefaultPolicyArgs> class BreadSlicer { typedef PolicySelector<PolicySetterl, PolicySetter2, PolicySetter3, PolicySetter4> Policies; // Использование Policies::Pi,Policies::P2,... // для обращения к различным стратегиям }; После этого остается только одна проблема— написать шаблон PolicySe lector. Он должен объединить различные аргументы шаблона в единый тип, который переопределяет конструкции-члены typedef, используемые по умолчанию. Такое объединение может быть достигнуто с использованием наследования. // PolicySelector<A,B,C,D> создает A,B,C,D как // базовые классы. Discriminatoro позволяет иметь // несколько одинаковых базовых классов template<typename Base, int D> class Discriminator : public Base { }; template <typename Setterl, typename Setter2f typename Setter3, typename Setter4> class PolicySelector : public Discriminator<Setterl,1>, public Discriminator<Setter2,2>, public Discriminator<Setter3,3>, public Discriminator<Setter4,4> { }; Обратите внимание на использование промежуточного шаблона Discriminator. Он необходим для того, чтобы можно было иметь одинаковые типы Setter. (Иметь несколько непосредственных базовых классов одного и того же типа нельзя; обойти это ограничение можно с помощью опосредованного наследования.) Теперь соберем в базовом классе все значения по умолчанию.
16.1. Именованные аргументы шаблона 313 // Именуем стратегии по умолчанию, как Р1, Р2, РЗ и Р4 class DefaultPolicies { public: typedef DefaultPolicyl PI; typedef DefaultPolicy2 P2; typedef DefaultPolicy3 P3; typedef DefaultPolicy4 P4; }; Однако вы должны быть внимательны и избегать неоднозначности при многократном наследовании от этого базового класса, т.е. базовый класс должен наследоваться виртуально. // Класс для стратегий по умолчанию позволяет избежать // неоднозначности при помощи виртуального наследования class DefaultPolicyArgs : virtual public DefaultPolicies { }; Наконец, нужно написать ряд шаблонов для переопределения значений стратегий, заданных по умолчанию. template <typename Policy> class Policyl_is : virtual public DefaultPolicies { public: typedef Policy PI; // Переопределение }; template <typename Policy> class Policy2_is : virtual public DefaultPolicies { public: typedef Policy P2; // Переопределение }; template <typename Policy> class Policy3__is : virtual public DefaultPolicies { public: typedef Policy РЗ; // Переопределение }; template <typename Policy> class Policy4_is : virtual public DefaultPolicies { public: typedef Policy P4; // Переопределение }; Вернемся к сути нашего примера и инстанцируем BreadSlicero следующим образом: SreadSlicer<Policy3_is<CustomPolicy> > be; Для этого BreadSlicero тип Policies определен как £olicySelector<Policy3__is<CustomPolicy>, DefaultPolicyArgs,
314 Глава 16. Шаблоны и наследование DefaultPolicyArgs, DefaultPolicyArgs> С помощью шаблона класса Discriminatoro в результате будет получена иерархия, в которой все аргументы шаблона являются базовыми классами (рис. 16.1). Важное замечание: все эти базовые классы имеют один и тот же виртуальный базовый класс Default- Policies, который определяет заданные по умолчанию типы для Р1, Р2, РЗ и Р4. Однако РЗ переопределен в одном из порожденных классов, а^именно в классе Policy3_is<>. Согласно так называемому правилу доминирования (domination rule), это определение скрьгоает определение базового класса. Таким образом, здесь нет неоднозначности2. Defaul t Pol idee typedef DefaultPolicyl PI typedef DefaultPolicy2 P2 typedef DefaultPolicy3 P3 typedef DefaultPolicy4 P4 (virtual) Policy3_is<CustomPolicy> typedef CuetomPolicy P3; £ £ (virtual) DefaultPolicyArgs £ (virtual) DefaultPolicyArgs £ (virtual) DefaultPolicyArgs Ж Discriminator<...,1> T Discriminator... ,2> T Discriminator<...,3> 3E Discriminatory...,4> J I PolicySelector<Policy3_ie<CuetomPolicy>, DefaultPolicyArgs, DefaultPolicyArgsf DefaultPolicyArgs> Puc. 16.1. Иерархия типов BreadSlicero::Policies Внутри шаблона BreadSlicer можно обращаться к четырем стратегиям, используя для этого квалифицированные имена наподобие Policies: : РЗ. template <... > class BreadSlicer { public: void print () { Policies::P3::doPrint(); } }; Полный исходный текст можно найти в файле inherit/namedtmpl. срр. Определение правила доминирования можно найти в разделе 10.2/6 Стандарта C++ [31]» а также в [15], раздел 10.1.1.
16.2. Оптимизация пустого базового класса 315 Здесь разработана методика для четырех параметров шаблона, но очевидно, что эта методика применима для любого разумного количества таких параметров. Обратите внимание, что при этом нигде не было реального инстанцирования объекта вспомогательного класса, содержащего виртуальные базовые классы. Следовательно, тот факт, что они являются виртуальными базовыми классами, не влияет на производительность или потребление памяти. 16.2. Оптимизация пустого базового класса Классы C++ часто бывают пустыми, т.е. их внутреннее представление не требует выделения памяти во время работы программы. Это типичное поведение классов, которые содержат только члены-типы, невиртуальные функции-члены и статические данные- члены. Нестатические данные-члены, виртуальные функции и виртуальные базовые классы требуют при работе программы выделения памяти. Однако даже пустые классы имеют ненулевой размер. Если вы хотите это проверить, попробуйте запустить приведенную ниже программу. // inherit/empty.cpp #include <iostream> class EmptyClass { }; int main() { std::cout « "sizeof(EmptyClass) : " « sizeof(EmptyClass) « '\n'; } На множестве платформ эта программа будет выводить в качестве размера Empty- Class число 1. Некоторые системы налагают на типы классов требования выравнивания и могут выводить другое небольшое значение (обычно 4). 16.2.1. Принципы размещения Проектировщики C++ имели множество причин избегать классов с нулевым размером. Например, массив классов, имеющих нулевые размеры, также имел бы нулевой размер, но при этом арифметика указателей оказалась бы неприменима. Пусть, например, ZeroSizedT — тип с нулевым размером. ZeroSizedT z[10]; &z[i] — &z[j] // Вычисление расстояния между /7 указателями/адресами
316 Глава 16. Шаблоны и наследование Обычно разность из предыдущего примера получается путем деления числа байтов между двумя адресами на размер объекта данного типа. Однако, если этот размер нулевой, понятно, что такая операция не приведет к корректному результату. Тем не менее даже при том, что в C++ нет типов с нулевым размером, стандарт C++ устанавливает, что, когда пустой класс используется в качестве базового, память для него не выделяется при условии, что это не приводит к размещению объекта по адресу, где уэюе располоэюен другой объект или подобъект того же самого типа. Рассмотрим несколько примеров, чтобы разъяснить, что означает на практике так называемая оптимизация пустого базового класса (empty base class optimization — ЕВСО). Рассмотрим приведенную ниже программу. // inherit/ebcol.cpp #include <iostream> class Empty { typedef int Int; // typedef не делает класс непустым }; class EmptyToo : public Empty { }; class EmptyThree : public EmptyToo { }; int main() { std::cout « "sizeof (Empty) : "«sizeof (Empty) « '\n•; std::cout « "sizeof (EmptyToo) : "«sizeof (EmptyToo) « '\n'; std: :cout « "sizeof (EmptyThree) : "«sizeof (EmptyThree) « ■\n'; } Если ваш компилятор осуществляет оптимизацию пустого базового класса, то он выведет один и тот же размер для каждого класса (но ни один из этих классов не будет иметь нулевой размер (рис. 16.2). Это означает, что внутри класса EmptyToo классу Empty не выделяется никакое пространство. Обратите внимание и на то, что пустой класс с оптимизированными пустыми базовыми классами (при отсутствии непустых базовых классов) также пуст. Это объясняет, почему класс EmptyThree может иметь тот же размер, что и класс Empty. Если же ваш компилятор не выполняет оптимизацию пустого базового класса, выведенные размеры будут разными (рис. 16.3). > Empty V EmptyToo > EmptyThree Рис. 16.2. Размещение EmptyThree компилятором, который реализует ЕВСО
16.2. Оптимизация пустого базового класса 317 Empty ? EmptyToo EmptyThree Рис. 16.3. Размещение EmptyThree компилятором, который не реализует ЕВСО Рассмотрим пример, в котором оптимизация пустого базового класса невозможна. // inherit/ebco2.срр #include <iostream> class Empty { typedef int Int; // typedef не делает класс непустым }; class EmptyToo : public Empty { }; class NonEmpty : public Empty, public EmptyToo { }; int main () { std::cout « "sizeof(Empty): " « sizeof(Empty) « '\n'; std::cout « "sizeof(EmptyToo): " « sizeof(EmptyToo) « '\n'; std::cout « "sizeof(NonEmpty): " « sizeof(NonEmpty) « '\n'; } Может показаться неожиданным, что класс NonEmpty не пустой. Ведь ни он, ни его базовые классы не содержат никаких членов. Но дело в том, что базовые классы Empty и EmptyToo класса NonEmpty не могут быть размещены по одному и тому же адресу, поскольку это привело бы к размещению объекта базового класса Empty, принадлежащего классу EmptyToo, по тому же адресу, что и объекта базового класса Empty, принадлежащего классу NonEmpty. Иными словами, два подобъекта одного и того же типа находились бы в одном месте, а это не разрешено правилами размещения объектов языка C++. Можно мысленно представить, что один из базовых подобъектов Empty помещен со смещением 0 байт, а другой — со смещением 1 байт, но полный объект NonEmpty все равно не может иметь размер в один байт (рис. 16.4).
318 Глава 16. Шаблоны и наследование Empty Empty EmptyToo > NonEmpty Рис. 16.4. Размещение объекта NonEmpty компилятором, реализующим ЕВСО Ограничение на оптимизацию пустого базового класса можно объяснить необходимостью проверки, не указывают ли два указателя на один и тот же объект. Поскольку указатели внутренне почти всегда представлены как обычные адреса, необходимо гарантировать, что два различных адреса соответствуют двум различным объектам. Это ограничение может показаться не очень существенным, однако с ним часто приходится сталкиваться на практике, поскольку многие классы наследуются из небольшого набора пустых классов, определяющих некоторое общее множество синонимов имен типов. Когда два подобъекта таких классов оказываются в одном и том же полном объекте, оптимизация запрещена. 16.2.2. Члены как базовые классы Оптимизация пустого базового класса не имеет эквивалента для данных-членов, поскольку, помимо прочего, это создало бы ряд проблем с представлением указателей на члены. В результате иногда то, что реализовано как данные-члены, желательно реализовать в виде (закрытого) базового класса. Однако и здесь не обходится без проблем. Наиболее интересна эта задача в контексте шаблонов, поскольку параметры шаблона часто заменяются типами пустых классов (хотя, конечно, полагаться на это нельзя). Если о параметре типа шаблона ничего не известно, оптимизацию пустого базового класса осуществить не так-то легко. Рассмотрим тривиальный пример. template <typename Tl, typenam^ T2> class MyClass { private: Tl a; T2 b; }; Вполне возможно, что один или оба параметра шаблона заменяются типом пустого класса. В этом случае представление MyClass<Tl,T2> может оказаться не оптимальным, что приведет к напрасной трате одного слова памяти для каждого экземпляра MyClass<Tl,Т2>. Этого можно избежать, сделав аргументы шаблона базовыми классами. template <typename Tl, typename T2> class MyClass : private Tl, private T2 { };
16.2. Оптимизация пустого базового класса 319 Однако этот простой вариант имеет свои проблемы. Он не сработает, если Т1 или Т2 заменяются типом, не являющимся классом или типом объединения. Он также не работает, если два параметра заменяются одним и тем же типом (хотя можно легко решить эту проблему добавлением лишнего уровня наследования, как было показано ранее в главе). Но даже после решения этих проблем адресации останется еще одна очень серьезная проблема: добавление базового класса может существенно изменить интерфейс данного класса. Для нашего класса MyClass эта проблема может показаться не очень значительной, поскольку здесь совсем немного взаимодействующих элементов интерфейса, но, как будет показано далее в главе, наличие виртуальных функций-членов меняет картину. Понятно, что рассматриваемый подход к ЕВСО чреват всеми описанными видами проблем. Более практичное решение может быть изобретено для распространенного случая, когда параметр шаблона заменяется только типами классов и когда доступен другой член шаблона класса. Основная идея состоит в том, чтобы "слить" потенциально пустой параметр типа с другим членом с использованием ЕВСО. Например, вместо записи template <typename CustomClass> class Optimizable { private: CustomClass info; // Может быть пустым void* storage; }; можно записать: template <typename CustomClass> class Optimizable { private: BaseMemberPair<CustomClass, void*> info_and_storage; }; Даже беглого взгляда достаточно, чтобы понять, что использование шаблона Base- MembeirPair делает реализацию Optimizable более многословной. Однако некоторые разработчики библиотек шаблонов отмечают, что повышение производительности (для клиентов их библиотек) стоит этой дополнительной сложности. Реализация BaseMemberPair может быть довольно компактной. // inherit/basememberpair.hpp #ifndef BASE_MEMBER_PAIR_HPP #define BASE_MEMBER_PAIR_HPP template <typename Base, typename Member> class BaseMemberPair : private Base { private: Member member;
320 Глава 16. Шаблоны и наследование public: // Конструктор BaseMemberPair (Base const& b, Member const& m) : Base(b), member (m) { } // Доступ к данным базового класса через first() Base const& first() const { return (Base const&)*this; } Base& first() { return (Base&)*this; } // Доступ к члену-данным посредством second() Member const& second() const { return this->member; } Members second() { return this->member; } #endif // BASE_MEMBER_PAIR_HPP Для доступа к инкапсулированным (и, возможно, оптимизированным с точки зрения расхода памяти) элементам данных реализация должна использовать функции-члены first () HsecondO. 16.3. Модель необычного рекуррентного шаблона Это странное название (curiously recurring template pattern — CRTP) обозначает общий класс методов, которые состоят в передаче класса-наследника в качестве аргумента шаблона одному из собственных базовых классов. В самой простой форме код C++ такой модели выглядит, как показано ниже. template <typename Derived> class CuriousBase { }; class Curious : public CuriousBase<Curious> { };
16.3. Модель необычного рекуррентного шаблона 321 Эта первая схема CRTP имеет независимый от параметра шаблона базовый класс: Curious не является шаблоном и, следовательно, защищен от проблем видимости имен зависимых базовых классов. Однако это не главная характеристика CRTP. Действительно, точно так же можно было использовать альтернативную схему. template <typename Derived> class CuriousBase { }; template <typename T> class CuriousTemplate : public CuriousBase<CuriousTemplate<T> > { }; От этой схемы недалеко до еще одной альтернативы, на этот раз включающей шаблонный параметр шаблона. template <template<typename> class Derived> class MoreCuriousBase { }; template <typename T> class MoreCurious : public MoreCuriousBase<MoreCurious> { }; Простейшее применение CRTP — отслеживание количества созданных объектов некоторого типа класса. Этого легко достичь посредством увеличения целого статического члена-данных в каждом конструкторе и его уменьшения в деструкторе: Однако необходимость обеспечить соответствующий код в каждом классе весьма утомительна. Вместо этого можно написать шаблон, приведенный ниже. // inherit/objectcounter.hpp ^include <stddef.h> template <typename CountedType> class ObjectCounter { private: static size_t count; // Количество объектов protected: // Конструктор по умолчанию Obj ectCounter() { ++ObjectCounter<CountedType>::count; }
322 Глава 16. Шаблоны и наследование // Конструктор копирования ObjectCounter(ObjectCounter<CountedType> const&) { ++0bj ееtCounter<CountedType>::count; } // Деструктор -Obj ectCounter() { —ObjectCounter<CountedType>::count; } public: // Возвращение количества имеющихся объектов: static size_t live() { return ObjectCounter<CountedType>::count; } // Инициализация счетчика значением ноль template <typename CountedType> size_t ObjectCounter<CountedType>::count = 0; Если вы хотите подсчитать количество активных (не уничтоженных) объектов некоторого типа класса, для этого достаточно породить класс из шаблона ObjectCounter. Например, можно определить и использовать класс строк с подсчетом объектов. // inherit/testcounter.epp #include "objectcounter.hpp" #include <iostream> template <typename CharT> class MyString : public ObjectCounter<MyString<CharT> > { }; int main() { MyString<char> sl# s2; MyString<wchar_t> ws; std::cout « "number of MyString<char>: " « MyString<char>::live() « std::endl; std::cout « "number of MyString<wchar_t>: " « ws.liveO « std::endl; } В общем случае метод CRTP полезен для отделения реализаций интерфейсов, которые могут только быть функциями-членами (например, конструкторы, деструкторы или операторы индексации).
16.4. Параметризованная виртуальность 323 16.4. Параметризованная виртуальность Язык C++ позволяет непосредственно использовать для параметризации шаблонов три вида объектов: типы, константы и шаблоны. Однако косвенно он позволяет параметризовать и кое-что другое, например виртуальность функций-членов. Простой пример демонстрирует эту удивительную методику. // inherit/virtual.cpp #include <iostream> class NotVirtual { }; class Virtual { public: virtual void foo() { } }; template <typename VBase> class Base : private VBase { public: // Виртуальность foo() зависит от его объявления // (если таковое имеется) в базовом классе VBase void foo() { std::cout « "Base::foo()" « *\n'; } }; template <typename V> class Derived : public Base<V> { public: void foo() { std::cout « "Derived::foo()" « '\n'; } }; int main() { Base<NotVirtual>* pi = new Derived<NotVirtual>; pl->foo(); // Вызов Base::foo() Base<Virtual>* p2 = new Derived<Virtual>; p2->foo(); // Вызов Derived::foot) }
324 Глава 16. Шаблоны и наследование Этот метод предоставляет инструмент для разработки шаблона класса, который пригоден для использования как при инстанцировании конкретных классов, так и при расширении с применением наследования. Однако только этого далеко не всегда достаточно для того, чтобы добавить виртуальность для некоторых функций-членов в целях получения более специализированного базового класса. Этот метод разработки требует использования фундаментальных конструкторских решений, поэтому обычно более практичной является разработка двух различных инструментальных средств (класса или иерархии шаблонов классов), чем попытка интегрировать их в одну шаблонную иерархию. 16.5. Заключение Именованные аргументы шаблона используются для упрощения некоторых шаблонов классов в библиотеке Boost. Эта библиотека использует метапрограммирование для создания типа со свойствами, подобными нашему шаблону PolicySelector (но без виртуального наследования). Более простой вариант, представленный здесь, был разработан одним из авторов данной книги — Дэвидом Вандевурдом (David Vandevoorde). Метод CRTP используется еще с 1991 года. Первым этот метод описал Джеймс Коп- лиен (James Coplien) [10]. С тех пор было опубликовано множество разнообразных применений CRTP. Однако иногда CRTP ошибочно применяют к параметризованному наследованию. Как было показано, CRTP не требует, чтобы наследование было параметризовано, а многие формы параметризованного наследования не согласуются с CRTP. Кроме того, CRTP иногда путают с методом Бартона-Нэкмана (см. раздел 11.7, стр. 201). Наш пример ObjectCounter практически идентичен методу, разработанному Скоттом Мейерсом (Scott Meyers) [22]. Билл Гиббоне (Bill Gibbons) был основным инициатором введения оптимизации пустого базового класса в язык программирования C++, а Натан Майерс (Nathan Myers) предложил шаблон, подобный нашему BaseMemberPair, для получения максимальной пользы от применения данной оптимизации. В библиотеке Boost имеется значительно более сложный шаблон compressecL_pair, который решает ряд упомянутых в данной главе проблем и который может использоваться вместо разработанного нами шаблона BaseMemberPair.
Глава 17 Метапрограммы Метапрограммирование — это автоматизированное программирование. Другими словами, в процессе метапрограммирования создается код, который, в свою очередь, генерирует новый код, выполняющий поставленные разработчиком задачи. Обычно понятие метапрограммирование подразумевает рефлексивность: метапрограммный компонент — это часть программы, для которой он генерирует фрагмент программного кода. Зачем нужно метапрограммирование? Как и большинство других технологий программирования, оно применяется, чтобы достичь больших функциональных возможностей ценой меньших затрат, измеряемых объемом кода, усилиями на сопровождение и т.п. Характерной особенностью метапрограммирования является то, что определенная часть необходимых пользователю вычислений выполняется на этапе трансляции программы. Его применение часто объясняется повышением производительности (вычисление, которое происходит во время трансляции, легче оптимизировать) или упрощением интерфейса (как правило, метапрограмма короче, чем программа, которая генерируется во время ее работы), а зачастую — обеими причинами. Нередко работа метапрограммы основана на использовании классов свойств и типов функций, описанных в главе 15, "Классы свойств и стратегий", с которой рекомендуется ознакомиться до изучения материала данной главы. 17.1. Первый пример метапрограммы В 1994 году на заседании Комитета по стандартизации языка C++ Эрвин Анрух (Erwin Unruh) доложил о том, что шаблоны можно применять для некоторых вычислений во время компиляции, и написал программу, которая находит простые числа. Интересно, что вычисление простых чисел в этой программе выполнялось в процессе компиляции, а не во время работы программы. Точнее говоря, компилятор выдавал последовательность сообщений об ошибках с простыми числами от двух до некоторого заранее Жданного значения. Хотя эту программу нельзя считать переносимой (сообщения об °Шибках не стандартизированы), она продемонстрировала, что механизм инстанцирова- **ия шаблонов можно использовать как примитивный рекурсивный язык, способный выполнять нетривиальные вычисления во время компиляции. Вычисления такого вида, ко-
326 Глава 17. Метапрограммы торые проводятся с помощью шаблонов, принято называть шаблонным метапрограмми- рованием (template metaprogramming). В качестве введения в метапрограммирование начнем с простого упражнения (упомянутая выше программа Эрвина, вычисляющая простые числа, будет рассмотрена позже). Программа, к изучению которой мы переходим, демонстрирует, как возвести число 3 в заданную степень во время компиляции. // meta/pow3.hpp #ifndef P0W3JHPP #define POW3_HPP //' Исходный шаблон для возведения числа 3 в n-ю степень template<int N> class Pow3 { public: enum { result = 3 * Pow3<N-l>::result }; }; // Полная специализация для завершения рекурсии templateo class Pow3<0> { public: enum { result = 1 } ; }; #endif /7P0W3_HPP В основе метапрограммирования лежит рекурсивный механизм инстанцирования шаблонов . Чтобы вычислить с помощью приведенной выше программы 3N, в ней применяется рекурсивное инстанцирование шаблонов, которое осуществляется по таким правилам: 1) 3N = 3*3N"X; 2) 3° = 1. В первом шаблоне реализовано общее рекурсивное правило. template<int N> class Pow3 { public: enum { result = 3 * Pow3<N-l>::result }; }; В процессе инстанцирования для какого-нибудь целочисленного значения N в шаблоне Pow3<> необходимо вычислить значение переменной перечислимого типа result. Это значение в три раза больше значения для того же шаблона, инстанцированного для числа N-1. Пример рекурсивных шаблонов уже встречался в разделе 12.4, стр. 225. Этот пример можно рассматривать как простейший случай метапрограммирования.
17.2. Значения перечислимого типа и статические константы 327 Второй шаблон — это специализация, завершающая рекурсию. В нем задается значение переменной result для Pow3<0>. templateo class Pow3<0> { public: enum { result = 1 }; }; Рассмотрим более подробно, что происходит в процессе вычисления с помощью этого шаблона числа З7. Для этого нужно инстанцировать шаблон Pow3<7>. // meta/pow3.срр #include <iostream> #include "pow3.hpp" int main () { std::count « "Pow3<7>::result = " « Pow3<7>::result « '\n'; } Сначала компилятор инстанцирует шаблон Pow3<7>. В результате получим: 3 * Pow3<6>: -.result Теперь нужно инстанцировать тот же шаблон для N = б. Для этого, в свою очередь, понадобится шаблон Pow3<5>, Pow3<4> и т.д. Рекурсия завершается инстанцированием шаблона Pow3<> для N = 0, в результате которого переменной result присваивается значение 1. Приведенная выше программа с шаблоном Pow3<> (включая его специализацию для N = 0) называется шаблонной метапрограммой (template metaprogram). Рассмотренный пример демонстрирует, как произвести вычисление, которое выполняется на этапе трансляции программы во время инстанцирования шаблона. Этот пример сравнительно простой и на первый взгляд может показаться не очЬнь полезным. Однако в некоторых ситуациях подобные приемы программирования весьма удобны. 17.2. Значения перечислимого типа и статические константы В старых компиляторах C++ значения перечислимого типа были единственной возможностью для программиста получить в свое распоряжение "истинные" константы (так называемые выражения-константы (constant-expressions)), которые можно применять в объявлении класса. Однако в процессе стандартизации языка C++ появилась концепция внутриклассных статических инициализирующих констант, что существенно изменило ситуацию. Применение этих констант проиллюстрировано в приведенном ниже примере.
328 Глава 17. Метапрограммы struct TrueConstants { enum { Three = 3 }; static int const Four = 4; }; Здесь Four — такая же истинная константа, как и Three. С учетом сказанного выше рассмотренную в предыдущем разделе метапрограмму Pow3 можно переписать. // meta/pow3b.hpp #ifndef P0W3_HPP #define P0W3_HPP // Исходный шаблон для возведения числа 3 в n-ю степень. template<int N> class Pow3 { public: static int const result = 3 * Pow3<N-l>::result; }; // Окончательная специализация, завершающая рекурсию, templateo class Pow3<0> { public: static int const result = 1; }; #endif //P0W3_HPP Единственное отличие данной версии программы от предыдущей заключается в том, что в ней вместо значений перечислимого типа используются статические константы- члены. Однако эта версия обладает одним недостатком: дело в том, что статические константы — это lvalue. Предположим, что в программе объявлена функция наподобие void foo(int const&); и в нее передается результат выполнения метапрограммы foo(Pow3<7>::result); В этом случае компилятор должен передать функции адрес переменной-члена Pow3<7>: : result следующим образом: инстанцировать определение этой статической переменной-члена и поместить ее в определенную ячейку памяти. В результате вычисление больше не ограничивается лишь процессами, происходящими во время компиляции. Переменные перечислимого типа не являются lvalue (т.е. они не могут иметь адрес). Таким образом, если они передаются "по ссылке", статическая память не используется. Все происходит почти точно так же, как если бы вычисленное значение передавалась как
17.3. Второй пример: вычисление квадратного корня 329 литеральная константа. Учитывая изложенное, мы предпочитаем использовать во всех приведенных в этой книге метапрограммах переменные перечислимого типа. 17.3. Второй пример: вычисление квадратного корня Рассмотрим несколько более сложный пример— метапрограмму, вычисляющую квадратный корень заданного числа N. Приведем код этой метапрограммы и изложим принцип ее работы. // meta/sqrtl.hpp #ifndef SQRT_HPP #define SQRTJiPP // Первичный шаблон, предназначенный для вычисления sqrt(N) template <int N, int L0=1, int HI=N> class Sqrt { public: // Вычисление округленного среднего значения, enum { mid = (LO+HI+D/2 }; // Определяем, в какой половине числового отрезка // находится искомое значение. enum { result = (N<mid*mid) ? Sqrt<N,LO,mid-l>::result : Sqrt<N,mid,HI>::result }; }; // Специализация шаблона для случая LO = HI. template <int N, int M> class Sqrt<N,M,M> { public: enum { result = M }; }; #endif // SQRT_HPP В первом шаблоне реализовано общее рекурсивное вычисление, которое вызывается с параметром шаблона N (квадратный корень которого вычисляется) и двумя другими необязательными параметрами. Этими параметрами задаются минимальная и максимальная границы числового отрезка, на котором выполняется поиск результата. Если шаблон вызывается только с одним аргументом, то по умолчанию принимается, что квадратный корень не может быть меньшим единицы и большим самого значения этого параметра. Рекурсия осуществляется по методу бинарного поиска, который часто называют методом деления пополам. Сначала числовой отрезок, на котором ведется поиск результата, делится пополам. Затем в шаблоне проводится проверка, в какой из образовавшихся
330 Глава 17. Метапрограммы половин интервала находится результат, и в зависимости от этого переменным L0 и HI присваиваются те или иные новые значения. Это осуществляется с помощью условного оператора ?:. Если возведенное в квадрат значение переменной mid (середины отрезка) больше N, поиск результата продолжается в первой половине отрезка. В противном случае тот же шаблон применяется для поиска результата во второй половине отрезка. Конечная специализация, завершающая рекурсивную процедуру, вызывается, если значения переменных L0 и HI равны одной и той же величине М, которая и является конечным результатом. Рассмотрим простую программу, использующую метапрограмму, с которой вы только что ознакомились. // meta/sqrtl.cpp #include <iostream> #include "sqrtl.hpp" int main() { std::cout « "Sqrt<16>: -.result « ' \n\- std::cout « "Sqrt<25>::result « ' \n' ; std::cout « nSqrt<42>::result « * \n\- std::cout « "Sqrt<l>::result « ' \n'; } В процессе анализа выражения Sqrt<16>::result компилятор расширяет его до Sqrt<16,1,1б>::result В шаблоне метапрограммы вычисляется выражение Sqrt<16,1, 1б>: : result. mid = (1+16+1)/2 = 9 result = (16<9*9) ? Sqrt<16,l,8>::result : Sqrt<16,9,16>::result = (16<81) ? Sqrt<16,l,8>::result : Sqrt<16,9,16>::result = Sqrt<16,l/8>: .-result После этого вычисляется значение Sqrt<16,1, 8>: : result. mid = (1+8+1)/2 = 5 11 « Sqrt<16>::result " « Sqrt<25>::result " « Sqrt<42>::result " « Sqrt<l>:-.result
17.3. Второй пример: вычисление квадратного корня 331 result = (1б<5*5) ? Sqrt<16,l,4>::result : Sqrt<16,5,8>::result = (1б<25) ? Sqrt<16,l,4>:-:result : Sqrt<16,5,8>::result = Sqrt<16,l,4>::result Аналогично, на следующем этапе нужно вычислить значение выражения Sqrt<16,1, 4>: :result. Подробно процесс этого вычисления выглядит так, как показано ниже. mid = (1+4+1)/2 = 3 result = (16<3*3) ? Sqrt<16,l,2>::result : Sqrt<16,3,4>::result = (16<9) ? Sqrt<16/l/2>::result : Sqrt<16,3,4>::result = Sqrt<16,3,4>::result Наконец, приходим к вычислению выражения Sqrt<16,3,4>: : result. mid = (3+4+1)/2 = 4 result = (1б<4*4) ? Sqrt<16,3,3>::result : Sqrt<16,4,4>::result = (16<16) ? Sqrt<16,3,3>::result : Sqrt<16,4,4>::result = Sqrt<16,4,4>::result Когда дело доходит до выражения Sqrt<16,4, 4>: : result, рекурсия заканчивается, поскольку это выражение подходит для явной специализации, в которой верхняя и нижняя границы интервала поиска совпадают. Таким образом, конечный результат таков: result = 4 Отслеживание инстанцирований шаблона Рассматривая предыдущий пример, мы останавливались на реализации важнейших этапов вычисления квадратного корня из 16. Однако при вычислении выражения result = (16<9*9) ? Sqrt<16,l,8>::result : Sqrt<16,9,16>::result компилятор инстанцирует шаблоны не только первой, но и второй ветви (Sqrt<16,9, 1б>). Более того, поскольку доступ к переменной-члену результирующего класса осуществляется с помощью оператора ::, происходит инстанцирование всех переменных-членов внутри класса. Это означает, что полное инстанцирование шаблона Sqrt<16,9, 1б> приводит к полному инстанцированию шаблонов Sqrt<16, 9,12> и Sqrt<16,13, 1б>. Чтобы полностью проследить за всем процессом, пришлось бы рассматривать несколько десятков инстанцирований. Их количество почти в два раза превышает N.
332 Глава 17. Метапрограммы Для большинства компиляторов это приводит к неоправданно большому потреблению ресурсов (особенно оперативной памяти). К счастью, существуют методы, которые помогают обуздать такое лавинообразное увеличение количества инстанцирований. Для выбора отрезка, на котором будет проводиться дальнейший поиск результата, воспользуемся не условным оператором ?:, а специализацией шаблона. Чтобы продемонстрировать сформулированное утверждение, перепишем метапрограмму Sqrt. // meta/sqrt2.hpp #include "ifthenelse.hpp" // Первичный шаблон, задающий основной шаг рекурсии, template <int N, int L0=1, int HI=N> class Sqrt { public: // Вычисление округленного среднего значения. enum { mid = (LO+HI+D/2 }; // Определяем, в какой половине числового отрезка // находится искомое значение. typedef typename IfThenElse<(N<mid*mid), Sqrt<N,LO,mid-l>, Sqrt<N,mid,HI> >::ResultT SubT; enum { result = SubT::result }; }; // Частичная специализация шаблона для завершения рекурсии template <int N, int S> class Sqrt<N,S,S> { public: enum { result = S }; >; Главное отличие этой версии программы от предыдущей заключается в применении шаблона If ThenElse из раздела 15.2.4, стр. 298. // meta/ifthenelse.hpp #ifndef IFTHENELSE_HPP #define IFTHENELSE_HPP // Первичный шаблон; в зависимости от значения первого // аргумента возвращает значение второго или третьего // аргумента. template<bool С, typename Та, typename Tb> class IfThenElse; // Частичная специализация: если значение первого аргумента
17.4. Применение переменных индукции 333 // равно true, то возвращается второй аргумент template<typename Та, typename Tb> class IfThenElse<true/ Та, Tb> { public: typedef Та ResultT; >; // Частичная специализация: если значение первого аргумента // равно false, то возвращается третий аргумент. template<typename Та, typename Tb> class IfThenElse<false, Та, Tb> { public: typedef Tb ResultT; }; #endif // IFTHENELSE_HPP Напомним, что шаблон If ThenElse — это конструкция, с помощью которой выбирается один из двух типов. Выбор осуществляется на основе значения логической константы. Если эта константа равна true, тип ResultT с помощью инструкции typedef объявляется синонимом типа Та; в противном случае он объявляется синонимом типа ТЬ. Здесь важно отметить, что применение инструкции typedef к шаблону класса не приводит к инстанцированию компилятором C++ тела данного экземпляра. Поэтому, если в программе встречается приведенный ниже фрагмент кода, то ни выражение Sqrt<N, L0, mid-l>, ни выражение Sqrt<N, mid, HI> не инстанцируются в полной мере. typedef typename IfThenElse<(N<mid*mid), Sqrt<N,LO,mid-l>, Sqrt<N,mid,HI> >::ResultT SubT; Какой бы из типов не оказался синонимом типа SubT, полное инстанцирование происходит лишь при вычислении выражения SubT: : result. Такая стратегия, в отличие от предпринятой перед этим, приводит к тому, что количество инстанцирований становится пропорциональным logi(N). Это значительно сокращает затраты вычислительных ресурсов, особенно при достаточно больших N. 17.4. Применение переменных индукции Можно возразить, что метапрограмма, рассмотренная в качестве предыдущего примера, выглядит слишком сложно и не очень понятно, где и как применить изученный материал на практике. Поэтому рассмотрим более простую и более "итерационную" реализацию метапрограммы, вычисляющей квадратный корень. Этот простой итеративный алгоритм можно сформулировать так. Чтобы вычислить квадратный корень из N, организуется цикл, в котором переменная I изменяется от 1 до
334 Глава 17. Метапрограммы значения, квадрат которого равен или не превышает N. Это значение переменной I и будет искомым квадратным корнем из N. Сформулированный алгоритм, выраженный обычными средствами C++, выглядит, как показано ниже. int I; for (1=1; I*KN; ++I) { } // Значение переменной I равно квадратному корню из N Однако для организации того же алгоритма в виде метапрограммы цикл нужно сформулировать с помощью рекурсии, после чего понадобится указать специализацию, завершающую процесс рекурсии. В результате реализации такого цикла получаем приведенную ниже метапрограмму. // meta/sqrt3.hpp #ifndef SQRT_HPP #define SQRT_HPP // Первичный шаблон, предназначенный для итерационного // вычисления sqrt(N). template <int N, int I=l> class Sqrt { public: enum { result = (I*KN) ? Sqrt<N,I+l>::result : I }; }; // Частичная специализация, предназначенная для завершения // процесса рекурсии template <int N> class Sqrt<N,N> { public: enum { result = N }; }; #endif // SQRT_HPP Цикл осуществляется путем использования переменной I в шаблоне Sqrt<N,I>. До тех пор пока логическое выражение I*I<N остается истинным, переменной result присваивается значение выражения Sqrt<N, I+l>, в котором реализуется следующая итерация. В противном случае переменной result присваивается значение переменной I. Например, если нужно вычислить значение Sqrt<16>, сначала вычисляется Sqrt<16,1>. Таким образом, в начале итерации значение так называемой переменной индукции I равно единице. Далее, поскольку I*I<N, переходим к следующей итерации, вычисляя Sqrt<N,I+l>: : result. Как только значение выражения 1*1 станет равным или превысит N, значение переменной I объявляется результатом.
17.4. Применение переменных индукции 335 Может возникнуть вопрос о том, зачем нужна специализация шаблона, завершающая процесс рекурсии. Казалось бы, первый шаблон рано или поздно найдет результирующее значение переменной I, благодаря чему рекурсия, по-видимому, окончится. На самом деле это не так, и причиной, как и раньше, является эффект инстанцирования обеих ветвей оператора ?:, который обсуждался в предыдущем разделе. Таким образом, в процессе вычисления выражения Sqrt<4> будет осуществляться несколько инстанцирова- ний шаблонов. • Этап 1: result = (1*1<4) ? Sqrt<4/2>: -.result : 1 • Этап 2: result = (1*1<4) ? (2*2<4) ? Sqrt<4,3>: .-result : 2 : 1 • Этап 3: result = (1*1<4) ? (2*2<4) ? (3*3<4) ? Sqrt<4,4>::result : 3 : 2 : 1 • Этап 4: result = (1*1<4) ? (2*2<4) ? (3*3<4) ? 4 : 3 : 2 : 1 Несмотря на то что результат найден на втором этапе, инстанцирование шаблонов осуществляется компилятором до тех пор, пока рекурсия не завершится в специализации шаблона. Если не задать специализацию, компилятор будет продолжать инстанцирование, пока не исчерпает свои внутренние ресурсы. Как и раньше, проблема решается с помощью шаблона If ThenElse. // meta/sqrt4.hpp #ifndef SQRT_HPP #define SQRT_HPP #include "iftheiielse.hpp" // Шаблон, в котором переменной result // присваивается аргумент шаблона template<int N> class Value { public:
336 Глава 17. Метапрограммы enum { result = N }; }; // Шаблон для итерационного вычисления sqrt(N) template <int N, int I=l> class Sqrt { public: // Инстанцирование следующего шага или результата typedef typename IfThenElse<(I*I<N), Sqrt<N,I+l>, Value<I> >::ResultT SubT; // ... enum { result = SubT::result }; }; #endif //SQRT_HPP В качестве завершающего критерия используется шаблон Valueo, который возвращает значение аргумента шаблона в result. Как и раньше, с помощью шаблона IfThenElseo удается уменьшить количество инстанцирований, которое становится пропорциональным не N, а /о£г№. Это весьма существенное сокращение затрат в процессе работы метапрограммы. Что касается компиляторов, в которых количество инстанцирований шаблонов ограничено, то с их помощью новая версия программы сможет вычислять квадратные корни из больших чисел, чем предыдущая. Если компилятор, например, поддерживает до 64 вложенных инстанцирований шаблонов, то максимальное число, из которого можно вычислить квадратный корень, равно не 64 (как для предыдущей реализации алгоритма), а 4096. Вывод "итеративного" шаблона Sqrt будет выглядеть так: Sqrt<16>::result = 4 Sqrt<25>::result = 5 Sqrt<42>::result = 7 Sqrt<l>::result = 1 Заметим, что для простоты рассмотренная выше реализация выдает округленное к большему целое значение квадратного корня (результат вычисления квадратного корня из 42 равен не 6, а 7). 17.5. Полнота вычислений Рассмотренные примеры шаблонов Pow3<> и Sqrto продемонстрировали, что шаблонные метапрограммы могут содержать:
17.6. Рекурсивное инстанцирование и рекурсивные аргументы шаблона 337 • переменные состояния (параметры шаблона); • циклические конструкции, реализуемые с помощью рекурсии; • конструкции, осуществляющие выбор пути с помощью условных выражений или специализаций; • арифметические операции с целыми числами. Если нет ограничений на количество рекурсивных инстанцирований шаблонов и количество переменных состояния, то этих элементов достаточно для вычисления почти всего, что поддается вычислению. Однако воплощение алгоритмов с помощью шаблонов может оказаться не таким удобным, как "обычное" программирование. Более того, инстанцирование шаблонов, как правило, требует существенных затрат ресурсов компилятора. При этом рекурсивное инстанцирование большого количества шаблонов значительно замедляет работу компилятора и даже может стать причиной того, что возможности вычислительных ресурсов компьютера будут исчерпаны. Согласно рекомендациям стандарта для языка программирования C++, компилятор должен иметь возможность осуществить как минимум 17 уровней рекурсивных инстанцирований, однако это не является жестким требованием. При интенсивном применении шаблонного программирования этот предел может оказаться далеко не достаточным. Таким образом, на практике к применению шаблонного программирования следует относиться крайне осторожно. Однако есть ситуации, когда такой метод является незаменимым инструментом программиста. В частности, иногда рекурсивные шаблоны могут быть "спрятаны" внутри более традиционных, что позволяет добиться более производительной реализации алгоритма. 17.6. Рекурсивное инстанцирование и рекурсивные аргументы шаблона Рассмотрим приведенный ниже рекурсивный шаблон. template<typename T, typename U> struct Doublify {}; template<intN> struct Trouble { typedef Doublify<typename Trouble<N-l>::LongType, typename Trouble<N-l>::LongType> LongType; }; templateo struct Trouble<0> { typedef double LongType; }; Trouble<10>::LongType ouch;
338 Глава 17. Метапрограммы Инструкция Trouble<10>: : LongType не только инициирует рекурсивное инстан- цирование шаблонов Trouble<9>, Trouble<8>,..., Trouble<0>, но и инстанцирует структуру Doublify, что приводит к быстрому усложнению определения типа. Скорость роста сложности продемонстрирована в табл. 17.1. Таблица 17.1. Усложнение объявления типа Trouble<N>:: LongType Имя типа Определение типа Trouble<0>::LongType double Trouble<l>::LongType Doublify<double/double> Trouble<2>::LongType Doublify<Doublify<double/double>, Doublify<double,double> > Trouble<3>::LongType Doublify<Doublify<Doublify<double/double>, Doublify<double/double> >, <Doublify<double,double>, Doublify<double,double> > > Как видно из этой таблицы, сложность описания типа выражения Trouble<N>:: LongType с увеличением N растет по закону 2N. Вообще говоря, в такой ситуации компилятор несет даже большую нагрузку, чем при рекурсивном инстанцировании шаблонов, в котором не принимают участия рекурсивные аргументы шаблона. Одна из возникающих в таких случаях проблем состоит в том, что компилятор сохраняет представление скорректированного имени типа. В этом скорректированном имени определенным образом закодирована точная специализация шаблона. В ранних версиях компиляторов C++ применялась кодировка, в которой длина была (грубо) прямо пропорциональна длине тела шаблона. Для шаблона Trouble<10>: : LongType в таких компиляторах пришлось бы затратить более 10000 символов. В более новых компиляторах C++ принимается во внимание, что в современных программах на C++ довольно часто встречаются вложенные шаблоны, и с учетом этого в них используется более компактная кодировка. Это способствует существенному снижению темпов роста объема кодировки для имен (например, для типа Trou- ble<10>: : LongType потребуется всего несколько сотен символов). При прочих равных условиях рекурсивное инстанцирование шаблонов предпочтительнее организовать так, чтобы аргументы шаблона не были рекурсивно вложенными. 17.7. Метапрограммы для развертывания циклов Одно из первых практических применений метапрограммирования состояло в развертывании циклов при выполнении числовых расчетов. Ниже рассматривается завершенный пример, в котором производятся эти вычисления. В приложениях, осуществляющих числовые расчеты, зачастую обрабатываются л-мерные массивы, или векторы. Одна из типичных операций в таких расчетах — так на-
17.7. Метапрограммы для развертывания циклов 339 зываемое скалярное произведение. Скалярным произведением двух векторов а и Ъ называется сумма попарных произведений всех компонентов обоих векторов. Например, если у каждого из векторов по три компонента (т.е. в соответствующих им одномерных массивах по три элемента), то их скалярное произведение определяется по формуле a[0]*b[0]+a[l]*b[l]+a[2]*b[2]; Обычно в математической библиотеке содержится функция, предназначенная для вычисления скалярного произведения. Рассмотрим приведенную ниже простейшую реализацию этой процедуры. // meta/loopl.hpp #ifndef LOOPl__HPP #define LOOPl_HPP template <typename T> inline T dot_product(int dim, T* a, T* b) { T result = T(); for(int i = 0; i < dim; ++i) { result += a[i]*b[i]; } return result; } #endif // LOOPl_HPP Вызовем эту функцию в приведенном ниже фрагменте программы. // meta/loopl.cpp #include <iostream> #include "loopl.hpp" int main() { int a[3] = { 1, 2, 3}; int b[3] = { 5, 6, 7}; std::cout « "dot_product(3,a,b) = " « dot_product(3,a,b) « ' Nn^- stdiicout « "dot_product(3, a,a) = " « dot_product(3,a,a) « ' \n'; } В результате получим следующее: dot_product (3,a,b) = 38 dot_product(3,a,a)= 14
340 Глава 17. Метапрограммы Результат правильный, однако он достигается слишком длинным путем, который нежелателен при разработке серьезных высокопроизводительных приложений. Чтобы достичь оптимальной производительности, недостаточно даже объявить функцию встраиваемой. Проблема в том, что компиляторам обычно лучше удается оптимизировать циклы, в которых выполняется большое количество итераций, а в данном случае такой метод непроизводительный. Намного лучше было бы непосредственно расписать этот цикл как а[0]*Ь[0]+а[1]*Ь[1]+а[2]*Ь[2] Конечно, если в процессе работы программы время от времени нужно вычислить од- но-два скалярных произведения, то производительность функции dot_product () не играет существенной роли. Если же подобная библиотечная функция будет применяться для вычисления нескольких миллионов скалярных произведений, то производительность ее работы приобретает важное значение. Разумеется, можно было бы не вызывать функцию dot_product (), а написать код для непосредственного вычисления скалярного произведения или разработать специальные функции для вычисления скалярного произведения одномерных массивов с одним, двумя, тремя и т.д. компонентами (что довольно утомительно). Шаблонное метапро- граммирование помогает в решении этой задачи. Нужно организовать цикл так, как показано в приведенной ниже программе. // meta/loop2.hpp #ifndef LOOP2_HPP #define LOOP2_HPP // Первичный шаблон, template <int DIM, typename T> class DotProduct { public: static T result(T* a, T* b) { return *a * *b + DotProduct<DIM-l,T>:rresult(a+l,b+l); } }; // Частичная специализация критерия окончания рекурсии template <typename T> class DotProduct<l/T> { public: static T result (T* a, T* b) { return *a * *b; } }; // Функция для вычисления скалярного произведения template <int DIM, typename T> inline T dot_product(T* a, T* b)
17.7. Метапрограммы для развертывания циклов 341 { return DotProduct<DIM/T>::result(a,b); } #endif // LOOP2_HPP Теперь достаточно лишь немного изменить приложение, чтобы получить тот же результат, что и раньше. // meta/loop2.срр #include <iostream> #include "1оор2.hpp" int main() { int a[3] = { 1, 2, 3}; int b[3] = { 5, 6, 7}; std::cout « "dot__product<3>(a,b) = " « dot_product<3>(a,b) « '\n'; std::cout « "dot_product<3>(a,a) = " « dot__product<3>(a,a) « 'Xn'; } Вместо инструкции dot_product(3,a,b) мы применяем инструкцию dot_product<3>(a,b) В этом выражении инстанцируется функция dot_product, вызов которой преобразуется в следующий код: DotProduct<3,int>::result(a,b) Это и служит началом метапрограммы. В теле метапрограммы переменной result присваивается сумма произведения первых элементов массивов а и b и скалярного произведения массивов, количество элементов в которых на единицу меньше, чем в исходных массивах а и Ь. Это скалярное произведение равняется предыдущему за вычетом произведения первых элементов массивов а и Ь. template <int DIM, typename T> class DotProduct { public: static T result (T* a, T* b) { return *a * *b +
342 Глава 17. Метапрограммы DotProduct<Dim-l,T>::result(a+l,b+l); } }; Критерий окончания — снижение количества компонентов вектора до единицы. template <typename T> class DotProduct<l,T> { public: static T result (T* a, T* b) { return *a * *b; } }; Таким образом, процесс инстанцирования функции dot_product<3> (a, b) будет выглядеть так: DotProduct<3, int<: -.result (a,b) = *a * *b + DotProduct<2,int>::result(a+l,b+l) = *a * *b + *(a+l) * *(b+l) + DotProduct<l,int>::result(a+2,b+2) = *a * *b + *(a+l) * *(b+l) + *(a+2) * *(b+2) Заметим, что при такой методике программирования нужно, чтобы количество элементов массива было известно во время компиляции (что справедливо хотя и часто, но далеко не всегда). В таких библиотеках, как Blitz++ [4], MTL [25] и РООМА [30] метапрограммы, подобные рассмотренной выше, используются в быстрых подпрограммах линейной алгебры. Такие метапрограммы работают лучше, чем оптимизаторы, поскольку они ассимилируют в себе знания более высокого уровня2. В промышленной реализации этих библиотек используется намного больше приемов, чем рассматривается в данной главе. Например, необдуманная организация циклов не всегда приводит к оптимизации работы программы; однако эти дополнительные инженерные подробности выходят за рамки нашей книги. 17.8. Заключение Как уже упоминалось, автором первой опубликованной метапрограммы был Эрвин Анрух, представляющий на заседании Комитета по стандартизации C++ компанию Siemens. Он заметил, что процесс инстанцирования шаблонов обладает вычислительной полнотой, и продемонстрировал это положение, разработав свою первую метапрограм- му. При этом был использован компилятор Metaware, выдающий в процессе трансляции В некоторых случаях метапрограммы работают намного быстрее, чем соответствующие программы на языке Fortran, несмотря на то, что оптимизатор языка Fortran, как правило, хорошо приспособлен именно для работы с приложениями такого вида.
17.8. Заключение 343 данной программы сообщения об ошибках, номера которых образуют последовательность простых чисел. Ниже приведен код упомянутой программы, обсуждавшийся на заседании Комитета по стандартизации C++ в 1994 году (модифицированный для работы со стандартными современными компиляторами)3. // meta/unruh.cpp // Вычисление простых чисел по Эрвину Анруху template <int р, int i> class is_prime { public: enum { prim = (p==2) || (p%i) && is_prime<(i>2?p:0),i-l>::prim }; }; templateo class is_prime<0,0> { public: enum {prim=l}; }; templateo class is_prime<0, l> { public: enum {prim=l}; }; template <int i> class D { public: D(void*) ; }; template <int i> class Prime_print { // Первичный шаблон, в котором организован // цикл для вывода простых чисел public: Prime_print<i-1> а; enum { prim = is_prime<i,i-l>::prim }; void f() { Выражаем благодарность Эрвину Анруху за предоставление этого кода для данной книги. Оригинальный вариант кода можно найти в [38].
344 Глава 17. Метапрограммы D<i> d = prim ? 1 : 0; a.f (); } }; templateo class Prime__print<l> { // Полная специализация шаблона // для завершения цикла public: enum {prim=0}; void f() { D<1> d = prim ? 1 : 0; }; }; #ifndef LAST #define LAST 18 #endif int main() Prime_print<LAST> a; a.f(); } В процессе компиляции этой программы компилятор выводит сообщения об ошибках, если в функции Prime_print: : f () не удается инициализировать переменную d. Это происходит, когда начальное значение равно 1, поскольку конструктор определен только для типа void*, а к этому типу корректно преобразуется лишь значение 0. Например, один из компиляторов среди других сообщений выдает такие: unruh.срр:3б: conversion from 'int' to non-scalar type ,D<17>1 requested unruh.cpp:3б: conversion from 'int' to non-scalar type 'D<13>' requested unruh.cpp:3б: conversion from 'int' to non-scalar type 'D<11>' requested unruh.cpp:3 б: conversion from 'int' to non-scalar type 'D<7>' requested unruh.cpp:3б: conversion from 'int' to non-scalar type 'D<5>' requested unruh.cpp:36: conversion from 'int' to non-scalar type 'D<3>' requested unruh.cpp:3б: conversion from 'int' to non-scalar type 'D<2>' requested Впервые концепция шаблонного метапрограммирования на C++ стала популярным инструментом программистов благодаря статье Тодда Вельдхаузена (Todd Veldhuizen)
17.8. Заключение 345 Using C++ Template Metaprograms (Использование шаблонных метапрограмм в C++) [40]. Работа Тодда над библиотекой Blitz++ (библиотека численных функций C++, выполняющих действия с массивами) [4] также внесла многие усовершенствования и дополнения в метапрограммирование (в частности, в методы применения шаблонов выражений, которые рассматриваются в следующей главе).
Глава 18 Шаблоны выражений В этой главе описывается метод шаблонного программирования, известный под названием шаблоны выражений. Впервые этот метод возник при работе с классами числовых массивов, и именно в таком контексте он рассматривается в данной главе. Класс числовых массивов поддерживает выполнение числовых операций с массивом как с целостным объектом. Например, к двум массивам можно применить операцию сложения; в результате ее выполнения получим массив, каждый элемент которого будет суммой соответствующих элементов исходных массивов. Аналогично, массив можно умножить на скаляр. Это означает, что на скаляр умножается каждый элемент исходного массива. Возникает естественное желание реализовать для массивов операторные обозначения, так знакомые нам по работе со встроенными скалярными типами. Array<double> х(1000), y(1000) х = 1.2*х + х*у; Для серьезных высокопроизводительных вычислительных комплексов важно, чтобы выражения такого рода вычислялись настолько эффективно, насколько можно ожидать от платформы, на которой выполняется этот код. Достичь того, чтобы такие вычисления можно было выполнять с помощью компактной записи, приведенной в рассмотренном выше примере, — задача далеко не тривиальная, однако при ее решении на помощь приходят шаблоны выражений. Шаблоны выражений напоминают шаблонное метапрограммирование. Частично это объясняется тем, что иногда они опираются на глубоко вложенное йнстанцирование шаблонов, которое не отличается от рекурсивного инстанцирования, встречающегося в шаблонном метапрограммировании. Не исключено, что на сходство этих двух методов также повлияло то, что оба они первоначально разрабатывались для поддержки высокопроизводительных операций с массивами (обратитесь к примеру применения шаблонов Для развертывания циклов в разделе 17.7, стр. 338). Эти два метода, несомненно, дополняют друг друга. Например, метапрограммирование удобно применять для небольших массивов фиксированного размера, а шаблоны выражений весьма эффективны для выполнения операций со средними и большими массивами, размеры которых задаются во время работы программы.
348 Глава 18. Шаблоны выражений 18.1. Временные объекты и раздельные циклы Чтобы обосновать применение шаблонов выражений, рассмотрим несложный (возможно, даже наивный) подход к реализации шаблонов, позволяющих выполнять операции с числовыми массивами. Основной шаблон массива мог бы выглядеть, как показано ниже (имя SArray обозначает простой массив — simple array). // exprtmpl/sarrayl.hpp #include <stddef.h> #include <cassert> template<typename T> class SArray { public: // Создание массива указанного размера explicit SArray (size_t s) : storage (new T[s] ) , storage__size(s) { init(); } // Конструктор копирования SArray(SArray<T> constfc orig) : storage (new T[orig. size () ] ) , storage__size (orig. size () ) { copy(orig); } // Деструктор: освобождение памяти -SArray() { delete[] storage; } // Оператор присвоения SArray<T>& operator = (SArray<T> constfc orig) { if (&orig != this) { copy(orig); } return *this; } // Возврат размера size__t size() const { return storage_size; } // Оператор индексации для констант и переменных
18.1. Временные объекты и раздельные циклы 349 Т operator[] (size_t idx) const { return storage[idx]; } T& operator[] (size_t idx) { return storage[idx]; } protected: // Инициализация значений конструктором по умолчанию void init() { for (size_t idx = 0; idx < size(); ++idx) { storage[idx] = T(); } } // Копирование значений из другого массива void copy (SArray<T> const& orig) { assert(size()==orig.size()); for(size_t idx = 0; idx < size(); ++idx) { storage[idx] = orig.storage[idx]; } } private: T* storage; size_t storage__size; }; Числовые операторы можно закодировать, как показано ниже. // exprtmpl/sarrayopsl.hpp // Сложение двух объектов SArray template<typename T> SArray<T> operator+(SArray<T> const& a, SArray<T> const& b) { . SArray<T> result(a.size()); for(size_t k = 0; k < a.sizeO; ++k) { result[k] = a[k]+b[k]; } return result; } // Умножение двух объектов SArray template<typename T> SArray<T> operator*(SArray<T> const& a, SArray<T> const& b) { SArray<T> result(a.size()); for(size_t k = 0; k < a.sizeO; ++k) { // Хранилище элементов // Количество элементов
350 Глава 18. Шаблоны выражений result[к] = а[к]*Ь[к]; } return result; } // Умножение скаляра на объект SArray template<typename T> SArray<T> operator*(T constfc s, SArray<T> const& a) { SArray<T> result(a.size()); for(size_t k = 0; k < a.sizeO; ++k) { result[k] = s*a[k]; } return result; } // Умножение объекта SArray на скаляр; // сложение скаляра и объекта SArray; // сложение объекта SArray и скаляра Можно было бы написать еще много версий этих и других операторов, однако для нашего примера достаточно того, что есть. // exprtmpl/sarrayl.cpp #include "sarrayl.hpp" #include "sarrayopsl.hpp" int main() { SArray<double> x(1000), y(1000); // x = 1.2*x + x*y; } Однако оказывается, что приведенная выше реализация операторов крайне неэффективна. Тому есть две причины. 1. При каждом применении оператора (за исключением оператора присвоения) создается, как минимум, один временный массив. Это означает, что в нашем примере создается по крайней мере три временных массива, в каждом из которых содержится по 1000 элементов (это при условии, что самому компилятору не требуется никаких дополнительных временных копий). 2. При каждом применении оператора компилятору требуется выполнить дополнительный обход массивов-аргументов и результирующих массивов. В нашем примере это приводит к необходимости считать приблизительно 6000 и записать около 4000 значений типа double.
18.1. Временные объекты и раздельные циклы 351 Оценим количество операций, которые выполняются в циклах, обрабатывающих временные массивы. tmpl = 1.2*х; // Цикл из 1000 операций // плюс создание и удаление массива tmpl tmp2 = x*y; // Цикл из 1000 операций // плюс создание и удаление массива tmp2 tmp3 = tmpl+tmp2; // Цикл из 1000 операций // плюс создание и удаление массива tmp3 х = tmp3; // 1000 операций считывания и // 1000 операций записи Если в компиляторе не применяется специальный высокопроизводительный распределитель памяти, то при работе с небольшими массивами основное время затрачивается на создание ненужных временных массивов. Для выполнения операций с очень большими массивами использование временных массивов абсолютно недопустимо, поскольку может оказаться, что их негде хранить. (Чтобы получить достоверные результаты при численном моделировании реальных систем, часто расходуется вся доступная память. Если же она будет применяться для хранения ненужных временных массивов, это приведет к снижению точности расчетов.) В ранних реализациях численных библиотек, предназначенных для работы с массивами, эта проблема оставалась нерешенной, и пользователям рекомендовалось вместо операторов сложения и умножения применять операторы присвоения со сложением и присвоения с умножением. Преимущество последних состоит в том, что в них вызывающий массив является одновременно и аргументом, и целевым массивом, поэтому они не требуют временных массивов. Например, два объекта класса SArray можно было бы сложить, как показано ниже. // exprtmp/sarrayops2.hpp // Присвоение с прибавлением объекта SArray template<class T> SArray<T>& SArray<T>::operator += (SArray<T> const& b) { for(size_t k = 0; k < size(); ++k) { (*this)[k] += b[k]; } return *this; } // Присвоение с умножением объекта SArray template<class T> SArray<T>& SArray<T>::operator * = (SArray<T> const& b) { for(size__t k = 0; k < size(); ++k) { (*this)[k] *= b[k]; }
352 Глава 18. Шаблоны выражений return *this; } // Присвоение с умножением на скаляр template<class T> SArray<T>& SArray<T>::operator *= (Т const& s) { for(size_t k = 0; k < size(); ++k) { (*this)[k] *= s; } return *this; } С помощью подобных операторов наш вычислительный пример можно было бы переписать. // exprtmpl/sarray2.срр #include "sarray2.hpp" #include "sarrayopsl.hpp" #include "sarrayops2.hpp" int main() { SArray<double> x(1000), y(1000); // Вычисляем x = 1.2*x + x*y SArray<double> tmp(x); tmp *= y; x *= 1.2; x += tmp; } Понятно, что метод, при котором используются только операторы присвоения с вычислением, тоже не оправдывает наших ожиданий. Он обладает такими недостатками: • обозначения становятся громоздкими; • полностью избавиться от ненужных временных массивов не удается; • в теле цикла выполняется множество операций; в результате для его выполнения требуется выполнить около 6000 считываний и 4000 записей значений типа double. На самом деле нам нужен некий единый "идеальный" цикл, в котором бы все выражение вычислялось для каждого индекса. int main() { SArray<double> х(1000), у(1000); for(int idx = 0 ; idx < x.sizeO; ++idx) {
18.2. Программирование выражений в аргументах шаблонов 353 x[idx] = 1.2*x[idx] + x[idx]*y[idx]; } } На этот раз удалось обойтись без временных массивов и ограничиться в каждой итерации цикла всего двумя операциями считывания (элементов массивов x[idx] и y[idx]) и одной операцией записи в память (x[idx]). В результате для выполнения всего цикла требуется выполнить около 2000 считываний и 1000 записей в память. На современных высокопроизводительных компьютерах пропускная способность памяти является ограничивающим фактором, снижающим скорость выполнения операций с массивами. Поэтому неудивительно, если на практике окажется, что производительность запрограммированного "вручную" цикла на один-два порядка выше, чем производительность продемонстрированного только что подхода, при котором применяется такая простая перегрузка оператора. Однако желательно добиться такой же производительности более элегантным путем, избегая непонятных обозначений или громоздкого кода, в котором легко допустить ошибку. 18.2. Программирование выражений в аргументах шаблонов Чтобы добиться цели, сформулированной в конце предыдущего раздела, нужно попытаться вычислить выражение не по частям, а сразу (т.е. так, чтобы справа от оператора присвоения находилось все выражение). Таким образом, перед вычислением нужно записать, какая операция к какому объекту будет применяться. Эти операции определяются во время компиляции, поэтому они могут быть запрограммированы как аргументы шаблонов. Для выражения 1. 2*х + х*у;, рассматриваемого в качестве примера, это означает, что результат умножения 1.2 *х — это не новый массив, а объект, в котором каждое значение х умножается на 1.2. Аналогично, в результате операции х*у получается объект, в котором каждый элемент массива х умножается на соответствующий элемент массива у. И наконец, когда нам понадобятся значения результирующего массива, выполняются все^помянутые (и отложенные) действия. Рассмотрим конкретную реализацию сформулированной выше программы действий. В процессе этой реализации выражение 1.2*х + х*у; преобразуется в объект такого типа: A_Add< A_Mult<A_Scalar<double>,Array<double> >, A_Mult<Array<double>,Array<double> > > В приведенных строках кода новый фундаментальный шаблон класса Array комбинируется с шаблонами классов A_Scalar, A_Add и A_Mult. Сопоставьте расположение имен Шаблонов с синтаксическим деревом, соответствующим рассматриваемому выражению (рис. 18.1). В этом вложенном шаблоне представлены типы участвующих в вычислении объектов и операции над ними. Шаблон скалярного типа A_Scalar будет приведен ниже; по сути, это просто "заполнитель" для фигурирующего в выражении скаляра.
354 Глаза 18. Шаблоны выражений Рис. 18.1. Синтаксическое дерево выражения 1.2 *х+х*у 18.2.1. Операнды шаблонов выражений Чтобы придать завершенный вид приведенному выше представлению выражения, необходимо создать ссылки на каждый из объектов классов A_Add и A_Mult, а также записать значение в объект класса A_Scalar (или ссылку на это значение). Ниже приведен пример определения соответствующих операндов. // exprtmpl/expropsl.hpp #include <stddef.h> #include <cassert> // Включение вспомогательного шаблона, в котором выбирается // вид передачи "узла шаблона выражения" — по значению или //по ссылке #include "expropsla.hpp" // Класс, представляющий сложение двух операндов template <typename T, typename OP1, typename 0P2> class A_Add { private: typename A__Traits<OPl>: :ExprRef opl; // Первый операнд typename A_Traits<0P2>: :ExprRef op2; // Второй операнд public: // Конструктор, в котором инициализируются // ссылки на операнды A_Add(OPl constfc a, 0P2 constfc b) : opl(a), op2(b) { } // Вычисление суммы по требованию Т operator[] (size_t idx) const { return opl[idx] + op2[idx]; } // size — максимальный размер size_t size() const {
18.2. Программирование выражений в аргументах шаблонов 355 assert (opl.size()==0 |[ ор2.size()==0 || opl.size()==op2.size()); return opl.size()1=0 ? opl.size() : op2.size(); } }; // Класс, представляющий умножение двух операндов template <typename T, typename OP1, typename OP2> class A_Mult { private: typename A_Traits<OPl>::ExprRef opl; // Первый операнд typename AJTraits<OP2>::ExprRef op2; // Второй операнд public: // Конструктор, в котором инициализируются // ссылки на операнды A_Mult (OP1 constfc a, OP2 const& b) : opl(a), op2(b) { } // Вычисление произведения по требованию Т operator[] (size_t idx) const { return opl[idx] * op2[idx]; } // size — максимальный размер size_t size() const { assert (opl.size()==0 || op2.size()==0 || opl.size()==op2.size()); return opl.size()!=0 ? opl.size() : op2.size(); } }; Как видите, здесь добавлены операция индексации и операция получения размера. Это позволит определять тип массива и значения его элементов, что понадобится в процессе выполнения операций, представленных "узлами" того поддерева, корнем которого является данный объект. Если в операции принимают участие только массивы, то размер результирующего объекта равен размеру любого из двух операндов. Если же операция выполняется над массивом и скаляром, в результате получим объект, размер которого равен операнду- массиву. Чтобы отличить операнд-массив от скалярного операнда, размер последнего задан равным нулю. Поэтому шаблон A_Scalar определяется так, как показано ниже. // exprtmpl/exprscalar.hpp // Класс объектов, представляющих скаляры template <typename T>
356 Глава 18. Шаблоны выражений class A_Scalar { private: Т const& s; // Значение скаляра public: // Конструктор с инициализирующим значением A_Scalar (T const& v) : s(v) { } // При выполнении операции взятия индекса значение // скаляра является значением каждого элемента Т operator [] (size__t) const { return s; } // Размер скаляра равен нулю size_t size() const { return 0; } }; Заметим, что наши скаляры допускают использование операции индексирования. Скаляр при этом можно рассматривать как массив с одинаковыми значениями всех элементов, равных значению скаляра. Вероятно, вы обратили внимание, что в классах операторов для определения операндов-членов применяется вспомогательный класс A_Traits. typename A__Traits<OPl>: :ExprRef opl; // Первый операнд typename A_Traits<OP2>::ExprRef op2; // Второй операнд Это необходимо по следующей причине: в общем случае операнды можно объявить как ссылки, поскольку большинство временных узлов иерархической структуры вычисляемого вьфажения связаны с выражением верхнего уровня и потому остаются в памяти до завершения вычисления всего выражения. Единственным исключением являются узлы AJScalar. Они фигурируют в операторных функциях и могут не оставаться в памяти до завершения вычисления всего вьфажения. Таким образом, чтобы избежать ссылок на уже не существующие скалярные объекты, скалярные операнды должны быть скопированы "по значению". Другими словами, нужны операнды-члены, обладающие такими свойствами: • в общем случае'они являются ссылками на константы: ОР1 const& opl; // Ссылка на первый операнд ОР2 const& op2; // Ссылка на второй операнд • но для скаляров это обычные значения: ОР1 opl; // Ссылка на первый операнд по значению ОР2 6р2; // Ссылка на второй операнд по значению
18.2. Программирование выражений в аргументах шаблонов 357 Здесь отлично применяются классы свойств. В классе свойств тип в общем случае определяется как ссылка на константу, а в случае скаляра — как обычное значение. // exprtmpl/expropsla.hpp /* Вспомогательный класс свойств, помогающий выбрать способ обращения к "узловому" объекту шаблона выражения - в общем случае — по ссылке; - для скаляров — по значению */ template <typename T> class A_Scalar; // Первичный шаблон template <typename T> class A_Traits { public: typedef T constfc ExprRef; // Вид обращения - // ссылка на константу }; // Специализация шаблона для скаляра template <typename T> class A_Traits<A_Scalar<T> > { public: typedef A_Scalar<T> ExprRef; // Вид обращения - // обычное значение }; 18.2.2. Тип Array Теперь, когда вы умеете кодировать выражения с использованием шаблонов, необходимо создать тип Array, который бы занимался реальным распределением памяти и располагал информацией о шаблонах выражений. Конечно, с инженерной точки зрения было бы хорошо, если бы его интерфейс был как можно более похож на интерфейс обычного массива и чтобы имелся интерфейс для представления выражений, которые определяют результирующие значения массива. Для этого объявим шаблон Array следующим образом: template <typename T, typename Rep = SArray<T> > class Array; Тип Rep может быть типом SArray, если Array представляет собой обычный массив , либо быть вложенным идентификатором шаблона наподобие A_Add или AJMult, который Здесь удобно было бы повторно использовать разработанный ранее класс SArray, но в библиотеках промышленного назначения может быть предпочтительнее целевая реализация, в которой все особенности класса SArray не нужны.
358 Глава 18. Шаблоны выражений кодирует выражение. В любом случае мы имеем дело с инстанцированием класса Array, что значительно упрощает дальнейшие действия. Фактически, чтобы различить два этих случая, даже не нужны специализации определений шаблона Array, хотя при подстановке вместо Rep типов наподобие A_Mul t некоторые из членов не могут быть инстанцированы. Ниже приведено определение шаблона Array. Его функциональные возможности ограниченны и мало отличаются от функциональных возможностей шаблона SArray. Однако если принцип работы кода понятен, то расширение возможностей шаблона затруднений не представляет. // exprtmpl/exprarray.hpp #include <stddef.h> #include <cassert> #include "sarrayl.hpp" template <typename T, typename Rep = SArray<T> > class Array { private: Rep expr_rep; // Данные массива public: // Создание массива указанного размера explicit Array(size_t s) : expr_rep (s) { } // Создание массива из возможного представления Array (Rep constfc rb) : expr_rep(rb) { } // Оператор присвоения объекта того же типа Array& operator^ (Array constfc b) { assert(size()==b.size()); for(size_t idx = 0; idx < b.sizeO; ++idx) { expr__rep[idx] = b[idx]; } return *this; } // Оператор присвоения для массивов отличающегося типа template<typename Т2, typename Rep2> Arrayfc operator = (Array<T2, Rep2> constfc b) { assert(size()==b.size()); for (size_t idx = 0; idx < b.sizeO; ++idx) { expr_rep[idx] = b[idx]; } return *this;
18.2. Программирование выражений в аргументах шаблонов 359 } // size - размер представленных данных size_t size() const { return expr_rep.size(); } // Оператор индекса для констант и переменных Т operator[] (size_t idx) const { assert(idx<size() ); return expr_rep[idx]; } T& operator[] (size_t idx) { assert(idx<size()); return expr_rep[idx]; } // Возврат текущего представления массива Rep const& rep() const { return expr__rep; } Rep& rep() { return expr__rep; } }; Как видно из представленного фрагмента кода, многие операции просто передаются в соответствующий объект класса Rep. Однако при копировании другого массива необходимо принимать во внимание, что этот другой массив на самом деле может быть создан на основе шаблона выражения. Таким образом, эта операция копирования параметризуется в терминах представления, лежащего в основе типа Rep. 18.2.3. Операторы Теперь у нас есть почти все, что необходимо для реализации эффективных числовых операторов при работе с шаблоном Array, за исключением самих операторов. Как уже отмечалось, эти операторы только собирают в единое выражение шаблонные объекты, сами результирующие массивы они не вычисляют. Необходимо реализовать по три версии каждого обычного бинарного оператора для таких пар операндов: массив-массив, массив-скаляр и скаляр-массив. В частности, чтобы получить возможность вычислять выражение, приведенное в качестве примера в начале данной главы, понадобятся такие операторы: // exprtmpl/exprops2.hpp // Сложение двух объектов класса Array template <typename Т, typename Rl, typename R2> Array<T,A_Add<T/Ri/R2> >
360 Глава 18. Шаблоны выражений operator+ (Array<T,Rl> const& a, Array<T,R2> const& b) { return Array<T,A_Add<T,Rl,R2> > (A_Add<T,Rl,R2>(a.rep() ,b.rep() ) ) ; } // Перемножение двух объектов класса Array template <typename T, typename Rl, typename R2> Array<T, A_Mult<T,Rl,R2> > operator* (Array<T,Rl> const& a, Array<T,R2> constfc b) { return Array<T,A_Mult<T,Rl,R2> > (A_Mult<T,Rl,R2>(a.rep() , b.repO)); } // Умножение скаляра на объект типа Array template <typename T, typename R2> Array<T, A_Mult<T,A_Scalar<T>,R2> > operator* (T constfc s, Array<T,R2> constfc b) { return Array<T,A_Mult<T,A_Scalar<T>,R2> > (A_Mult<T,A_Scalar<T>,.R2>(A_Scalar<T>(s) ,b.rep() ) ) ; } // Умножение объекта типа Array на скаляр // Сложение скаляра и объекта типа Array // Сложение объекта типа Array и скаляра //... Как видно из рассмотренных примеров, объявление этих операторов является громоздким, однако нельзя сказать, чтобы в самих функциях выполнялось много действий. Например, в операторе суммирования двух массивов сначала создается объект класса A_Add<>, представляющий этот оператор и операнды A_Add<T, Rl, R2> (a.rep () , b. rep () ) Затем этот объект помещается в объект класса Array. После этого результат можно использовать, как любой другой объект, с помощью которого представляются содержащиеся в массиве данные: return Array<T/A__Add<T,Rl/R2> > (...); При вычислении скалярного произведения для создания объекта класса A_Mult применяется шаблон A_Scalar: A_Mult<T/Ascalar<T>/R2>(A_Scalar<T>(s), b.repO) После этого результат снова помещается в объект класса Array: return Array<T,A_Mult<T,A_Scalar<T>/R2> > (...); Другие бинарные операторы настолько похожи друг на друга, что для большинства из них можно применить макросы с небольшим количеством исходного кода. С помощью других макросов можно реализовать унарные операторы.
18.2. Программирование выражений в аргументах шаблонов 361 18.2.4. Подведем итог При первом знакомстве с понятием шаблонов выражений сложная взаимосвязь различных объявлений и определений может отпугивать. Поэтому проведем нисходящий обзор процессов, которые происходят при работе рассматриваемого примера. Надеемся, что это поможет вам получить целостное понимание принципа работы подобных программ. Анализируемый код выглядит следующим образом (его можно найти в файле meta/exprmain. cpp на прилагаемом компакт-диске): int main () { Array<double> х(1000), y(1000); x = 1.2*x + x*y; } Поскольку в объявлении объектов х и у аргумент Rep опущен, по умолчанию принимается, что это объекты класса SArray<double>. Таким образом, х и у— это массивы, в которых хранятся значения, а не записи операций. При анализе выражения 1.2*х + х*у компилятор сначала обращается к левой операции *, операндами которой являются скаляр и массив. Поэтому в процессе разрешения перегрузки оператора умножения выбирается такая его форма: template <typename Т, typename R2> Array<T, A_Mult<T,A_Scalar<T>,R2> > operator* (T const& s, Array<T,R2> const& b) { return Array<T,A_Mult<T,A_Scalar<T>,R2> > (A_Mult<T,A_Scalar<T>,R2>(A_Scalar<T>(s) , b.repO ) ) ; } Типы операндов — double и Array<double, SArray<double> >, следовательно, тип результата будет таким: Array<double, A_Mult<double, A_Scalar<double>, SArray<double> > > Результирующее значение использует объект A_Scalar<double>, созданный из значения 1.2 типа double, и представление SArray<double> объекта х. Затем компилятор переходит к обработке операции умножения х*у, операндами которой являются два массива. Для этого применяется соответствующая форма перегруженного оператора operator*. template <typename T, typename Rl, typename R2> Array<T, A__Mult<T,Rl,R2> > operator* (Array<T,Rl> const& a, Array<T,R2> const& b) { return Array<T,A_jyiult<T,Rl,R2> >
362 Глава 18. Шаблоны выражений (Ajy[ult<T,Rl,R2>(a.rep() , b.repO ) ) ; } Оба операнда имеют тип Array<double, SArray<double> >, поэтому тип результата следующий: Array<double, AJMult<double, SArray<double>, SArray<double> > > На этот раз объект A_Mult использует два представления класса SArray<double>: одно для массива х, второе для массива у. Наконец, выполняется операция +. Ее операндами, как и операндами предыдущей операции, являются два массива, типы которых получены в результате предыдущих операций. Таким образом, к паре массивов применяется оператор +'такого вида: template <typename Т, typename Rl, typename R2> Array<T, A_Add<T, Rl, R2 > > operators- (Array<T,Rl> constfc a, Array<T,R2> const& b) { return Array<T,A_Add<T,Rl,R2> > (A_Add<T,Rl,R2>(a.rep(),b.rep())); } Вместо параметра типа Т подставляется тип double, вместо R1 — тип A__Mult<double, A_J3calar<double>, SArray<double> > и вместо R2 — тип A_Mult<double, SArray<double>/ SArray<double> > Таким образом, справа от знака равенства стоит выражение типа Array<double, A_Add<double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<dpuble> > > > Этот тип соответствует шаблону оператора присвоения из шаблона Array. template <typename T, typename Rep = SArray<T> > class Array { public: // Оператор присвоения для массивов разных типов template<typename T2, typename Rep2> Array& operator= (Array<T2, Rep2> const& b) { assert(size()==b.size()); for (size_t idx = 0; idx<b.size(); ++idx) { expr_rep[idx] = b[idx]; } return *this; }
18,2. Программирование вьфажений в аргументах шаблонов 363 }; В результате выполнения оператора присвоения вычисляется каждый элемент находящегося слева массива х. При этом оператор взятия индекса применяется к представлению правой части, тип которой приведен ниже. A__Add< double, A_Mult<double, A_Scalar<double>, SArray<double> >, A_Mult<double, SArray<double>, SArray<double> > > > Детальный разбор результатов работы этого оператора показывает, что для данного индекса idx он вычисляет выражение (1. 2*х [idx]) + (х [idx] *у [idx]), а это именно то, что нам нужно. 18.2.5. Присвоение шаблонов выражений Для элемента массива с аргументом Rep, который в рассматриваемом примере входит в шаблоны вьфажений A_Mult и A_Add, невозможно инстанцировать операции записи (в самом деле, выражение а+b = с не имеет смысла). Однако вполне допустима операция записи шаблонов других вьфажений, если в результате получается объект, который может стоять слева от оператора присвоения. Например, если массив состоит из целочисленных элементов, то он может играть роль индекса другого массива. Другими словами, выражение х[у] = 2*х[у] ; должно означать for(size_t idx = 0; idx < y.sizeO; ++idx) { x[y[idx]] = 2*x[y[idx]]; } Чтобы можно было реализовать подобную операцию, в основании класса Array должен находиться шаблон выражения, который ведет себя как lvalue (т.е. ссылается на ячейку памяти, в которую можно произвести запись). Составные части такого шаблона выражения принципиально не отличаются, например, от шаблона A_Mult. Единственное важное отличие состоит в том, что для операторов индексации реализуется версия с модификатором const и без него, и что эти операторы возвращают ссылки на lvalue. // exprtmpl/exprops3.hpp template<typename T, typename Al, typename A2> class A_Subscript { public: // Конструктор, инициализирующий ссылки на операнды A__Subscript (Al constfc a, A2 constfc b) : al(a), a2(b) { } // Обработка индексации при запросе значения
364 Глава 18. Шаблоны выражений Т operator[] (size_t idx) const { return al[a2[idx]]; } T& operator!] (size_t idx) { return al[a2[idx]]; } // Размер внутреннего массива size_t size() const { return a2.size(); } private: Al const & al; A2 const & a2; }; Для реализации предложенного ранее расширения оператора, возвращающего элемент массива по индексу, потребуется добавить в шаблон класса Array дополнительные операторы взятия индекса. Один из этих операторов можно определить как показано ниже (по-видимому, понадобится также его версия с модификатором const). // exprtmpl/exprops4.hpp template<typename T, typename Rl, typename R2> Array<T,A_Subscript<T,Rl,R2> > Array<T,Rl>::operator[] (Array<T,R2> const& b) { return Array<T,A_Subscript<T,Rl,R2> > (A_Subscript<T,Rl,R2>(a.rep(),b.rep())); } 18.3. Производительность и ограничения шаблонов выражений Чтобы убедить читателя в оправданности идеи о шаблонах выражений, в этой главе был приведен тот аргумент, что такие шаблоны позволяют значительно повысить производительность выполнения операций с массивами. Если проследить за обработкой шаблонов выражений, то можно обнаружить, что в них множество встроенных функций вызывают друг друга, и что при этом в стеке размещается множество шаблонов объектов. Чтобы получился код, работающий так же хорошо, как написанный вручную, оптимизатор должен выполнять полное встраивание и исключение небольших объектов. Во время написания данной книги редко встречались компиляторы C++, способные выполнять эти операции. Метод шаблонных выражений не решает всех проблем, возникающих при выполнении численных операций с массивами. Например, он не работает при умножении матрицы на вектор, записанном в виде х = А*х; // Ссылка на 1-й операнд // Ссылка на 2-й операнд
18.4. Заключение 365 В приведенной инструкции х — это вектор-столбец, а А — квадратная матрица пхп. Без временного массива здесь не обойтись, поскольку каждый элемент результирующего массива зависит от каждого элемента исходного массива. К сожалению, в цикле, заданном в шаблоне выражения, первый элемент массива х тут же изменится, и в вычислении последующих значений этого массива будет участвовать уже не исходное, а измененное значение х [ 0 ], что приведет к неправильному результату. С другой стороны, для вычисления несколько отличающегося выражения х = А*у; временный массив не нужен, если только переменные х и у не указывают на один и тот же объект. Это означает, что для выполнения такой операции во время выполнения работы программы понадобились бы сведения о том, в каких взаимоотношениях находятся операнды. Для этого создается динамическая структура, представляющая древовидную схему выражения, что является альтернативой шаблонам выражений. Впервые такой подход был применен Робертом Дэвисом (Robert Davies) в библиотеке NewMat [28] задолго до того, как были придуманы шаблоны выражений. Область применения шаблонов выражений не ограничивается только численными расчетами. Их увлекательное использование было предложено, например, Яакко Ярви (Jaakko Jarvi) и Гэри Пауэллом (Gary Powell) в библиотеке Lambda Library [20]. В ней функциональные объекты стандартной библиотеки используются как объекты- выражения. Например, благодаря этой библиотеке можно записать такой код: void lambda_demo(std::vector<long*>& ones) { std: :sort (ones.begin() , ones.endO, *_1 > *_2); В приведенном фрагменте кода массив сортируется в порядке возрастания тех значений, на которые ссылаются его элементы. Если бы не было библиотеки Lambda, пришлось бы создавать несложный, но громоздкий функциональный тип. Однако благодаря библиотеке нужные операторы можно составлять с помощью простого встроенного синтаксиса. В рассмотренном примере _1 и J2 — это шаблоны-заполнители, предоставляемые библиотекой Lambda. Они соответствуют элементарным объектам-выражениям, которые также являются функторами. С помощью методов, рассмотренных в данной главе, из этих элементарных объектов можно составлять более сложные выражения. 18.4. Заключение Шаблоны выражений были изобретены независимо Тоддом Вельдхаузеном (Todd Veldhuizen), благодаря которому и появился этот термин, и Дэвидом Вандевурдом (David Vandevoorde). Это произошло, когда в языке программирования C++ еще не было шаблонов членов (в то время казалось, что они никогда не будут добавлены в C++). Из-за этого возникали некоторые проблемы, связанные с реализацией оператора присвоения: он не мог быть параметризован для шаблонов выражений. Один из способов обойти эту проблему состоял в том, чтобы ввести в шаблон выражения оператор преобразования к классу Copier, параметризованному шаблоном выражения, но унаследованному от ба-
366 Глава 18. Шаблоны выражений зового основного класса, параметризованного только типом элемента. Далее этот базовый класс предоставлял (виртуальный) интерфейс copy_to, на который мог ссылаться оператор присвоения. Ниже этот механизм представлен схематически (имена шаблонов совпадают с использованными в данной главе). template<typename T> class Copierlnterfасе {, public: virtual void copy_to(Array<T, SArray<T> >&) const; }; template<typename T, typename X> class Copier : public CopierBase<T> { public: Copier(X const &x): expr(x) {} virtual void copy__to(Array<T, SArray<T> >&) const { // Реализация цикла присвоения } private: X const &expr; }; template<typename T, typename Rep = SArray<T> > class Array { public: // Делегированный оператор присвоения Array<T, Rep>& operator=(CopierBase<T> const &b) { b.copy_to(rep); }; }; template<typename T, typename Al, typename A2> class A_mult { public: operator Coppier<T, A_Mult<T, Al, A2> >(); }; При этом в программу добавляется дополнительный уровень сложности, а на обработку шаблонов выражений во время выполнения программы затрачиваются определенные дополнительные ресурсы. Однако даже с учетом этих замечаний производительность полученных программ была на то время впечатляющей. В стандартной библиотеке C++ содержится шаблон класса valarray; предполагается, что он будет полезен в приложениях, в которых применяются методы, использованные в данной главе для разработки шаблона Array. Предшественник шаблона valarray был разра-
18.4. Заключение 367 ботан с учетом того, что на рынке программных продуктов появятся компиляторы, предназначенные для научных расчетов, которые смогут распознать этот тип и преобразовать использующую его программу в высокопроизводительный машинный код. Такие компиляторы должны были обладать способностью эффективно работать с этим типом, однако они так и не появились (частично из-за того, что рынок программных продуктов в области научных расчетов занимает сравнительно небольшую долю, а частично из-за того, что после реализации класса valariray в виде шаблона проблема усложнилась). После изобретения метода шаблонов выражений один из его авторов Дэвид Вандевурд (David Vandevoorde) подал в Комитет по стандартизации C++ предложение преобразовать класс valarray в шаблон Array, рассмотренный в данной главе (конечно, доработанный с тем, чтобы придать ему все функциональные возможности класса valarray). Это предложение было первым, в котором планировалось документировать концепцию параметра Rep. До этого фактические массивы, хранящие реальные данные, и псевдомассивы, предназначенные дня реализации шаблонов выражений, были разными массивами. Например, в коде пользователя вводилась функция f оо (), аргументом которой является объект класса Array: double foo(Array<double> const&); Тогда ее вызов в виде f оо (1.2 *х) приводил к преобразованию шаблона выражения в реальный массив с фактическими данными, даже если для выполнения операций с аргументами временный массив не требуется. Если же в нашем распоряжении есть аргумент Rep, в который можно поместить шаблон выражения, то функцию f оо () можно объявить так: template<typename R> double foo(Array<double, R> const&); При этом не будет выполняться никаких преобразований, в которых нет необходимости. Предложение о модификации класса valarray поступило в то время, когда процесс стандартизации C++ уже шел полным ходом. Чтобы его принять, пришлось бы переписать весь текст документации, касающийся класса valarray. В результате предложение было отклонено, а в документацию были внесены некоторые исправления, позволяющие описывать классы, основанные на шаблонах выражений. Однако практическое использование этих возможностей намного сложнее, чем рассмотренное в данной главе. На момент написания книги не было известно ни одной реализации этих возможностей, а стандартный класс valarray, вообще говоря, довольно неэффективен при выполнении тех операций, для которых он разработан. Наконец, следует упомянуть, что многие новаторские подходы, рассмотренные в настоящей главе, а также те, что вошли в библиотеку STL2, имеют одну общую особенность. Все они были первоначально реализованы на одном и том же компиляторе — версии 4 компилятора Borland C++. Он был, вероятно, первым компилятором, благодаря которому шаблонное программирование стало широко распространенным среди разработчиков, пользующихся C++. 2 STL (Standard Template Library), или стандартная библиотека шаблонов, кардинально изменила представления о библиотеках C++; позже она вошла в стандартную библиотеку C++ [18].
Часть IV Нетрадиционное использование шаблонов Шаблоны могут использоваться для разработки тщательно продуманных библиотек элементов, естественным путем объединяемых в одно целое. То же достигается и при разработке нешаблонных библиотек. Однако когда речь идет о простых приложениях, упрощающих обычное программирование, традиционные процедурные или объектно- ориентированные библиотеки не всегда оказываются оптимальным решением из-за непропорционально больших накладных расходов. Препроцессор С позволяет помочь в ряде подобных случаев, но он далеко не всегда адекватен стоящей перед программистом задаче. В этой части рассматриваются некоторые задачи, идеальным средством решения которых оказываются шаблоны. • Схема классификации типов • Интеллектуальные указатели • Кортежи • Функторы Наша цель — продемонстрировать применение обсуждавшихся ранее технологий на практике. Мы будем комбинировать и модифицировать их для создания полезных, имеющих широкое практическое применение программных компонентов. Тем не менее основная тема остается прежней— шаблоны C++, а не, например, разработка какой-то библиотеки. Поэтому надеемся, что представленный нами код послужит учебным руководством для создателей библиотек, но ни в коем случае не претендуем на то, что этот код — наилучшее решение поставленных задач.
Глава 19 Классификация типов Порой весьма полезной может оказаться возможность определения того, является ли параметр шаблона встроенным типом, указателем, классом и т.д. В следующих разделах описана разработка шаблона общего назначения, который позволит определять различные свойства передаваемого ему типа. В результате можно написать код, например, следующего вида: if (TypeT<T>::IsPtrT) { } else if (TypeT<T>::IsClassT) { } Более того, выражения наподобие ТуреТ<Т>: -.IsPtrT будут представлять собой логические константы— корректные, не являющиеся типами аргументы шаблонов. В свою очередь, это обеспечит построение более сложных и мощных шаблонов, поведение которых зависит от свойств их аргументов типов. 19.1. Определение фундаментальных типов Прежде всего разработаем шаблон для определения того, является ли передаваемый тип фундаментальным типом языка программирования. Считая, что по умолчанию тип не является фундаментальным, специализируем шаблон для фундаментальных типов. // types/typel.hpp // Основной шаблон: Т не является фундаментальным типом tempiate<typename T> class IsFundaT { public: enum { Yes = 0, No = 1 }; };
372 Глава 19. Классификация типов // Макрос для специализации для базового типа #define MK_FUNDA_TYPE(T) \ templateo class IsFundaT<T> { \ public: \ enum { Yes = 1, No = 0 }; \ }; MK_.FUNDA_.TYPE (void) MK_FUNDA_TYPE(bool) MK_FUNDA_TYPE(char) MK_FUNDA_TYPE(signed char) MK_FUNDA_TYPE(unsigned char) MK_.FUNDA.TYPE (wchar_t) MK_FUNDA_TYPE(signed short) MK_FUNDA_TYPE(unsigned short) MK_FUNDA_TYPE(signed int) MK_FUNDA_TYPE(unsigned int) MK_FUNDA_TYPE(signed long) MK_FUNDA_TYPE(unsigned long) #if LONGLONG_EXISTS MK_FUNDA_TYPE(signed long long) MK_FUNDA_TYPE(unsigned long long) #endif // LONGLONG_EXISTS MK_FUNDAJTYPE(float) MK_JUNDA_TYPE(double) MK__FUNDA_TYPE(long double) #undef MK_FUNDA_TYPE Первый шаблон определяет общий случай, т.е. в общем случае IsFundaT<T>: : Yes равно 0 (или false). template<typename T> class IsFundaT { public: enum { Yes = 0, No = 1 }; }; Для каждого базового типа определена слециализация таким образом, что IsFundaT<T>: :Yes равно 1 (или true). Это реализуется с помощью макроса, разворачивающегося в необходимый код. Например, MK_FUNDA_TYPE(bool) разворачивается в
19.2. Определение составных типов 373 templateo class IsFundaT<bool> { public: enum { Yes = 1,•No = 0 }; }; Далее приведена демонстрационная программа, использующая этот шаблон. // types/typeltest.cpp #include <iostream> #include "typel.hpp" template <typename T> void test(T const& t) { if (IsFundaT<T>::Yes) { std::cout « "T - фундаментальный тип" « std::endl; } else { std::cout « "T - не фундаментальный тип" « std::endl; } } class MyType { }; int main() { test(7); test(MyType()); } При запуске этой программы будет получен следующий вывод: Т - фундаментальный тип Т - не фундаментальный тип су— Точно так же можно определить функции IsIntegralT и IsFloatingT для выяснения того, является ли некоторый тип интегральным скалярным типом или скалярным типом с плавающей точкой. 19.2. Определение составных типов Типы, сконструированные из других типов, называются составными. Простейшие составные типы включают, например, простые типы, указатели, ссылки и массивы. Все они построены на основе одного базового типа. Классы и функции также являются состав-
374 Глава 19. Классификация типов ными типами, но они могут включать несколько фундаментальных типов (в качестве параметров или членов). Простые составные типы можно классифицировать с помощью частичной специализации. Начнем с обобщенного определения класса свойств, описывающего составные типы, которые не являются классами и перечислениями (о них речь идет несколько позже). // types/type2.hpp template<typename T> class CompoundT { // Первичный шаблон public: enum { IsPtr = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; }; Тип-член BaseT является синонимом для непосредственного типа, представляющего собой тип, на основе которого строится параметр шаблона. BottomT — окончательный тип, не являющийся ссылкой, указателем или массивом, на основании которого построен тип Т. Так, если Т представляет собой int**, то BaseT является синонимом int*, a BottomT — синонимом int. Для случая указателя на член BaseT представляет собой тип члена, a ClassT— класс, которому этот член принадлежит. Например, если Т — указатель на функцию-член типа irit(X: :*) О, то BaseT является типом функции int (), a ClassT соответствует X. Если Т не является указателем на член, то ClassT представляет собой Compound<void> (этот выбор произволен: вы можете предпочесть использовать не класс). Частичная специализация для указателей и ссылок достаточно проста. // types/type3.hpp template<typename T> class CompoundT<T&> { // Частичная специализация для ссылок public: enum { IsPtr = 0, IsRefT = 1, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T*> { // Частичная специализация // для указателей public: enum { IsPtr = 1, IsRefT = 0, IsArrayT = 0,
19.2. Определение составных типов 375 IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; Массивы и указатели на члены могут рассматриваться с использованием той же методики, но для вас может оказаться неожиданностью то, что частичная специализация включает большее количество параметров, чем исходный шаблон. // types/type4.hpp #include <stddef.h> template<typename T, size__t N> class CompoundT<T[N]> { // Частичная специализация // для массивов public: enum { IsPtr = 0, IsRefT = 0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T> class CompoundT<T[]> { // Частичная специализация // для пустых массивов public: enum { IsPtr = 0, IsRefT =0, IsArrayT = 1, IsFuncT = 0, IsPtrMemT = 0 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef CompoundT<void> ClassT; }; template<typename T, typename C> class CompoundT<T C::*> { // Частичная специализация // для указателей на член public: enum { IsPtr = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 0, IsPtrMemT = 1 }; typedef T BaseT; typedef typename CompoundT<T>::BottomT BottomT; typedef С ClassT; };
376 Глава 19. Классификация типов Внимательный читатель должен был заметить, что определение члена BottomT требует рекурсивного инстанцирования шаблона CompoundT для различных типов Т. Рекурсия завершается, когда Т больше не является составным типом и используется определение обобщенного шаблона (либо когда Т представляет собой тип функции, как вы увидите позже). Распознать функции гораздо сложнее. В следующем разделе для этого используются более "продвинутые" методы. 19.3. Определение типов функций Связанная с определением типов функций проблема состоит в том, что в силу произвольного количества параметров не существует конечной синтаксической конструкции, описывающей их все. Один из подходов к решению этой задачи — частичные специализации для функций со списком аргументов, который короче заданного ограничения. Приведем несколько первых таких специализаций. // types/type5.hpp template<typename R> class CompoundT<R()> { public: enum { IsPtr = 0, IsRefT IsFuncT = 1, IsPtrMemT typedef R BaseTO; typedef R BottomTO; typedef CompoundT<void> ClassT; }; template<typename R, typename Pl> class CompoundT<R(PI)> { public: enum { IsPtr = 0, IsRefT IsFuncT = 1, IsPtrMemT typedef R BaseT(Pl); typedef R BottomT(PI); typedef CompoundT<void> ClassT; }; template<typename R, typename Pl> class CompoundT<R(Pl,...)> { public: enum { IsPtr = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = 1, IsPtrMemT = 0 }; typedef R BaseT(Pl); typedef R BottomT(PI); 0, IsArrayT = 0, 0 }; 0, IsArrayT = 0/ 0 };
19.3. Определение типов функций 377 typedef CompoundT<void> ClassT; }; Преимущество такого подхода в явном создании членов typedef для каждого типа параметра. Более обобщенный метод использует принцип SFINAE (substitution-failure-is-not-an- error — неверная подстановка не является ошибкой; см. раздел 8.3.1, стр. 129). Ключевым в использовании принципа SFINAE в данном случае является поиск конструкции, которая некорректна для функций, но корректна для всех иных типов (или наоборот). Поскольку вы уже можете распознавать ряд категорий типов, их можно исключить из рассмотрения. Таким образом, можно воспользоваться массивами, элементы которых не могут иметь тип void, быть ссылками или функциями. В результате появляется приведенный ниже код. template<typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template<typename U> static One test(...); template<typename U> static Two test(U(*)[1]); public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) == 1 }; enum { No = !Yes }; Ь , При использовании этого определения шаблона значение IsFunctionT<T>: :Yes будет ненулевым только для типов, которые не могут быть элементами массива. Единственный недостаток этого метода состоит в том, что это справедливо не только для функций, но и для ссылочных типов и типа void. К счастью, этот недостаток легко устраняется частичными специализациями для ссылочных типов и типа void. template<typename T> class IsFunctionT<T&> { public: enum { Yes = 0 } ; enum { No = !Yes }; }; templateo class IsFunctionT<void> { • public: enum { Yes = 0 }; enum { No = !Yes }; }; templateo
378 Глава 19. Классификация типов class IsFunctionT<void const> { public: enum { Yes = 0 }; enum { No = !Yes }; }; Существуют и другие способы решения. Например, можно воспользоваться уникальностью типа функции F, которая состоит в том, что ссылка F& неявно преобразуется к F* без наличия пользовательского преобразования. Рассмотренный выше пример приводит нас к окончательной версии кода. // types/type6.hpp template<typename T> class IsFunctionT { private: typedef char One; typedef struct { char a[2]; } Two; template<typename U> static One test (...); template<typename U> static Two test(U(*)[1]); public: enum { Yes = sizeof(IsFunctionT<T>::test<T>(0)) ==1 }; enum { No = !Yes }; }; template<typename T> class IsFunctionT<T&> { public: enum { Yes = 0 }; enum { No = !Yes }; }; templateo class IsFunctionT<void> { public: enum { Yes = 0 }; enum { No = !Yes }; }; templateo class IsFunctionT<void const> { public: enum { Yes « 0 }; enum { No =t !Yes }; ) i
19.3. Определение типов функций 379 // То же для типов void volatile и void const volatile template<typename T> class CompoundT { // Первичный шаблон public: enum { IsPtr = 0, IsRefT = 0, IsArrayT = 0, IsFuncT = IsFunctionT<T>::Yes, IsPtrMemT = 0 }; typedef T BaseT; typedef T BottomT; typedef CompoundT<void> ClassT; }; Реализация первичного шаблона не исключает предложенных ранее специализаций, так что для ограниченного количества параметров можно получить типы параметров функций и типы возвращаемых ими значений. Существует одна интересная историческая альтернатива, основанная на том, что в определенный момент развития C++ конструкция template<class T> struct X { long aligner; Т m; }; могла объявлять функцию-член X: : m () вместо нестатического члена-данных X: : га (в настоящее время это не так в соответствии со стандартом C++). Во всех реализациях того времени размер Х<Т> не превышал размер типа Х0, если тип Т представлял собой функцию (так как невиртуальные функции-члены на практике не увеличивают размер класса). struct X0 { long aligner; } С другой стороны, если Т — тип некоторого объекта, то размер Х<Т> должен быть больше размера Х0 (член aligner требуется постольку, поскольку размер пустого класса обычно равен размеру класса с членом-данными типа char). Теперь можно, объединив рассмотренные методы, классифицировать все типы, за исключением классов и перечислений. Если тип не является фундаментальным или одним из типов, распознаваемых шаблоном CompoundT, он должен быть либо перечислением, либо классом. В следующем разделе рассматривается применение разрешения перегрузки для того, чтобы уметь различать эти два случая.
380 Глава 19. Классификация типов 19.4. Классификация перечислений с помощью разрешения перегрузки Разрешение перегрузки представляет собой процесс, который среди различных функций с одним и тем же именем выбирает необходимую на основе информации о типах ее аргументов. Как вскоре будет показано, разрешение перегрузки может быть выполнено без реального вызова этой функции. Это очень полезно для проверки существования неявного преобразования типов. Интересующее нас неявное преобразование типов — это преобразование перечисления в интегральный тип, которое позволяет идентифицировать перечисления. Ниже приведена реализация рассмотренного метода. // types/type7.hpp struct SizeOverOne { char с[2]; }; template<typename T, bool convert_possible = !CompoundT<T>::IsFuncT && !CompoundT<T>::IsArrayT> class ConsumeUDC { public: operator T() const; }; // Преобразование к типу функции невозможно template<typename T> class ConsumeUDC<T, false> { }; // Преобразование к типу void невозможно template<bool convert_possible> class ConsumeUDC<void/ convert_possible> { >; char enum_check(bool); char enum__check(char) ; char enum_check(signed char); char enum_check(unsigned char); char enum_check(wchar_.t) ; char enum_check(signed short); char enum_check(unsigned short); char enum_check(signed int); char enum_check(unsigned int); char enum_check(signed long); char enum_check(unsigned long);
19.4. Классификация перечислений с помощью разрешения перегрузки 381 #if LONGLONG_EXISTS char enum_check(signed long long); char enum_check(unsigned long long); #endif // LONGLONGJEXISTS // Устранение случайного преобразования float в int char enum__check (float) ; char enum__check (double) ; char enum__check(long double); SizeOverOne enum_check(...); // Все прочие случаи template<typename T> class IsEnumT { public: enum { Yes = IsFundaT<T>::No && !CompoundT<T>::IsRefT && !CompoundT<T>::IsPtr && !CompoundT<T>::IsPtrMemT && sizeof(enum_check(ConsumeUDC<T>()))==1 }; enum { No = !Yes }; }; Самое главное в приведенном исходном тексте— применение выражения sizeof к вызову функции. В результате мы получаем размер типа, возвращаемого данной функцией. Следовательно, для разрешения вызова enutn_check () применяются стандартные правила, но реального вызова функции не происходит, так что определения функций не являются необходимыми. В нашем случае функция enum__check () возвращает значение типа char, размер которого равен 1, если ее аргумент преобразуем в интегральный тип. Все другие типы приводят к вызову функции с троеточием, которое представляет собой самый нежелательный в аспекте разрешения перегрузки вызов. Тип, возвращаемый функцией enum_check () с троеточием, был специально создан таким образом, чтобы гарантировать, что его размер превышает один байт1. Следует очень аккуратно подойти к вопросу создания аргумента для функции enunv_check (). Прежде всего заметим, что мы не знаем, каким образом конструируется объект типа Т (возможно, для этого требуется вызов какого-то специального конструктора). Для решения этой проблемы можно объявить функцию, возвращающую тип Т и создать аргумент путем вызова этой функции. Поскольку этот вызов размещается внутри выражения sizeof, реального определения функции не требуется. Гораздо более тонким моментом является то, что разрешение перегрузки может выбрать объявление функции enum_check () для интегрального типа, если аргумент Т имеет тип клас- На практике вполне применим, например, тип double, но теоретически такой тип тоже может иметь размер, равный 1. Поскольку функция не может возвращать массив, нам пришлось "завернуть" его в структуру.
382 Глава 19. Классификация типов са, но этот класс определяет пользовательскую функцию преобразования (used-defined conversion — UDC) к интегральному типу. Эта проблема решается преобразованием к типу Т с использованием шаблона ConsumeUDC. Тип аргумента функции enum_check () получается путем использования оператора преобразования к типу Т. В результате выражение вызова enum_check () анализируется следующим образом (см. приложение Б, "Разрешение перегрузки"). • Исходный аргумент представляет собой временный объект ConsumeUDC<T>. • Если Т— фундаментальный интегральный тип, выбирается функция enum^check (), аргумент которой имеет этот тип. • Если Т — перечисление, то выполняется его преобразование в интегральный тип (обычно int) и выбирается соответствующая этому интегральному типу функция enum_check(). • Если Т представляет собой-тип класса с оператором приведения к интегральному типу, то этот оператор преобразования не играет никакой роли, поскольку при разрешении перегрузки может выполняться только одно пользовательское преобразование типа, и это преобразование типа ConsumeUDC<T> в Т. • Прочие типы Т невозможно сделать соответствующими интегральному типу, так что во всех остальных случаях выбирается версия enuni_check () с троеточием. Наконец, поскольку нас интересуют только перечислимые типы, но не фундаментальные типы или указатели, можно использовать разработанные ранее шаблоны Is- FundaT и CompoundT для исключения указанных типов из множества типов, для которых IsEnumT<T>: : Yes будет иметь ненулевое значение. 19.5. Определение типов классов После использования рассмотренных шаблонов для классификации типов остаются нераспознанными только типы, представляющие собой классы (class, struct и union). Одним из возможных подходов для распознавания является использование принципа SFI- NAE (см. раздел 15.2.2, стр. 293). Другой подход основан на исключении: если тип не является фундаментальным, перечислением или составным типом, то он должен быть типом класса. Ниже приведен шаблон, реализующий эту идею. // types/type8.hpp tempiate<typename T> class IsClassT { public: enum { Yes = IsFundaT<T>::No && IsEnumT<T>::No && !CompoundT<T>::IsPtrT &&
19.6. Окончательное решение 383 !CompoundT<T>::IRefT && !CompoundT<T>::IsArrayT && !CompoundT<T>::IsPtrMemT && !CompoundT<T>::IsFuncT }; enum { No = !Yes }; }; 19.6. Окончательное решение Теперь, когда мы способны классифицировать все возможные типы, имеет смысл сгруппировать все классифицирующие шаблоны в единый общий шаблон. Это делается с помощью относительно небольшого заголовочного файла. // types/typet.hpp #ifndef TYPET_HPP #define TYPET_HPP // Определение IsFundaTo #include "type1.hpp" // Определение первичного шаблона Compound<T> (первая версия) //ttinclude "type2.hpp" // Определение первичного шаблона Compound<T> (вторая версия) #include "typeб.hpp" // Определение 'специализаций Compound<T> #include "type3.hpp" ttinclude "type4.hpp" #include "type5.hpp" // Определение IsEnumTo #include "type7.hpp" // Определение IsClassTo #include "type8.hpp" // Определение шаблона для работы в одном стиле template<typename T> class TypeT { public: enum { IsFundaT = IsFundaT<T>::Yes, IsPtrT s CompoundT<T>::IsPtrT, IsRefT = CompoundT<T>::IsRefT, IsArrayT = CompoundT<T>::IsArrayT, IsFuncT = CompoundT<T>::IsFuncT, IsPtrMemT = CompoundT<T>::IsPtrMemT, IsEnumT = IsEnumT<T>::Yes,
384 Глава 19. Классификация типов IsClassT = IsClassT<T>::Yes }; }; #endif // TYPET_HPP Приведенная далее программа демонстрирует применение шаблонов классификации типов. // types/types.cpp #include "typet.hpp" #include <iostream> class MyClass { }; void myfuncO { } enum E { el }; // Проверка путем передачи типа в качестве аргумента шаблона template<typename T> void check() { if (TypeT<T>::IsFundaT) { std::cout « " IsFundaT "; } if (TypeT<T>::IsPtrT) { std::cout « " IsPtrT "; } if (TypeT<T>::IsRefT) { std::cout « " IsRefT "; } if (TypeT<T>::IsArrayT) { std::cout « " IsArrayT "; } if (TypeT<T>::IsFuncT) { std::cout « " IsFuncT "; } if (TypeT<T>::IsPtrMemT) { std::cout « " IsPtrMemT "; } if (TypeT<T>::IsEnumT) { std::cout « " IsEnumT "; } if (TypeT<T>::IsClassT) { std::cout « " IsClassT "; }
19.6. Окончательное решение 385 std::cout « std::endl; } // Проверка путем передачи типа как аргумента функции template<typename T> void checkT(T) { check<T>(); // Для указателей определяем, на что они указывают if (TypeT<T>::IsPtrT || ТуреТ<Т>::IsPtrMemT) { check<typename CompoundT<T>::BaseT>(); } } int main() { std::cout « "int:" « std::endl; check<int>(); std::cout « "int&:" « std::endl; check<int&>(); std::cout « "char[42]:H « std::endl; check<char[42]>(); std::cout « "MyClass:" « std::endl; check<MyClass>(); std::cout « "ptr to enum:" « std::endl; E*.ptr = 0; checkT(ptr); std::cout « "42:" « std::endl; checkT(42); std::cout « "myfunc():" « std::endl; checkT(myfunc); std::cout « "memptr to array:" « std::endl; char (MyGlass::*memptr)[] = 0; checkT(memptr); Приведем вывод данной программы. int: IsFundaT int&: IsRefT char[42]:
386 Глава 19. Классификация типов IsArrayT MyClass: IsClassT ptr to enum: IsPtrT IsEnumT 42: IsFundaT myfunc(): IsPtrT IsFuncT memptr to array: IsPtirMemT IsArrayT 19.7. Заключение Возможность программы проверять свои собственные высокоуровневые свойства (например, структуры типов) иногда именуют рефлексией (reflection). Таким образом, рассмотренная в этой главе схема представляет собой не что иное, как рефлексию времени компиляции — технологию, родственную метапрограммированию (см. главу 17, "Метапрограммы"). Идея хранения свойств типов в виде членов специализаций шаблонов датируется примерно срединой 1990-х годов. Среди ранних серьезных приложений шаблонов классификации типов следует упомянуть шаблон type_traits в реализации STL от SGI (ныне известной как Silicon Graphics). Шаблон SGI предназначался для цредставления некоторых свойств своего аргумента. Эта информация затем использовалась при оптимизации ряда алгоритмов STL для данного типа. Интересной особенностью решения SGI стало то, что некоторые компиляторы SGI распознавали специализации _type__traits и обеспечивали информацию о его аргументе, которую нельзя было вывести с помощью стандартных методов (обобщенная реализация шаблона -^-type_traits не оптимальна, но вполне безопасна при использовании). Возможность использования принципа SFINAE для классификации типов была отмечена при его уточнении в процессе стандартизации языка. Однако этот принцип никогда не был формально документирован, в результате позже была затрачена масса усилий на повторную разработку описанных в этой главе методик. Один из наиболее значительных вкладов был внесен Андреем Александреску (Andrei Alexandrescu), который использовал оператор sizeof для определения результата разрешения перегрузки. И наконец, следует отметить, что более полный шаблон классификации типов можно найти в библиотеке Boost [8]. В свою очередь, эта реализация служит основой для добавления возможностей такого рода в стандартную библиотеку (см. также раздел 13.10, стр. 243, о соответствующих языковых расширениях).
Глава 20 Интеллектуальные указатели Память представляет собой ресурс, с которым программы на C++ обычно работают явным образом. Эта работа включает в себя получение и освобождение блоков памяти. Один из наиболее тонких вопросов управления динамически выделенной памятью связан с тем, когда она должна быть освобождена. Среди различных инструментов, с помощью которых можно упростить этот аспект программирования, есть так называемые шаблоны интеллектуальных указателей (smart pointer). В C++ интеллектуальные указатели представляют собой классы, которые ведут себя подобно обычным указателям (в том смысле, что обеспечивают операторы разыменования - > и *), но при этом инкапсулируют некоторую память или политику управления ресурсами. В этой главе рассматривается разработка шаблонов интеллектуальных указателей, инкапсулирующих две различные модели владения — исключительную и разделяемую. • Исключительное владение может быть достигнуто с небольшими, по сравнению с использованием обычных указателей, накладными расходами. Интеллектуальные указатели, реализующие эту политику, полезны при генерации исключений во время работы с динамически выделенными объектами. • Разделяемое владение может иногда приводить к исключительно сложным ситуациям, связанным со временем жизни объекта. В таких случаях может оказаться разумным передать решение вопросов времени жизни объектов от программиста программе. Термин интеллектуальный указатель означает, что имеется объект, на который он указывает. Альтернативы для указателей на функции рассматриваются отдельно, в главе 22, "Объекты-функции и обратные вызовы". 20.1. Holder и Trule В этом разделе рассматриваются два типа интеллектуальных указателей: holder (дословно— держатель, хранитель), предназначенный для исключительного хранения объекта, и так называемый trule (от transfer capsw/e — оболочка передачи), предназначенный для передачи владения от одного держателя другому.
388 Глава 20. Интеллектуальные указатели 20.1.1. Защита от исключений Исключения были введены в C++ для повышения надежности программ. Они позволяют более четко отделить обычный путь выполнения от некоторого исключительного пути. Буквально сразу же после введения исключений в C++ многие заметили, что бездумное использование исключений приводит к неприятностям, в частности к утечке памяти. Приведем пример, показывающий одну из множества возможных неприятных ситуаций. void do_something() { Something* ptr = new Something; // Выполняем некоторые действия с ptr ptr->perform(); delete ptr; } В данной функции с помощью оператора new создается объект, над ним выполняются некоторые операции, после чего он уничтожается с помощью оператора delete. К сожалению, если после создания объекта, но до его удаления могут случиться всякие неприятности и будет сгенерировано исключение, то занимаемая объектом память не будет освобождена, т.е. мы получим тривиальную утечку памяти. Вторая проблема состоит в том, что так и не будет вызван деструктор объекта (например, буферы не будут записаны на диск, сетевое подключение не будет освобождено, окно не будет закрыто и т.д.). Данная конкретная проблема решается с помощью явного обработчика исключений. void do_something{) { Something* ptr = 0; try { ptr я new Something; // Выполняем некоторые действия с ptr ptr->perform(); } catch (...) { delete ptr; throw; // Передача исключения } delete ptr; } Это вполне приемлемое решение, но даже в таком простейшем случае видно, что исключительный путь выполнения начинает главенствовать над обычным и удаление объекта происходит в двух различных местах — как в обычном пути выполнения, так и в
20.1. Holder и Trule 389 исключительном. Это решение с усложнением функции становится все хуже и хуже. Рассмотрим, что произойдет, если в функции будут создаваться два объекта. void do_two_things () { Something* first = new Something; first->perform(); Something* second = new Something; second ->perform(); delete second; delete first; } Использовать явный обработчик исключений можно различными способами, но все они выглядят не очень привлекательно. Приведем один из вариантов. void do__two_things () { Something* first = 0; Something* second = 0; try { first = new Something; first->perform(); second = new Something; second ->perform(); } catch(...) { delete second; delete first; throw; // Передача исключения } delete second; delete first; } Здесь предполагается, что оператор delete не генерирует исключений . В данном примере код обработчика исключений представляет собой существенную часть программы, но можно показать (и это гораздо важнее), что он становится к тому же наиболее тонким ее местом. Кроме того, требования безопасности исключений приводят к существенному изменению обычного пути выполнения программы. Это вполне разумное предположение. Деструкторов, которые могут генерировать исключения, следует избегать, поскольку деструкторы автоматически вызываются при генерации исключений и в этой ситуации генерация нового исключения приведет к немедленному аварийному завершению программы.
390 Глава 20. Интеллектуальные указатели 20.1.2. Holder К счастью, не так уж трудно написать небольшой шаблон класса, который инкапсулирует стратегию второго примера. Идея состоит в том, чтобы написать класс, который ведет себя подобно указателю, но при этом уничтожает объект, на который указывает, при уничтожении самого указывающего объекта или при присвоении ему другого указателя. Назовем такой класс holder (держатель, хранитель), поскольку он предназначен для безопасного хранения объекта при выполнении различных вычислений. Ниже показано, как это можно сделать. // pointers/holder.hpp template <typename T> class Holder { private: *T* ptr; // Ссылка на хранимый объект public: // Конструктор по умолчанию Holder() : ptr(0) { } // Конструктор для указателя explicit Holder(Т* р) : ptr(p) { } // Деструктор - освобождение объекта -Holder() { delete ptr; } // Присвоение нового указателя Holder<T>& operator= (T* р) { delete ptr; Ptr = р; return *this; } // Операторы для работы с указателем Т& operator* () const { return *ptr; } T* operator-> () const { return ptr; } // Получение указываемого объекта T* get () const {
20.1. Holder и Trule 391 return ptr; } // Освобождение от владения объектом void release() { ptr = 0; } // Обмен владением с Другим хранителем void exchange_with(Holder<T>& h) { swap(ptr,h.ptr); } // Обмен владением с другим указателем void exchange_with(T*& p) { swap (ptr, p) ; } private: // Копирование и копирующее присвоение запрещены Holder (Holder<T> constfc); Holder<T>& operator= (Holder<T> const&); }; Семантически наш класс получает во владение объект, на который указывает ptr. Этот объект должен быть создан с помощью оператора new, так как при уничтожении объекта вызывается оператор delete2. Функция-член release () снимает обязанности по управлению хранимым объектом с хранителя. Однако оператор присвоения достаточно интеллектуален для того, чтобы уничтожить и освободить ресурсы хранимого объекта во избежание утечки памяти и прочих негативных последствий (так как оператор присвоения не возвращает указатель на ранее хранившийся объект). Кроме того, в шаблон добавлены две функции-члена exchange_with(), которые позволяют нам обмениваться объектами без их уничтожения. Ниже показацо, как можно переписать наш пример при использовании шаблона Holder. void do_two_things () { Holder<Something> first(new Something); first->perform(); Holder<Something> second(new Something); second->perform(); 2 " Для обеспечения гибкости можно добавить еще один параметр шаблона, который будет определять стратегию освобождения объекта.
392 Глава 20. Интеллектуальные указатели Так гораздо понятнее. Благодаря работе деструктора Holder код стал не только безопасным в смысле исключений, Н9 теперь удаление объектов происходит автоматически и при обычном пути выполнения. Обратите внимание, что при инициализации нельзя использовать присваивающий синтаксис. Holder<Something> first = new Something; // ОШИБКА Дело в том, что конструктор объявлен как explicit, и имеется определенное различие между X х; Y у(х); // Явное преобразование и X х; Y у = х; // Неявное преобразование В первом случае новый объект типа Y создается с использованием явного преобразования из типа X, в то время как в последнем новый объект типа Y создается с помощью неявного преобразования, которое в шаблоне Holder запрещено ключевым словом explicit. 20.1.3. Holder в качестве члена класса Избежать утечки ресурсов можно также, используя Holder внутри класса. Если член класса имеет этот тип, а не обычный тип указателя, нам зачастую не требуется иметь дело с этим членом в деструкторе, поскольку такой объект будет удален при удалении члена Holder. Кроме того, этот шаблон помогает избежать утечки ресурсов в случае генерации исключения при инициализации объекта. Заметим, что деструкторы вызываются только для полностью сконструированных объектов и, если исключение генерируется в конструкторе, то деструкторы вызываются только для тех объектов-членов, конструкторы которых завершили свою работу без ошибок. Если не использовать шаблон Holder, это приведет к утечке ресурсов: например, если после первого успешного выделения ресурсов последовало неуспешное. // pointers/refmeml.hpp class RefMembers { private: MemType* ptrl; MemType* ptr2; public: // Конструктор по умолчанию // - утечка при исключении во втором new RefMembers () ,. : ptrl (new MemType) , ptr2 (new MemType) { }
20.1. Holder и Trule 393 // Конструктор копирования // - утечка при исключении во втором new RefMembers (RefMembers const& x) : ptrl(new MemType(*x.ptrl)) , ptr2 (new MemType (*x.ptr2) ) { } // Оператор присвоения const RefMembers& operator= (RefMembers const& x) { *ptrl = *x.ptrl; *ptr2 = *x.ptr2; return *this; } -RefMembers () { delete ptrl; delete ptr2; } }; Если же вместо обычных указателей использовать шаблон Holder, то потенциальных утечек ресурсов можно легко избежать. // pointers/refmem2.hpp #include "holder.hpp" class RefMerabers { private: Holder<MemType> ptrl; Holder<MemType> ptr2; public: // Крйструктор по умолчанию // - никаких возможных утечек RefMembers () : ptrl(new MemType), ptr2(new MemType) { } // Конструктор копирования // - никаких возможных утечек RefMembers (RefMembers constfc x) : ptrl(new MemType(*x.ptrl)) , ptr2(new MemType(*x.ptr2)) { } // Оператор присвоения
394 Глава 20. Интеллектуальные указатели const RefMembersfc operator= (RefMembers constfc x) { *ptrl = *x.ptrl; *ptr2 = *x.ptr2; return *this; } // Деструктор не является необходимым // (деструктор по умолчанию удаляет объекты ptrl и ptr2) }; Заметим, что, хотя можно и не определять деструктор, все равно придется определить конструктор копирования и оператор присвоения. 20.1.4. Захват ресурса есть инициализация Общая идея, лежащая в основе класса Holder, — захват ресурса есть инициализация (resource acquisition is initialization — RAII) — введена в [34]. Вводя параметры шаблона для стратегии освобождения, соответствующий приведенной ниже схеме код void do () { // Захват ресурсов RES1* resl = acquire_resource_l(); RES2* res2 = acquire_resource__2 () ; // Освобождение ресурсов release__resource_2 (res2) ; release_resource_l(resl); } можно заменить следующим: void do() { // Захват ресурсов Holder<RESl/...> resl(acquire_resource_l()); Holder<RES2, . . . > res2(acquire_resource_2()); } 20.1.5. Ограничения Holder Далеко не все проблемы решаются с помощью шаблона Holder. Рассмотрим пример. Something* load_something() {
20.1. Holder и Trule 395 Something* result *= new Something; reacLsomething(result); return result; } В этом примере две вещи усложняют код. 1. В приведенной функции вызывается другая функция, read_something (), которой в качестве аргумента должен передаваться обычный указатель. 2. loacLsomething () возвращает обычный указатель. При использовании шаблона Holder код становится безопасным в смысле исключений, но при этом повышается его сложность. Something* load__something () < Holder<Something> result(new Something); read_something(result.get_pointer()); Something* ret = result.get_pointer(); result.release(); return ret: } По-видимому, функция read_something () не осведомлена о существовании шаблона Holder, так что мы должны получить реальный указатель с помощью вызова функции- члена get_pointer (). При использовании данной функции Holder продолжает осуществлять управление объектом, и получатель результата вызова функции должен отдавать себе отчет, что владельцем объекта, на который указывает данный указатель, является не он, a Holder. Если функции get_pointer () в шаблоне нет, можно воспользоваться пользовательским штератором разыменования * вкупе со встроенным оператором взятия адреса &. Еще одной альтернативой является явный вызов оператора ->. read_something(&*result); read_something(result.operator->()); Вы наверняка согласитесь, что второй вариант — очень жалкая альтернатива первому. Однако такой вариант не оставит потенциальную опасность подобных действий незамеченной программистом. Еще одно замечание по приведенному ранее примеру кода касается вызова функции- члена release () для снятия владения с объекта. Это предотвращает автоматическое уничтожение объекта при завершении функции, и его можно вернуть вызывающей функции. Обратите внимание на необходимость сохранения возвращаемого значения во временной переменной перед освобождением.
396 Глава 20. Интеллектуальные указатели Something* ret = result.get_pointer() ; result.release(); return ret: Чтобы избежать этого, можно использовать инструкцию вида return result.release(); Однако при этом следует изменить функцию release () с тем, чтобы она возвращала указатель на освобождаемый объект. template<typename T> class Holder { Т* release() { Т* ret = ptr; ptr = 0; return ret; } } Из всего этого можно сделать вывод: интеллектуальные указатели не так уж интеллектуальны, но их использование способно намного упростить жизнь программиста. 20.1.6. Копирование Holder Вы, вероятно, заметили, что в реализации шаблона Holder мы запретили копирование, поместив конструктор копирования и копирующий оператор присвоения в закрытой части класса. Ведь в чем состоит цель копирования, как не в получении второго объекта, по сути идентичного первому? В случае интеллектуального указателя Holder это означает, что копия также является владельцем объекта, и попытка двух освобождений объекта со стороны обоих интеллектуальных указателей неизбежно приведет к хаосу и некорректной работе программы. Таким образом, копирование — неподходящая операция для хранителей объектов. Вместо этого следует говорить, скорее, о передаче объектов от одного хранителя другому. Операция передачи легко осуществляется с помощью операции освобождения объекта, за которой следует инициализация или присвоение. Holder<Something> hi(new Something); Holder<Something> h2(hi.release()); Заметим еще раз, что синтаксис Holder<X> h = р; неприменим, поскольку влечет за собой неявное преобразование типов, в то время как конструктор объявлен как explicit: Holder<Something> h2 = hi.release(); // ОШИБКА
20.1. Holder и Trule 397 20.1.7. Копирование Holder при вызовах функций Явная передача работает вполне корректно, однако ситуация несколько усложняется, если передача осуществляется через границы вызова функций. Для случая передачи Holder от вызывающей функции в вызываемую всегда можно воспользоваться передачей по ссылке, а не по значению. Использование освобождения с последующей инициализацией может привести к проблемам при передаче нескольких аргументов. МуСlass x; callee(hi.release(),х); // Передача х может // сгенерировать исключение Если компилятор сначала вычислит hi. release (), то последующая передача х (в предположении, что осуществляется передача по значению), которая может привести к генерации исключения, вызовет утечку памяти, поскольку у объекта, хранившегося в hi, больше не будет владельца. Поэтому класс Holder всегда следует передавать только по ссылке. К сожалению, возврат хранителя объекта по ссылке далеко не всегда удобен, поскольку приводит к тому, что время жизни хранителя должно превышать вызов функции, а это, в свою очередь, не способствует ясному пониманию того, когда и как должен быть освобожден хранимый объект. Как вариант, можно вызвать функцию release () непосредственно перед возвратом из функции. Рассмотрим следующий код: Something* creator() { Holder<Something> h(new Something); MyClass x; // Введен с иллюстративной целью return h.releaseO; } В этом случае необходимо отдавать себе отчет, что деструкция х может вызвать исключение, которое генерируется после освобождения своего объекта хранителем h и до того, как этот объект получит вызывающая функция. В результате можно получить утечку ресурсов^СЭто еще раз подтверждает мысль о том, что генерация исключений в деструкторе — идея весьма нездоровая, особенно в том отношении, что генерация исключения в деструкторе в процессе развертывания стека при обработке предыдущего исключения приведет к немедленному завершению программы.) 20.1.8. Trule Для решения проблем такого рода введем вспомогательный шаблон класса, предназначенный для передачи хранителей. Назовем его Trule (от transfer capswte — оболочка передачи) и определим, как показано ниже. // pointers/trule.hpp #ifndef TRULE__HPP
398 Глава 20. Интеллектуальные указатели #define TRULE_HPP template <typename T> class Holder; template <typename T> class Trule { private: T* ptr; // Объекты, с которыми работает класс public: // Конструктор, гарантирующий, что класс используется // только как возвращаемый тип для передачи Holder // вызывающей функции Trule(Holder<T>& h) { ptr = h.get(); h.release(); } // Конструктор копирования Trule(Trule<T> constfc t) { ptr = t.ptr; const_cast<Trule<T>&>(t).ptr = 0; } // Деструктор -Trule () { delete ptr; } private: // Запрещено использование в качестве lvalue //и копирующее присвоение Trule(Trule<T>&); Trule<T>& operators (Trule<T>&); friend class Holder<T>; }; #endif // TRULE_HPP Очевидно, что конструктор копирования выглядит довольно безобразно. Поскольку шаблон Trule предназначен для работы в качестве возвращаемого типа функции, его объекты всегда выступают в роли временных (rvalue); следовательно, они могут оказаться ограниченными ссылкой на константный тип. Однако передача не может быть копированием и должна забрать владение объектом у исходного Trule, присвоив соответствующему указателю нулевое значение. Очевидно, что это не константная операция. То, что сделано нами в конструкторе Trule, выглядит безобразно, но вполне корректно,
20.1. Holder и Trule 399 \ если передаваемый объект на самом деле константным не является. Следовательно, при использовании шаблона Trule вы должны быть внимательны и объявлять возвращаемый тип функции как Trule<T>, но не как Trule<T> const. Заметим, что такого рода код при преобразовании Holder в Trule не используется: Holder должен быть неконстантным lvalue. Вот почему был использован отдельный тип для передачи вместо объединения необходимой нам функциональности в одном шаблоне с функциональностью Holder. Чтобы запретить использование Trule где бы то ни было помимо возвращаемого типа для передачи Holder, конструктор копирования, получающий ссылку на неконстантный объект, и аналогичный оператор присвоения объявлены закрытыми. Это позволяет избежать использования объектов Trule в качестве lvalue, но достаточной эта мера не является. Однако назначение этого шаблона — помочь программисту, а не помешать ополоумевшему хакеру. Шаблон Trule не будет полон до тех пор, пока не будет распознаваться шаблоном Holder. // pointers/holder2extr.hpp template <typename T> class Holder { // Ранее определенные члены public: Holder(Trule<T> constfc t) { ptr = t.ptr; const_cast<Trule<T>&>(t).ptr = 0; } Holder<T>& operator» (Trule<T> constfc t) { delete ptr; jptr = t.ptr; const_cast<Trule<T>&>(t).ptr = 0; return *this; } }; Для иллюстрации взаимодействия пары Holder/Trule можно переписать наш пример load_something () и вызвать ее. #include "holde^.hpp" #include "trule.hpp" class Something { };
400 Глава 20. Интеллектуальные указатели void read_something(Something* x) { } Trule<Something> load_something() { Holder<Something> result(new Something); read_something(result.get()) ; return result; } int main() { Holder<Something> ptr(load_something()); } В завершение следует отметить, что нами создана пара шаблонов классов, которые практически так же удобны в обращении, как и обычные указатели, но при этом обладают тем преимуществом, что при необходимости самостоятельно управляют освобождением объектов, в частности при развертывании стека в результате генерации исключения. 20.2. Счетчики ссылок Шаблон Holder и вспомогательный шаблон Trule хорошо подходят для временного хранения структур в выделенной памяти, с тем чтобы в случае генерации исключения и развертывания стека эта память была автоматически освобождена. Однако утечки памяти могут проявляться и в другом контексте, в частности когда несколько объектов объединены в одну сложную структуру. Общее правило управления динамически распределяемыми объектами гласит: если в приложении ничто не указывает на динамически распределенный объект, то такой объект должен быть уничтожен, а его память должна быть доступна для повторного использования. Таким образом, нет ничего удивительного в том, что программисты по мере сил и возможностей стремятся автоматизировать выполнение этого правила. Проблема состоит только в том, как определить, что ничто больше не указывает на объект. Одна идея, многократно реализованная на практике, состоит в использовании так называемого счетчика ссылок (reference counting). Для каждого объекта, на который может иметься указатель, хранится количество таких указателей, и, когда оно падает до нуля, объект уничтожается. Для того чтобы данная стратегия была осуществима в C++, следует придерживаться определенных соглашений. В частности, поскольку непрактично отслеживать создание, копирование и уничтожение обычных указателей на объект, достаточно распространенным является требование использования для работы с объектом со счетчиком ссылок только интеллектуальных указателей определенного типа. В этом разделе рассматривается реализация таких интеллектуальных указателей со счетчиками
20.2. Счетчики ссылок 401 ccbinoK. Такой указатель представляет собой шаблон, главным параметром которого являемся тип объекта, на который он указывает. template<typename T ...> class CountingPtr { public: // Конструктор, создающий новый счетчик для // указываемого объекта explicit CountingPtr(T*); // Копирование увеличивает значение счетчика CountingPtr(CountingPtr<T ...> const&); // Деструктор уменьшает значение счетчика inline -CountingPtr(); // Присвоение уменьшает значение счетчика для //ранее указанного объекта, и увеличивает // значение счетчика для присваиваемого объекта // (будьте осторожны с присвоением самому себе!) CountingPtr<T ...>& operator=(CountingPtr<T...> constfc); // Операторы интеллектуального указателя inline T& operator* (); inline T* operator->() ; }; Параметр Т представляет собой единственный необходимый для построения функционирующего шаблона счетчика ссылок параметр. Хотя так и хочется оставить его единственным параметром с тем, чтобы шаблон был как можно более простым и надежным, мы выбрали CountingPtr для демонстрации использования параметров стратегий (концепция, рассматриваемая в главе 15, "Классы свойств и стратегий"). Комментарии в приведенном фрагменте кода поясняют общий подход к счетчикам ссылок: каждыйЯбонструктор, деструктор и оператор присвоения могут потенциально изменить значение счетчика ссылок (когда значение счетчика достигнет нуля, объект удаляется). 20.2.1. Где находится счетчик Поскольку наша идея состоит в подсчете количества указателей на объект, было бы совершенно логично разместить счетчик в объекте. К сожалению, это не совсем удачная идея, если тип указываемого объекта не предусматривает такой счетчик. Если в самом объекте счетчика ссылок нет, его можно разместить в отдельно выделенной области памяти, которая имеет, как минимум, ту же продолжительность жизни, что и указываемый объект; иными словами, эта область памяти должна быть динамически выделена. Применение для этого стандартного : : operator new из используемого компилятора обычно дает не самые лучшие результаты в плане производительности.
402 Глава 20. Интеллектуальные указатели Этот оператор должен быть способен выделять память под объекты произвольного размера без существенных накладных расходов, что требует определенных вычислительных компромиссов. Таким образом, при реализации счетчиков ссылок гораздо более распространено использование специализированных распределителей памяти. Менее распространенной альтернативой раздельному выделению памяти для объекта и счетчика является использование распределителя памяти специального назначения, который может выделять дополнительную память для счетчика ссылок. Вместо того чтобы указывать способ выделения памяти для счетчика заранее, можно указать место хранения счетчика в качестве параметра шаблона. Этот параметр и есть стратегией счетчика (см. главу 15, "Классы свойств и стратегий"). Интерфейс этой стратегии может состоять из функции, возвращающей значение целого типа, и функции, выделяющей при необходимости память для этого целого. Однако по ряду причин требуется обеспечить интерфейс несколько более высокого уровня. 20.2.2. Параллельный доступ к счетчику В среде с единственным потоком выполнения управление счетчиком тривиально. Базовыми операциями являются увеличение, уменьшение и сравнение значения счетчика с нулем. Однако во многопоточной среде счетчик может совместно использоваться интеллектуальными указателями из разных потоков выполнения. В этом случае необходимо добавить интеллектуальный указатель к самому счетчику ссылок, с тем, например, чтобы иметь возможность упорядочить операции увеличения значения счетчика со стороны разных потоков. На практике для этого требуется та или иная (явная или неявная) блокировка. Вместо указания того, каким образом осуществляется данная блокировка, можно указать интерфейс счетчика достаточно высокого уровня для введения операций блокирования. Потребуем, чтобы счетчик представлял собой класс с приведенным ниже интерфейсом. class CounterPolicy { public: // Следующие специальные члены (конструкторы, // деструктор и копирующее присвоение) в ряде // случаев не обязаны быть объявлены явно, но // должны быть доступнь* CounterPolicy(); CounterPolicy(CounterPolicy constfc); -CounterPolicy() ; CounterPolicy& operator=(CounterPolicy const&); // Считаем, что Т - тип указываемого объекта void init(T*); // Инициализация единичным значением, // возможно, с выделением памяти void dispose(Т*); // Возможно, приводит к освобождению // счетчика void increment(Т*);// Атомарное увеличение на 1 void decrement(T*);// Атомарное уменьшение на 1
20.2. Счетчики ссылок 403 bool is_zero(T*); // Проверка на равенство 0 >; Тип Т, используемый в данном шаблоне, передается как параметр шаблона. Этот тип используется только стратегиями, работающими со счетчиками. Блокировка предотвращает одновременное обращение только к счетчику, но не к объеюу CountingPtr. Следовательно, если несколько интеллектуальных указателей на некоторый объект совместно используются разными потоками, приложению может потребоваться ввести дополнительные блокировки для корректного упорядочения операций с CountingPtr. Сами интеллектуальные указатели не могут отвечать за блокировку на этом уровне. 20.2.3. Деструкция и освобождение памяти Когда больше нет указателей на данный объект, наша стратегия должна удалить его. В C++ это достигается с помощью оператора delete, однако это не всегда так. Иногда выделенную для объекта память необходимо освободить с помощью другой функции, например free (). Кроме того, если объект на самом деле представляет собой массив, то для освобождения памяти может потребоваться использование оператора delete [ ]. Поскольку, как вы смогли убедиться, могут быть самые разные варианты удаления объекта и освобождения памяти, имеет смысл ввести отдельную стратегию объекта. Ее интерфейс очень прост. class ObjectPolicy { public: // Следующие специальные члены (конструкторы, // деструктор и копирующее присвоение) в ряде // случаев не обязаны быть объявлены явно, но // должны быть доступны ObjectPolicy(); ObjectPolicy(ObjectPolicy const&); -ObjectPolicy(); ObjectPolicyfc operator=(ObjectPolicy constfc); // Считаем, что Т - тип указываемого объекта void disposed1*) ; }; ' Вполне можно обогатить эту стратегию другими операциями, которые могут использовать указываемый объект, например operator* или operator->. Один из распространенных вариантов — включение дополнительной проверки при разыменовании интеллектуального указателя, проверяющей, действительно ли существует указываемый объект. С другой стороны, вполне возможно использование дополнительных параметров стратегии для подобных проверок. Для краткости расширения стратегии такого рода не рассматриваются, но если вы разберетесь с принципами работы стратегии, то не составит большого труда добавить к ней все, что вы пожелаете.
404 Глава 20. Интеллектуальные указатели Для большинства объектов, работа с которыми осуществляется с помощью Count- ingPtr, можно воспользоваться приведенной ниже простейшей стратегией объекта. // pointers/stdobjpolicy.hpp class StandardObjectPolicy { public: template<typename T> void dispose(T* object) { delete object; } >; Понятно, что эта стратегия не годится для работы с оператором new [ ]. Замена стратегии для этого случая тривиальна. // pointers/stdarraypolicy.hpp class StandardArrayPolicy { public: template<typename T> void dispose(T* array) { delete[] array; } }; Заметим, что в обоих случаях реализация dispose () представляет собой шаблон члена. Вместо этого можно также воспользоваться параметризованным классом стратегии. Обсуждение данной альтернативы можно найти в разделе 15.1.6, стр. 287. 20.2.4. Шаблон CountingPtr Теперь, когда вы разобрались с интерфейсами стратегий, можно приступить к реализации интерфейса CountingPtr. // pointers/countingptr.hpp template<typename T, typename CounterPolicy = SimpleReferenceCount, typename ObjectPolicy = StandardObjectPolicy> class CountingPtr : private CounterPolicy, private ObjectPolicy { private: // Сокращения: typedef CounterPolicy CP; typedef ObjectPolicy OP; T* object_pointed_to; // Указываемый объект // (NULL при его отсутствии) public: // Конструктор по умолчанию (нет явной инициализации)
20.2. Счетчики ссылок 405 CountingPtrO { this->object_pointed_to = NULL; } // Конструктор преобразования типа // (из встроенного указателя) explicit CountingPtr(T* p) { this->init(р); // Инициализация обычным указателем } // Конструктор копирования CountingPtr (CountingPtr<T,CP,OP> const& cp) : CP((CP const&)cp), // Копирование стратегий 0P((OP const&)cp) { this->attach(cp); // Копирование указателя //и увеличение счетчика } // Деструктор -CountingPtrO { this->detach(); // Уменьшение счетчика (и // уничтожение счетчика, если // это последний его владелец) } // Присвоение встроенного указателя CountingPtr<T,CP,OP>& operator= (T* р) { // Проверка на указание на хранимый объект assert(p != this->object_pointed__to) ; this->detach(); // Уменьшение счетчика (и // уничтожение счетчика, если // это последний его владелец) this->init(p); // Инициализация обычным указателем return *this; } // Копирующее присвоение (с защитой от // присвоения самому себе) CountingPtr<T,CP,0P>& operator= (CountingPtr<T,CP,OP> const& cp) { if (this->object_pointed_to != cp.object_pointed_to) { this->detach(); // Уменьшение счетчика (и // уничтожение счетчика, если // это последний его владелец) // Присвоение стратегий CP::operator=((CP const&)cp); OP::operator=((OP const&)cp); // Копирование указателя и увеличение счетчика this->attach(cp);
406 Глава 20. Интеллектуальные указатели } return *this; } // Операторы, делающие данный класс // интеллектуальным указателем Т* operator-> () const { return this->object_pointed__to; } T& operator* () const { return *this->object_pointed_to; } // Прочие интерфейсы могут быть добавлены позже //... I private: // Вспомогательные функции // - Инициализация обычным указателем void init (T* р) { if (p != NULL) { CounterPolicy::init(p); } this->object_pointed__to = p; } // - Копирование указателя и увеличение счетчика void attach(CountingPtr<T,CP,OP> const& cp) { this->object__pointed__to = cp.object_pointed_to; if (cp.object_pointed_to != NULL) { CounterPolicy: -.increment (cp.object_pointed_to) ; } } // - Уменьшение счетчика (и его уничтожение, если // это последний его владелец) void detach() { if (this->object_pointed_to != NULL) { CounterPolicy::decrement(this->object_pointed_to); if (CounterPolicy: :is_zero(this->object_pointed__to) ) { // Уничтожение счетчика CounterPolicy::dispose(this->object_pointed_to); // Использование стратегии объекта )цля удаления // указываемого объекта ObjectPolicy: :dispose (this->object_pointed__to) ; }
20.2. Счетчики ссылок 407 Этот шаблон не содержит ничего сложного. Самый тонкий момент— проверка на присвоение самому себе в операторе копирующего присвоения. В большинстве случаев оператор присвоения должен отсоединить указатель со счетчиком от указываемого объекта, тем самым уменьшая связанный с объектом счетчик, что может привести к удале- ниюсамого объекта. Однако при присвоении самому себе такое действие будет преждевременным и приведет к некорректному результату. Заметим также, что необходимо использовать явную проверку на равенство указателя нулю, поскольку нулевой указатель не имеет связанного с ним счетчика. Альтернативой нашему подходу может быть делегирование проверки классам стратегий. Кстати, одна из возможных стратегий может состоять в том, чтобы не допустить существования нулевого указателя со счетчиком. Применяя такую стратегию, вы получите в результате определенное повышение производительности. Для включения стратегий использовано наследование. Это гарантирует, что если стратегии представляют собой пустые классы, то для них не потребуется лишняя память (если компилятор способен к оптимизации пустых базовых классов, описанной в разделе 16.2, стр.. 315). Для того чтобы избежать видимости членов классов стратегий в классе интеллектуального указателя, можно использовать описанный в разделе 16.2.2, стр. 318, шаблон BaseMemberPair. Поскольку здесь есть больше одного аргумента шаблона по умолчанию, может оказаться, что выгоднее использовать описанную в разделе 16.1, стр.311, методику для удобной и выборочной подмены значений по умолчанию. Однако не будем делать этого по соображениям краткости. 20.2.5. Простой незахватывающий счетчик Хотя конструирование CountingPtr полностью завершено, реализация конструкции все еще не закончена. Здесь все еще нет кода стратегии счетчика. Давайте начнем с того, что рассмотрим стратегию счетчика, который не хранится в указываемом объекте, т.е. незахватывающую (или неразрушающую (noninvasive, nonintrusive)) стратегию счетчика. Главным вопросом данной стратегии оказывается размещение счетчика и выделение для него памяти. Счетчик должен совместно использоваться несколькими объектами CountingPtr, следовательно, он должен существовать до тех пор, пока не будет уничтожен последний интеллектуальный указатель. Обычно это выполняется с помощью распределителя специального назначения, задача которого — распределение памяти для малых объектов фиксированного размера. Однако, поскольку конструирование таких распределителей— тема не совсем уместная в книге о шаблонах C++, воздержимся от обсуждения данного вопроса3. Вместо этого будем считать, что у нас имеются функции alloc_counter () и dealloc_counter (), задача которых — распределение памя- Распределители могут быть параметризованы различными путями (например, для выбора стратегий со ссылками для параллельного доступа), но мы не думаем, что их рассмотрение облегчит понимание шаблонов и их применения.
408 Глава 20. Интеллектуальные указатели ти для величин типа size_t. При данных предположениях можно записать наш простой счетчик, как показано ниже. #include <stddef.h> // Определение типа size_t #include "allocator.hpp" class SimpleReferenceCount { private: size_t* counter; // Выделенный счетчик public: SimpleReferenceCount () { counter = NULL; } // Конструктор копирования и оператор копирующего // присвоения по умолчанию вполне работоспособны, // так как просто копируют счетчик public: // Выделение счетчика и инициализация его значения template<typename T> void init (T*) { counter = alloc_counter(); *counter = 1; } // Удаление счетчика template<typename T> void dispose (T*) { dealloc_counter(counter); } // Увеличение на единицу template<typename T> void increment (T*) { ++*counter; } // Уменьшение на единицу template<typename T> void decrement (T*) { —*counter; } // Проверка на равенство нулю template<typename T> bool is_zero (Т*) { return ^counter ==0; } \> }; Поскольку эта стратегия не пустая (в ней хранится указатель на счетчик), она увеличивает размер класса CountingPtr. Этот размер может быть уменьшен, если хранить указатель на объект отдельно от счетчика, вместо размещения его непосредственно в классе интеллектуального указателя. Такое решение требует внесения изменений в кон-
20.2. Счетчики ссылок 409 струкцию стратегии и приводит к снижению производительности доступа к объекту из-за внесения дополнительного уровня косвенности. Заметим также, что эта конкретная стратегия не использует сам указываемый объект. Другими словами, параметр, передаваемый ее функциям-членам, никогда не используется. В следующем разделе вы познакомитесь с другой стратегией, которая использует данный параметр. 20.2.6. Шаблон простого захватывающего счетчика Захватывающая (или разрушающая (invasive, intrusive)) стратегия счетчика представляет собой стратегию, которая размещает счетчик в типе самого обрабатываемого объекта (или, возможно, в некоторой области, управляемой данным объектом). Это условие требует выбора соответствующей конструкции типа объекта в процессе его разработки, так что данное решение оказывается привязанным к конкретной реализации объекта. Однако в иллюстративных целях будет разработана несколько более обобщенная захватывающая стратегия. Для выбора места размещения счетчика в указываемом объекте используем параметр шаблона, не являющийся типом и представляющий собой указатель на член класса указываемого объекта. Поскольку в этом случае память для счетчика выделяется как часть памяти для самого объекта, реализация такой стратегии проще, чем незахватывающей стратегии, хотя синтаксис указателя на член класса оказывается менее обобщенным. // pointers/memberrefcount.hpp template< typename ObjectT, // Тип класса со счетчиком typename CountT, // Тип указателя CountT ObjectT::*CountP> // Размещение счетчика class MemberReferenceCount { public: // Конструктор и деструктор по умолчанию // вполне работоспособны // Инициализация счетчика единицей void init (ObjectT* object) { object->*CountP = 1; } // Никаких действий по уничтожению счетчика не требуется void dispose (ObjectT*) { } // Увеличение значения счетчика на единицу void increment (ObjectT* object) { ++object->*CountP;
410 Глава 20. Интеллектуальные указатели } // Уменьшение значения счетчика на единицу void decrement (ObjectT* object) { —object~>*CountP; } // Проверка значения счетчика на равенство нулю template<typename T> bool is^zero (ObjectT* object) { return object^>*CountP ==0; } }; Данная стратегия позволяет программисту, реализующему класс, быстро снабдить его указателем со счетчиком ссылок. Приведем набросок схемы такого класса. class ManagedType { private: size_t ref_count; public: typedef CountingPtr<ManagedType, MemberReferenceCount <ManagedType, size_t, &ManagedType::ref_count> > Ptr; }; При таком подходе ManagedType:: Ptr представляет собой удобный способ ссылки на тип интеллектуального указателя, предназначенного для доступа к обьеюу ManagedType. 20.2.7. Константность В C++ типы X const* и X*const различны. Первый означает, что указываемый элемент не может быть модифицирован, в то время как второй говорит о неизменности самого указателя. Такая же двойственность отмечается и у рассмотренных указателей со счетчиками ссылок: X const* соответствует CountingPtr<X const>, в то время как X*const соответствует CountingPtr<X> const. Другими словами, константность указываемого объекта представляет собой свойство аргумента шаблона. Рассмотрим, как это наблюдение влияет на открытые функции-члены CountingPtr. Операторы разыменования не изменяют указатель, поэтому они являются константными функциями-членами, обеспечивающими доступ к указываемому объекту. Поскольку константность данного объекта определяется параметром шаблона Т, в возвращаемом типе данных операторов можно использовать значение Т без уточняющих квалификаторов. Значение int* не может быть инициализировано значением int const*, поскольку это в конечном счете может привести к изменению константного объекта посредством
20.2. Счетчики ссылок 411 неконстантного. По тем же соображениям необходимо гарантировать, что Count- ingPtr<int> не может быть инициализировано ни значением типа Count- ingPtr<int const>, ни даже просто int const*. И вновь использование простого (не const) параметра шаблона Т влечет за собой нужный эффект. Это может показаться достаточно простым соображением, однако весьма распространены реализации интеллектуальных указателей, которые объявляют конструктор или оператор присвоения принимающими параметр типа Т const* (по всей видимости, напрасно). Рассмотренные соображения применимы и к операторам присвоения. Разумеется, сами эти операторы не могут быть константными. 20.2.8. Неявные преобразования типов Встроенные указатели могут участвовать в ряде неявных преобразований типов, а именно: • преобразовании к vo i d *; • преобразовании к указателю на базовый подобъект указываемого объекта; • преобразовании к типу bool (false в случае нулевого указателя и true в противном случае). Возможна ситуация, когда потребуется эмулировать такие преобразования типов в шаблоне CountingPtr, однако это далеко не тривиальная задача, как вы вскоре увидите. Кроме того, некоторые программисты предпочитают добавлять в свои интеллектуальные указатели преобразования в тип соответствующего встроенного указателя, например чтобы Count- ingPtr<int const> мог быть преобразован в тип int const*. К сожалению, возможность неявного преобразования в типы встроенных указателей разрушает исходную посылку о том, что все указатели на объект со счетчиком ссылок являются указателями типа CountingPtr. Таким образом, приходится отказаться от предоставления такой возможности неявного преобразования типов, и Count ingPtr<X> не может быть неявно преобразован в void* или X*. Другими недостатками неявного преобразования в типы встроенных указателей (в предположении, что ср представляет собой указатель со счетчиком ссылок) являются следующие: • становятся корректными как операция delete ср, так и : : delete ср; • все виды некорректной арифметики указателей становятся ^диагностируемыми (например, выражения ср [n], cp2-cpl и т.п.). Тем не менее неявные преобразования в другие специализации CountingPtr могут иметь смысл. Например, можно представить неявное преобразование в Count- ingPtr<void> (этот тип может быть полезным типом непрозрачного указателя наподобие void*). Однако при этом имеются определенные ограничения: захватывающая стратегия счетчика не в состоянии работать с таким преобразованием в силу того, что тип void не содержит счетчика. Точно так же может оказаться несовместимым с захватывающей стратегией и базовый класс указываемого объекта.
412 Глава 20. Интеллектуальные указатели Однако такое неявное преобразование можно добавить к рассматриваемому шаблону CountingPtr. Если буде^ предпринята попытка преобразования, не совместимого с данной стратегией счетчика, произойдет ошибка инстанцирования. Ниже приведен пример неявного преобразования. template<typename T, typename CounterPolicy = SimpleReferenceCount, typename ObjectPolicy = StandardObjectPolicy> class CountingPtr: private CounterPolicy, private ObjectPolicy { private: // Сокращения: typedef CounterPolicy CP; typedef ObjectPolicy OP; public: // Добавляем преобразующий конструктор и обеспечиваем // возможность доступа к закрытым компонентам другого // экземпляра: friend template<typename Т2, typename СР2, typename OP2> class CountingPtr; template<typename S> // S может быть void // или базовым классом Т CountingPtr(CountingPtr<S, OP, CP> constfc cp) : OP((OP const&)cp) , CP((CP const&)cp) , obj ec t_pointed_,to (cp. obj ect_pointed_to) { if (cp.object__pointed_to >= NULL) { CP: : increment (cp.object__pointed_to) ; } } }; Заметим, что в данном случае преобразующий конструктор более легко обеспечивает требующееся неявное преобразование типов, чем оператор преобразования (в частности, необходимо убедиться в корректном копировании счетчика ссылок). Преобразование в тип bool может показаться очень простым: нужно лишь добавить к CountingPtr пользовательский оператор преобразований типа. template<typename Т, typename CounterPolicy = SimpleReferenceCount, typename ObjectPolicy = StandardObjectPolicy> class CountingPtr: private CounterPolicy, private ObjectPolicy {
20.2. Счетчики ссылок 413 public: operator bool() const { return this->object_pointed_to != (T*)0; } }; Данный способ работает, но допускает неожиданные и нежелательные эффекты. Например, при использовании такого преобразования можно сложить два объекта Count- ingPtr! Это достаточно серьезно, чтобы отказаться от приведенного оператора. Преобразование в тип bool нужно для поддержки конструкций вида if (ср) . . . или while (! ср) . . .. Таким образом, разрешить данную проблему можно путем использования преобразования в void* (тип, который неявно преобразуется в bool только там, где это необходимо, причем преобразование выполняется корректно, без побочных эффектов) . Такой подход имеет свои недостатки, но они распространяются на те интеллектуальные указатели, для которых, как мы уже решили, неявное преобразованием в void* не должно применяться. Простое (но часто не замечаемое) решение этой задачи состоит в определении преобразования в тип указателя на член вместо указателя на встроенный тип. Действительно, тип указателя на член также поддерживает неявное преобразование в bool, но, в отличие от обычного указателя, этот тип не может использоваться с оператором delete или в арифметике указателей. Ниже показано, как можно применить описанный метод. template<typename T, typename CounterPolicy = SimpleReferenceCount, typename ObjectPolicy = StandardObjectPolicy> class CountingPtr: private CounterPolicy, private ObjectPolicy { private: class BoolConversionSupport { int dummy; }; public: operator BoolConversionSupport::*() const { return this->object_pointed__to ? &BoolConversionSupport::dummy : 0; } >; Заметим, что данное решение не увеличивает размер CountingPtr, поскольку при этом нет добавления новых данных-членов. Использование закрытого вложенного класса позволяет избежать конфликтов с клиентским кодом. 4 Например, это сделано в классах стандартных потоков C++.
414 Глава 20. Интеллектуальные указатели 20.2.9. Сравнения И в завершение рассмотрим разработку различных операторов сравнения для указателей со счетчиками ссылок. Встроенные указатели поддерживают как операторы проверки на равенство (== и ! =), так и операторы упорядочения (<, <= и т.д.). Для встроенных указателей операторы упорядочения гарантированно работают только для двух указателей, которые указывают на элементы одного и того же массива, но для указателей со счетчиками ссылок это условие обычно не выполняется. Следовательно, рассматривать данные операторы смысла не имеет. (Однако при необходимости эти операторы могут быть реализованы практически так же, как и операторы проверки на равенство.) Ниже приведена реализация оператора == (реализация оператора ! = аналогична). template<typename T, typename\ CounterPolicy = SimpleReferenceCount, typename ObjectPolicy = StandardObjectPolicy> class CountingPtr: private CounterPolicy, private ObjectPolicy { public: friend bool operator { return cp == p; } friend bool operator ==(T const* p, CountingPtr<T,CP,OP> constfc cp) { return p == cp; } }; template<typename Tl, typename T2, typename CP, typename OP> inline bool operator ==(CountingPtr<T,CP,OP> const& cpl, CountingPtr<T,CP,OP> const& cp2) { return cpl.operator->() == cp2.operator->(); } b , Оператор вне класса представляет собой шаблон, который позволяет сравнивать указатели со счетчиками, указывающие на различные типы. Его реализация демонстрирует возможность выделения встроенного указателя, инкапсулированного в CountingPtr. Явный вызов operator-> достаточно необычен, чтобы обратить внимание на потенциальную небезопасность, заключенную в реализации данного оператора. ==(CountingPtr<T,CP,OP> const& cp, Т const* p)
20.3. Заключение 415 Два других оператора являются нешаблонными. Однако поскольку они зависят от параметров шаблонов, то должны быть реализованы как дружественные с определением внутри класса. Так как эти операторы не являются шаблонами, к их аргументам применимо обычное неявное преобразование типов, которое включает неявное преобразование нуля в значение нулевого указателя. 20.3. Заключение Шаблоны интеллектуальных указателей представляют собой, вероятно, второе по важности после контейнеров применение шаблонов. Однако детали такого применения далеки от очевидности, как наглядно продемонстрировано в данной главе. Более подробно этот вопрос рассматривается в книгах других авторов, например [1,24]. Стандартная библиотека C++ содержит шаблон интеллектуального указателя auto_ptr, предназначенного для тех же целей, что и рассмотренная в данной главе пара шаблонов Holder/Trule, но без использования дополнительного шаблона путем применения правил перегрузки C++ в контексте инициализации переменных5. Были и другие шаблоны интеллектуальных указателей, которые предлагалось включить в стандартную библиотеку, однако они были отклонены комитетом по стандартизации C++. В библиотеке Boost можно найти ряд интеллектуальных указателей, предназначенных для решения разнообразных задач [7]. 5 Пояснение данного механизма выходит за рамки тематики книги, поэтому заинтересованный читатель может обратиться к дополнительной литературе, например [16]. Заметим, что шаблон auto_ptr основан, в частности, на механизме, который ряд авторов считают дефектом стандарта C++.
\ \,
Глава 21 Кортежи * В этой книге для иллюстрации возможностей шаблонов часто используются однородные контейнеры и типы, подобные массивам. Такие однородные структуры расширяют концепцию массива в языках программирования С и C++ и часто используются во многих приложениях. В этих языках также есть неоднородные образования, способные хранить данные — классы или структуры. Кортежи — это шаблоны классов, позволяющие объединять объекты разных типов. Начнем рассмотрение с дуэтов (duo) — конструкций, аналогичных стандартному шаблону std: :pair. Здесь будет также показано, каким образом дуэты могут быть вложены друг в друга, собирая воедино произвольное количество членов: формируя тройки, четверки и т.д.1 21.1. Класс Duo Дуэт— это тип, объединяющий два объекта. Этот тип подобен классу std: :pair, входящему в состав стандартной библиотеки. Однако в рассматриваемый здесь класс добавлены новые функциональные возможности, несколько отличающиеся от возможностей стандартного класса, поэтому и имя для него выбрано другое. В самом простом виде конструкцию Duo можно определить так: template <typename Tl, typename T2> struct Duo { Tl vl; // Значение первого поля T2 v2; // Значение второго поля }; Такая структура может пригодиться, например, в качестве типа, возвращаемого функцией, результаты работы которой могут быть как корректными, так и неверными. Duo<bool,X> result = foo(); if (result.vl) { Количество собранных воедино элементов не может быть полностью произвольным, поскольку максимальная глубина последовательных вложений шаблонов ограничена и зависит от реализации компилятора C++.
418 Глава 21. Кортежи // Результат корректный; // его значение содержится в result.v2. } Возможны и другие применения структуры Duo. Структура, определенная в виде шаблона, обладает некоторым преимуществом перед обычной, но это преимущество незначительное. В конце концов, не так уж сложно задать структуру с двумя полями, что позволит выбирать для этих полей осмысленные имена. Однако продолжим расширять нашу базовую конструкцию, пытаясь сделать ее более удобной в применении. Для начала добавим в нее конструкторы. template <typename Tl, typename T2> class Duo { public: Tl vl; // Значение первого поля T2 v2; // Значение второго поля // Конструкторы. ^ Duo () : vl(), v2() { } Duo (Tl constfc a, T2 constfc b) : Vila), v2(b) { } }; Обратите внимание, что для конструктора по умолчанию используется список инициализации, поэтому переменные-члены встроенных типов инициализируются нулями (см. раздел 5.5, стр. 78). Чтобы избежать необходимости явного указания типов параметров, можно добавить функцию, обеспечивающую вывод типов полей. template <typename Tl, typename T2> inline Duo<Tl,T2> make_duo (Tl constfc a, T2 constfc b) { return Duo<Tl,T2>(a,b); } В результате создание и инициализация объектов класса Duo упрощается. Вместо последовательности инструкций Duo<bool, int> result; result.vl = true; result.v2 = 42; return result; можно написать return make__duo (true, 42) ;
21.1. Класс Duo 419 Хорошие компиляторы C++ способны выполнить такую оптимизацию этой инструкции, что сгенерированный код будет эквивалентен return Duo<bool,int>(true,42); Еще одно усовершенствование класса Duo состоит в том, чтобы предоставить доступ к типам его полей, что позволит надстраивать поверх этого класса шаблоны-адаптеры. template <typename Tl, typename T2> class Duo { public: typedef Tl Typel; // Тип первого поля typedef T2 Type2; // Тип второго поля enum { N = 2 }; // Количество полей Tl vl; Т2 v2; // Значение первого поля // Значение второго поля }; // Конструкторы Duo () : vl(), v2() { } Duo (Tl const& a, T2 const& b) : vl(a), v2(b) { } На данном этапе разработки шаблон очень похож на реализацию шаблона std: :pair. Однако между этими шаблонами есть ряд различий: • разные имена; • в шаблон Duo добавлен член N, обозначающий количество полей; • в нашем шаблоне нет инициализации его членов, позволяющей выполнять неявное преобразование типов в процессе конструирования объекта; • в шаблоне Duo отсутствуют операторы сравнения. Более полная и аккуратная реализация шаблона класса Duo могла бы выглядеть так, как показано ниже. // tuples/duol.hpp #ifndef DUO_HPP #define DUO HPP template <typename Tl, class Duo { public: typedef Tl Typel; typename T2> // Тип первого поля typedef T2 Type2-; // Тип второго поля enum { N = 2 }; // Количество полей
private: Tl valuel; // Значение первого поля. T2 value2; // Значение второго поля. public: \ // Конструкторы Duo() : valuel(), value2() { } Duo (Tl const & a, T2 const & b) : valuel(a), value2(b) { } // Для неявного преобразования типов //в процессе создания объекта template <typename Ul, typename U2> Duo (Duo<Ul,U2> const & d) : valuel(d.vlO), value2 (d.v2 () ) { / // Для неявного преобразования типов //в процессе присвоения template <typename Ul, typename U2> Duo<Tl, T2>& operator = (Duo<Ul,U2> const & d) valuel = d.valuel; value2 = d.value2; return *this; } // Доступ к полям T1& vl() { return valuel; } Tl const& vl() const { return valuel; } T2& v2() { return value2; } T2 constSc v2 () const { return value2; } // Операторы сравнения
21.1. Класс Duo 421 // (позволяют работать со смешанными типами): template <typename Tl, typename T2, typename Ul, typename U2> inline bool operator == (Duo<Tl,T2>const& dl, Duo<Ul,U2>const& d2) { return dl.vl() == d2.vl() && dl.v2() == d2.v2(); } template <typename Tl, typename T2, typename Ul, typename U2> inline bool operator != (Duo<Tl,T2> const& dl, Duo<Ul,U2> constfc d2) { return !(dl==d2); } // Функция для удобства создания и инициализации объектов template <typename Tl, typename T2> inline Duo<Tl,T2> make_duo (Tl const & a, T2 const & b) { return Duo<Tl,T2>(a,b); } #e^idif // DUO__HPP Перечислим внесенные изменения: • переменные-члены объявлены как закрытые, а в класс добавлены функции доступа; • благодаря явной инициализации обеих переменных-членов в конструкторе по умолчанию template <typename Tl, typename T2> class Duo { Duo() : valuel(), value() { } } можно быть уверенным в том, что значения встроенных типов будут инициализированы нулями (см. раздел 5.5, стр. 78); • в класс добавлены шаблоны-члены, что позволяет создавать и инициализировать объекты с использованием смешанных типов;
422 Глава 21. Кортежи • объявлены операторы сравнения == и !=; обратите внимание, что в шаблонах вводятся разные наборы параметров, чтобы можно было сравнивать объекты, в которые входят пары значений разных типов. Все шаблоны-члены позволяют выполнять операции со смешанными типами. Это означает, что можно выполнять инициализацию, присвоение и сравнивание таких объектов класса Duo, для которых необходимо неявное преобразование типов, например: // tuples/duol.cpp tinclude "duol.hpp" Duo<float,int> foo () { return make_duo(42,42); } int main () if (foo() U make_duo(42,42.0)) { //... } } В представленном фрагменте программы в теле функции foo () выполняется преобразование типа Duo<int, int>, возвращаемого функцией make_duo(), в тип Duo< float, int>, возвращаемый функцией foo (). Далее объект этого типа сравнивается с объектом, который возвращается функцией make_duo (42,42 .0) и принадлежит типу Duo<int,double>. Для объединения в одну конструкцию трех и большего количества значений можно было бы создать шаблоны Trio и ему подобные. Однако есть более структурированный подход, — помещение объектов класса Duo друг в друга. Эта идея и является темой следующих разделов. 21.2. Рекурсивное вложение объектов класса Duo Рассмотрим такое объявление объекта: Duo<int, Duo<char/ Duo<bool, double> > > q4; Объект q4 принадлежит так называемому типу рекурсивного дуэта (recursive duo), ин- станцированному из шаблона Duo. Аргумент, стоящий на месте второго параметра типа, сам является типом Duo. Рекурсию можно было бы также организовать с помощью первого параметра, однако в оставшейся части этой главы будет принято соглашение, в соответствии с которым под рекурсивными дуэтами подразумеваются только такие объекты класса Duo, в которых из шаблона Duo инстанцируется второй параметр типа.
21.2. Рекурсивное вложение объектов класса Duo 423 21.2.1. Количество полей Легко подсчитать, что объект q4 объединяет в себе четыре значения, принадлежащих типам int, char, bool и double. Чтобы облегчить формальный подсчет количества полей, можно частично специализировать шаблон Duo. // tuples/duo2.hpp template <typenamie A, typename B, typename C> class Duo<A, Duo<B,C> > { public: typedef A Tl; // Тип первого поля typedef Duo<B,C> T2; // Тип второго поля enum { N = Duo<B/C>::N + 1 }; // Количество полей private: Tl valuel; T2 value2; // Значение первого поля // Значение второго поля public: // Остальные открытые члены оставляем без изменений }; Определим для полноты частичную специализацию шаблона Duo такую, чтобы это хранилище неоднородных объектов можно было представить в вырожденном виде, содержащем только одно поле. // tuples/duo6.hpp // Конкретизация шаблона Duo<> с одним полем, template <typename A> struct Duo<A,void> { public: typedef A Tl; typedef void T2; enum { N = 1 }; private: Tl valuel; / public: // Конструкторы. Duo() : valuel() { } Duo (Tl const & a) : valuel(a) { } // Тип первого поля. // Тип второго поля. // Количество полей. // Значение первого поля.
424 Глава 21. Кортежи // Доступ к полю. Т1& vl() { return valuel; } Tl const& vl() const { return valuel; } void v2() { } void v2() const { } }; Обратите внимание, что в этой конкретизации шаблона функции-члены v2 () не выполняют никаких действий; нужны они исключительно для строгости. 21.2.2. Типы полей На самом деле рекурсивные дуэты не так удобны, как, скажем, классы Trio (трио) или Quartet (квартет), которые несложно было бы создать по аналогии с классом Duo. Например, чтобы получить доступ к третьему значению объекта q4, объявленному в начале этой главы, понадобится выражение, подобное такому: q4.v2() .vl() Его трудно назвать компактным или интуитивно понятным. К счастью, рекурсивные шаблоны можно организовать так, чтобы из рекурсивных дуэтов можно было эффективно извлекать значения и типы полей. Рассмотрим сначала код функции типа DuoT, возвращающей тип и-го поля рекурсивного дуэта (код этой функции можно найти в файле tuples/duo3 .hpp). Обобщенное определение // Первичный шаблон для типа N-ro поля (дуэта) Т template <int N, typename T> class DuoT { public: typedef void ResultT; // В общем случае' тип // результата — void. }; гарантирует, что для всех типов, отличающихся от Duo, результирующим будет тип void. Благодаря простой частичной специализации будет получена возможность запрашивать типы полей, входящих в состав нерекурсивных объектов^сласса Duo. // Специализация для 1-го поля обычного дуэта template <typename A, typename B>
21.2. Рекурсивное вложение объектов класса Duo 425 class DuoT <1, Duo<A,B> > { public: typedef A ResultT; }; // Специализация для 2-го поля обычного дуэта template <typename A, typename B> class DuoT <2, Duo<A,B> > { public: typedef В ResultT; }; Заметим, что и-й тип рекурсивного дуэта — это (и-1)-й тип его второго поля. // Специализация для N-ro поля рекурсивного дуэта template <int N, typename A, typename B, typename C> class DuoT<N, Duo<A, Duo<B,C> > > { public: typedef typename DuoT<N-l, Duo<B,C> >::ResultT ResultT; }; Запрос типа первого поля рекурсивного дуэта останавливает рекурсию. // Специализация для 1-го поля рекурсивного дуэта, template <typename A, typename В, typename C> class DuoT<l, Duo<A, Duo<B,C> > > { public: typedef A ResultT; }; Отметим, что нужно также специализировать случай второго поля рекурсивного дуэта, чтобы избежать неоднозначности в нерекурсивном случае. // Конкретизация для 2-го поля рекурсивного дуэта template <typename A, typename В, typename C> class DuoT<2, Duo<A, Duo<B,C> > > { public: typedef В ResultT; }; Конечно, это не единственный способ реализации шаблона DuoT. Один из альтернативных подходов— воспользоваться шаблоном IfThenElse (см. раздел 15.2.4, стр. 298), с помощью которого можно достичь аналогичного эффекта. 21.2.3. Значения полей Извлечение из рекурсивного дуэта и-го значения (в виде lvalue) лишь немного сложнее, чем извлечение соответствующего типа. Мы стремимся к тому, чтобы этот вызов имел вид val<i\7> {duo). Однако для решения поставленной задачи понадобится вспо-
426 Глава 21. Кортежи могательный шаблон класса DuoValue, поскольку только для шаблонов классов можно задавать частичную специализацию, а именно она позволит нам осуществить эффективный рекурсивный доступ к нужному значению. Ниже показано, как функция val () делегирует свою задачу доступа. // tuples/duo5.hpp # include "typeop.hpp" // Возврат N-го значения дуэта' template <int N, typename A, typename B> inline typename TypeOp<typename DuoT<N,Duo<A, B> >::ResultT>::RefT val(Duo<A,B>& d) { return DuoValue<N, Duo<A, B> >::get(d); } // Возврат N-го значения дуэта-константы template <int N, typename A, typename B> inline typename TypeOp<typekame DuoT<N, Duo<A, B> >::ResultT> \ ::RefConstT val(Duo<A,B> const& d) { return DuoValue<N, Duo<A, B> >::get(d); } Шаблон DuoT уже использовался в предыдущем разделе, а сейчас он оказался полезным в функции val () при возвращении типа объекта. Кроме того, здесь применяется функция типа ТуреОр из раздела 15.2.3 (стр. 295), благодаря которой создается ссылочный тип, даже если тип поля сам является ссылкой. Приведенная ниже полная реализация класса DuoValue аналогична обсуждавшейся ранее реализации класса DuoT (элементы этой реализации рассматриваются позже). // tuples/duo4.hpp # include "typeop.hpp" // Первичный шаблон для значения N-го поля (дуэта) Т template <int N, typename T> class DuoValue { public: static void get(T&) { // В общем случае } // значение отсутствует static void get(T constfc) { } };
21.2. Рекурсивное вложение объектов класса Duo 427 // Специализация для 1-го поля обычного дуэта template <typename A, typename B> class DuoValue<l, Duo<A, B> > { public: static A& get(Duo<A, B> &d) { return d.vlO ; } static A const& get(Duo<A/ B> const &d) { return d.vlO ; } }; // Специализация для 2-го поля обычного дуэта template <typename A, typename B> class DuoValue<2, Duo<A, B> > { public: static B& get(Duo<A, B> &d) { return d.v2(); } ' static В const& get(Duo<A, B> const &d) { return d.v2(); } }; // Специализация для N-го поля рекурсивного дуэта template <int N, typename A, typename B, typename C> struct DuoValue<N# Duo<A, Duo<B,C> > > { static typename TypeOp<typename DuoT<N-l, Duo<B,C> >::ResultT>::RefT get(Duo<A, Duo<B,C> > &d) { return DuoValue<N-l, Duo<B,C> >::get(d.v2()); } static typename TypeOp<typename DuoT<N-l, Duo<B,C> >::ResultT>::RefConstT get(Duo<A, Duo<B,C> > const &d) { return DuoValue<N-l/ Duo<B,C> >::get(d.v2()); } }; // Специализация для 1-го поля рекурсивного дуэта template <typename A, typename В, typename C> class DuoValue<l, Duo<A, Duo<B,C> > > { public: static A& get(Duo<A, Duo<B,C> > &d) { return d.vl(); }
428 Глава 21. Кортежи static A const& get(Duo<A, Duo<B,C> > const &d) { return d.vl(); } }; // Специализация для 2-го поля рекурсивного дуэта, template <typename A, typename В, typename C> class DuoValue<2, Duo<A, Duo<B,C> > > { public: static B& get(Duo<A, Duo<B,C> > &d) { return d.v2().vl(); } static В const& get(Duo<A, Duo<B,C> > const &d) { return d.v2().vl(); } }; Как и в классе DuoT, в обобщенном определении класса DuoValue происходит отображение на функции, возвращающие результат типа void. Шаблоны функций могут возвращать выражения типа void, благодаря чему функция val () применима к объектам, тип которых отличен от Duo или которые выходят за рамки допустимых значений N (это упрощает реализацию некоторых шаблонов). // Первичный шаблон для значения N-ro поля (дуэта) Т template <int N, typename T> class DuoValue { public: static void get(T&) { // В общем случае } // значение отсутствует, static void get(T constfc) { . } }; Как и ранее, сначала специализируется случай нерекурсивных дуэтов. // Специализация для 1-го поля обычного дуэта template <typename A, typename B> class DuoValue<l, Duo<A, B> > { public: static A& get(Duo<A, B> &d) { return d.vl(); } static A const& get(Duo<A/ B> const &d) { return d.vl(); } } ; Затем мы переходим к рекурсивным дуэтам (здесь снова пригодится шаблон DuoT).
21.2. Рекурсивное вложение объектов класса Duo 429 template <int N, typename A, typename B, typename C> class DuoValue<N, Duo<A, Duo<B,C> > > { public: static typename TypeOp<typename DuoT<N-l, Duo<B,C> >::ResultT> . ::RefT get(Duo<A, Duo<B,C> > &d) { return DuoValue<N-l/ Duo<B,C> >: :get (<J.v2 () ) ; } }; // Специализация для 1-го поля рекурсивного дуэта template <typename A, typename В, typename C> class DuoValue<l, Duo<A, Duo<B/C> > > { public: static A& get(Duo<A, Duo<B/C> > &d) { return d.vl(); } }; // Специализация для 2-го поля рекурсивного дуэта template <typename A, typename В, typename C> class DuoValue<2, Duo<A, Duo<B,C> > > { public: static B& get(Duo<A/ Duo<B,C> > &d) { return d.v2().vl(); } }; В приведенной ниже программе демонстрируется, как используются дуэты. // tupples/duo5.срр # include "duo1.hpp" #include "duo2.hpp" #include "duo3.hpp" #include "duo4.hpp" #include "duo5.hpp" #include <iostream> int main() { // Создание и использование обычного дуэта Duo<bool,int> d; std::cout « d.vl() « std::endl; std::cout « val<l>(d) « std::endl;
430 Глава 21. Кортежи // Создание и использование трио Duo<bool,Duo<int,float> > t; val<l>(t) = true; val<2>(t) =42; val<3>(t) = 0.2; std::cout « val<l>(t) « std::endl; std::cout « val<2>(t) « std::endl; std::cout « val<3>(t) « std::endl; } Вызов функции val<3>(t) в конечном счете означает следующее: t.v2() .v2() Поскольку на этапе компиляции рекурсия раскрывается путем инстанцйрования шаблонов, а функции представляют собой обычные встроенные функции доступа, приведенный выше код оказывается весьма эффективным. Хорошие компиляторы преобразуют такие программы в код, эквива^нтный коду доступа к полям обычной структуры. Однако объявление и создание рекурсивных объектов класса Duo все еще выглядит громоздко. Следующий раздел посвящен этому вопросу. 21.3. Создание класса Tuple Благодаря вложенной структуре рекурсивных дуэтов к ним удобно применять методы шаблонного метапрограммирования. Однако программисту приятнее работать с интерфейсом этих конструкций в таком виде, будто это интерфейс обычной структуры. Для этого можно определить рекурсивный шаблон Tuple со многими параметрами, производный от рекурсивного типа Duo соответствующего размера. Ниже приводится код этого класса, в котором количество полей не превышает пяти, однако создать подобный шаблон с десятью или двенадцатью полями ненамного сложнее. Код шаблона Tuple можно найти в файле tuples / tuplel .hpp. Если количество полей в объектах типа Tuple будет меняться, то некоторые параметры типа останутся неиспользованными. По умолчанию эти параметры принадлежат типу NullT, который определен именно для этого использования в качестве заполнителя. Тип NullT пришлось применить вместо типа void потому, что тип void не может выступать в роли параметра. // Тип, представляющий неиспользованные параметры типа class NullT { }; Шаблон класса Tuple определен как производный от шаблона класса Duo, в котором на один параметр больше (последний параметр имеет тип NullT).
21.3. Создание класса Tuple 431 // Шаблон Tupleo является производным от шаблона Duo<>, //в котором на один параметр типа NullT больше template <typename PI, typename P2 = NullT, typename P3 = NullT, typename P4 = NullT, typename P5 = NullT> class Tuple : public Duo<PI,typename Tuple<P2,РЗ,Р4,P5,NullT>::BaseT> { public: typedef Duo<Pl,typename Tuple<P2,P3,P4,P5,NullT>::BaseT> BaseT; // Конструкторы: Tuple() {} Tuple(TypeOp<Pl>::RefConstT al, Type0p<P2>::RefConstT a2, Type0p<P3>: :RefConstT a3 = NullTO, Type0p<P4>: :RefConstT a4 = NullTO, Type0p<P5>: :RefConstT a5 =.NullTO) : BaseT(al, Tuple<P2,РЗ,Р4,P5,NullT>(a2,a3,a4,a5)) { } }; Обратите внимание, как происходит сдвиг параметров при их передаче в ходе шага рекурсии. Поскольку в базовом типе, который мы наследуем, определены типы-члены Т1 и Т2, в производном классе вместо обычных идентификаторов Тп используются идентификаторы Рл . Для завершения рекурсии понадобится специализация, которая относится к нерекурсивному дуэту. // Специализация, завершающая рекурсию наследования template <typename PI, typename P2> class Tuple<Pl,P2,NullT,NullT,NullT> : public Duo<Pl,P2> { public: typedef Duo<Pl,P2> BaseT; Tuple() {} Tuple(TypeOp<Pl>::RefConstT al, Type0p<P2>::RefConstT a2, TypeOp<NullT>: : Re f Const T = NullTO, TypeOp<NullT>: :RefConstT = NullTO, 2 Согласно правилу поиска имен, принятому в C++, имена, наследуемые от независимых базовых классов, обрабатываются раньше, чем имена параметров шаблонов. В данном случае это не должно вызывать проблем, поскольку базовый класс является зависимым. Однако некоторые компиляторы из имеющихся в наличии во время написания данной книги неправильно воспринимали рассматриваемые конструкции.
432 Глава 21. Кортежи TypeOp<NullT>: :RefConstT = NullTO) : BaseT(al, a2) { } }'; Например, объявление Tuple<bool,int,float,double> t4(true,42,13,1.95583); приводит к цепочке наследований, показанной на рис. 21.1. Duo< Ifloatl , I double! > Duo< \Ш . lyuple<float,doublefN^lT,NullTrNullT>l > Рио<ГЬооП ,{Tuple<lnt. float.double.NullT.NullTH > Tuple<bool,int/float,double,NU11T> Рис. 21.1. Тип объекта Tuple<bool, int, float, double> Другая специализация предусматривается на случай, когда дуэт на самом деле содержит одно поле. // Специализация для объекта с одним полем template <typename Pl> class Tuple<Pl,NullT,NullT,NullT,NullT> : public Duo<Pl,void> { public: typedef Duo<Pl,void> BaseT; Tuple() {} Tuple(TypeOp<Pl>::RefConstT al, TypeOp<NullT>: :RefConstT = NullTO, TypeOp<NullT>: :RefConstT = NullTO, TypeOp<NullT>: :RefConstT = NullTO, TypeOp<NullT>: :RefConstT = NullTO) : BaseT(al) { } }; Наконец, возникает естественное желание иметь в своем распоряжении функцию, аналогичную определенной в разделе 21.1, стр. 417, функции make_duo (), с помощью
21.3. Создание класса Tuple 433 которой можно было бы автоматически выводить параметры шаблона. К сожалению, шаблон этой функции приходится объявлять отдельно для объектов класса Tuple каждого возможного размера, поскольку в шаблонах функций нельзя применять аргументы шаблонов по умолчанию3, а применяемые по умолчанию аргументы функций не принимают участия в процессе вывода параметров шаблонов. Ниже приведены определения этих функций. // Функция с одним аргументом template <typename Tl> inline Tuple<Tl> make_tuple(Tl const &al) { return Tuple<Tl>(al); } // Функция с двумя аргументами template <typename Tl, typename T2> inline Tuple<Tl/T2> make_tuple(Tl const &al, T2 const &a2) { return Tuple<Tl,T2>(al,a2); } // Функция с тремя аргументами template <typename Tl, typename T2, typename T3> inline Tuple<Tl,T2/T3> make_tuple(Tl const &al, T2 const &a2, T3 const &a3) { return Tuple<Tl,T2,T3 >(al,a2,a3); } // Функция с четырьмя аргументами template <typename Tl, typename Т2, typename ТЗ, typename T4> inline Tuple<Tl,T2,T3,T4> make_tuple(Tl const &al, T2 const &a2, T3 const &аЗ# Т4 const &a4) { return Tuple<Tl/T2/T3/T4>(al,a2/a3,a4); } // Функция с пятью аргументами template <typename Tl, typename T2, typename T3, При пересмотре стандарта C++ это ограничение, скорее всего, будет снято (см. раздел 13.3, стр.233).
434 Глава 21. Кортежи typename T4, typename T5> inline Tuple<Tl,T2,T3,T4,T5> make_tuple(Tl const &al, T2 const &a2, ТЗ const &a3, T4 const &a4, T5 const &a5) { return Tuple<Tl,T2,T3,T4,T5>(al,a2,a3,a4,a5); } Ниже приведена программа, иллюстрирующая применение объектов класса Tuple. // tuples/tuplel.cpp #include "tuplel.hpp" #include <iostream> int main() { // Создание и использование объекта класса Tuple, // содержащего только одно поле Tuple<int> tl; ^ val<l>(tl) += 42; std::cout « tl.vl() « std::endl; // Создание и использование дуэта Tuple<bool,int> t2; std::cout « val<l>(t2) « "f n; std::cout « t2.vl() « std::endl; // Создание и использование трио Tuple<bool,int,double> t3; val<l>(t3) = true; val<2>(t3) =42; val<3>(t3) =0.2; std::cout « val<l>(t3) « ", std::cout « val<2>(t3) « ", std::cout « val<3>(t3) « std::endl; t3 = make__tuple(false, 23, 13.13); std::cout « val<l>(t3) « ■, " std::cout « val<2>(t3) « ", » std::cout « val<3>(t3) « std::endl;
21.4. Заключение 435 // Создание и использование квартета Tuple<bool,int,float,double> t4(true,42,13,1.95583); std::cout « val<4>(t4) « std::endl; std::cout « t4,v2().v2().v2() « std::endl; } б реализациях, предназначенных для промышленного применения, представленный ранее код класса Tuple следовало бы дополнить. Например, можно было бы задать шаблоны оператора присвоения, облегчающие преобразование объектов этого класса. Если этого не сделать, то типы соответствующих полей должны точно соответствовать друг другу. Tuple<bool,int,float> t3; t3 = make_tuple(false, 23, 13.13); // ОШИБКА: значение 13.13 // имеет тип double 21.4. Заключение Разработка кортежей — одна из областей применения шаблонов, в которой многие программисты работают независимо друг от друга. Особенности создаваемых ими версий очень различаются, однако в основе многих реализаций лежит идея структуры рекурсивных дуэтов (подобных рассмотренным в данной главе). Одан из интересных подходов предложен Андреем Александреску (Andrei Alexandrescu) [1]. Он четко отделил список типов от списка полей кортежа. В результате вводится понятие списка типов (type list), которое находит самые разные применения (одно из них — конструирование кортежей с инкапсулированными типами). В разделе 13.13, стр. 248, рассматривается понятие списочного параметра, представляющего собой расширение языка, с помощью которогЬ реализация кортежей почти тривиальна.
Глава 22 Объекты-функции и обратные вызовы Объект-функция (function object), также называемый функтором (functor), представляет собой любой объект, который можно вызывать с помощью синтаксиса вызова функции. В языке программирования С к такому синтаксису приводит использование конструкций трех видов: функций, функциеобразных макросов и указателей на функции. Поскольку функции и макросы — это не объекты, получается, что единственными функторами, доступными в С, являются указатели на функции. Язык C++ предоставляет больше возможностей. Оператор вызова функции может быть перегружен для классов, существует концепция ссылки на функцию, а кроме того, синтаксис вызова функции имеют функции-члены и указатели на них. Нельзя сказать, что все эти концепции одинаково полезны, однако сочетание понятия функтора с параметризацией времени компиляции, возможной благодаря шаблонам, приводит к новым мощным методам программирования. Кроме разработки типов функторов, в этой главе большое внимание уделяется также идиомам их использования. При работе с функторами почти неизбежно приходится иметь дело с обратными вызовами (callback) — ситуацией, когда пользователю библиотеки нужно, чтобы библиотечная функция вызвала некоторую функцию, определенную в коде пользователя. В качестве классического примера можно привести функцию сортировки, для работы которой нужна функция, способная сравнивать два элемента из сортируемого набора данных. В этом случае функция сравнения передается в функцию сортировки в качестве функтора. Традиционно термин обратный вызов применяется для функторов, которые передаются в аргументах вызова функций (в противоположность, например, аргументам шаблонов), и мы будем придерживаться этой традиции. К сожалению, термины объект-функция (function object) и функтор (functor) несколько неоднозначны в том смысле, что разные члены сообщества программистов на C++ иногда трактуют их немного по-разному. Чаще всего различия в определениях касаются того, причислять ли к функторам (или объектам-функциям) только объекты классов, или указатели на функции также являются функторами. Кроме того, нередко приходится читать или слышать, как в ходе обсуждения типы классов объектов-функций называют объектами-функциями. Другими словами, фраза "класс объектов-функций то-то и то-то..." сокращается до "объекты-функции то-то и то-то...". Несмотря на то что в по-
438 Глава 22. Объекты-функции и обратные вызовы вседневной работе эта терминология применяется несколько йебрежно, она положена в основу данной главы, причем в том виде, в котором определена в ее начале. Прежде чем углубиться в особенности реализации функторов с помощью шаблонов, обсудим некоторые свойства вызовов функций, которыми объясняются определенные преимущества функторов, основанных на шаблонах. 22Л. Прямые, непрямые и встраиваемые вызовы Обычно, наталкиваясь на определение невстраиваемой функции, компилятор С или C++ генерирует ее код, который заносится в объектный файл. Кроме того, он создает имя, связанное с этим машинным кодом. В языке С это имя, как правило, совпадает с именем самой функции, а в C++ оно обычно дополняется закодированными типами параметров, благодарящему обеспечиваются уникальные имена перегруженных функций (полученное в результате имя часто называют скорректированным именем (mangled name), иногда также применяется термин декорированное имя (decorated name)). Аналогично, когда компилятору встречается вызов функции, например f(); он генерирует машинный код для вызова функции данного типа. В большинстве языков программирования для выполнения команды вызова необходим начальный адрес вызываемой подпрограммы. Этот адрес может быть составной частью инструкции (в таком случае инструкция называется прямым вызовом (direct call)) либо храниться где-то в памяти или в аппаратном регистре (непрямой, или косвенный вызов (indirect call)). Почти во всех современных архитектурах вычислительных систем в наличии имеются инструкции вызова подпрограмм обоих типов. По причинам, описание которых выходит за рамки настоящей книги, прямые вызовы обрабатываются эффективнее, чем косвенные. Фактически по мере усложнения архитектуры компьютеров разница в производительности этих двух видов вызовов возрастает, поэтому компиляторы по возможности пытаются сгенерировать инструкцию прямого вызова. В общем случае компилятору неизвестно, цо какому адресу расположена функция (например, она может находиться в другой единице трансляции). Однако если компилятору известно имя функции, он генерирует инструкцию прямого вызова с фиктивным адресом. Кроме того, в объектном файле с телом функции генерируется входная точка, благодаря которой компоновщик получает возможность заменить инструкцию вызова с фиктивным адресом инструкцией, в которую подставлен адрес функции с указанным именем. Поскольку компоновщик имеет доступ ко всем единицам трансляции, он располагает информацией о расположении инструкций вызова и объектных файлов функций и поэтому всегда способен организовать прямые вызовы1. Аналогичную роль компоновщик играет, например, при доступе к переменным, заданным в определенных пространствах имен.
22.1. Прямые, непрямые и встраиваемые вызовы 439 К сожалению, если имя функции неизвестно, приходится применять непрямые вызовы. Обычно это бывает тогда, когда функции вызываются с помощью указателей. void foo(void (*pf)()) { • ' . ' pf(); // Непрямой вызов с помощью // указателя на функцию pf } В приведенном примере компилятор не может знать, на какую функцию указывает параметр pf (в другом месте функция f оо () может быть вызвана с другим указателем в качестве аргумента), поэтому компоновщик не может ориентироваться на имена. Адрес вызова остается неизвестным до непосредственного выполнения кода функции. Несмотря на то что на современных компьютерах инструкции прямого вызова часто выполняются почти так же быстро, как и любые другие (например, инструкции сложения двух целых чисел), они все же могут серьезно снизить производительность работы программы. Продемонстрируем это на примере. int fl(int const & г) return ++(int&)r; // Нерационально, но законно int f2(int const & г) return г; int f3() return 42; int foo() int param = 0; int answer = 0; answer = f 1 (param) ; f2(param) ; f 3 () ; return answer + param; } Функция f 1 () содержит аргумент типа const int, который передается по ссылке. Обычно это означает, что объект, который передается в функцию, не модифицируется в ней. Однако, если значение передаваемой величины поддается изменению, в программе на C++ модификатор const может быть законным путем отменен, а значение величины — модифицировано. (Можно возразить, что так делать неразумно, однако стандарт
440 Глава 22. Объекты-функции и обратные вызовы C++ допускает подобные операции.) Именно это и происходит в функции £1 (). В силу вышесказанного компилятор, оптимизирующий сгенерированный код в пределах каждой функции (по такому принципу работает большинство компиляторов), должен учитывать, что в любой функции, в которую передаются ссылки или указатели на объект, этот объект может быть изменен. При этом нужно учесть, что в общем случае компилятору доступна информация только об объявлении функции, поскольку ее определение (или реализация) часто находится в другой единице трансляции. Таким образом, в приведенном выше примере большинство компиляторов предполагают, что в функции f 2 () переменная par am тоже может быть модифицирована (несмотря на то, что на самом деле это не так). Фактически компилятор даже не может предполагать, что функция f 3 () не модифицирует локальную переменную par am. Действительно, ведь у функций f 1 () и f 2 () во время их вызова была возможность сохранить адрес этой переменной в глобально доступном указателе. Поэтому с точки зрения компилятора^обладающего неполными сведениями, нет ничего невозможного в том, что в функции f 3 () значение переменной param будет изменено с помощью этого глобального указателя. В результате обычные вызовы функций усложняют работу большинства компиляторов, заставляя их сохранять промежуточные значения многих переменных не в регистрах процессора, а в основной памяти, доступ к которой осуществляется несколько дольше. Это мешает выполнить многие виды оптимизаций, для которых бы понадобилось перемещение машинного кода (вызов функции часто создает барьер такому перемещению). Существуют компиляторы C++, способные отследить многие подобные случаи использования псевдонимов (в области видимости функции f 1 () выражение г — это псевдоним объекта, который в области видимости функции f оо () носит имя param). Однако это достигается за счет уменьшения скорости компиляции, увеличения расхода ресурсов и снижения степени надежности полученного кода. Проекты, на компиляцию которых с помощью обычных компиляторов потребовалось бы несколько минут, в таких системах компилируются несколько часов, а иногда и дней (при условии, что в распоряжении компилятора имеется несколько гигабайт памяти). Более того, компиляторы, о которых идет речь, как правило, намного сложнее, чем обычные, поэтому вероятность генерации неправильного кода в них выше. Даже если компилятор с повышенной степенью оптимизации сгенерирует правильный код, в исходном коде могут содержаться непреднамеренные нарушения запутанных правил использования псевдонимов в языках программирования С и C++2. При обычной оптимизации такие нарушения могут оказаться вполне безобидными, но при улучшенной оптимизации они превращаются в ошибки. Однако значительную помощь обычным оптимизаторам может оказать процесс встраивания функций. Предположим, что функции f 1 (), f 2 () и f 3 () объявлены как встраиваемые. В этом случае компилятор преобразует код функции f оо () таким образом, что он будет эквивалентен приведенному ниже. В качестве примера такой ошибки можно привести попытку доступа к переменной типа unsigned iiit с помощью обычного указателя на int (со знаком).
22.2. Указатели и ссылки на функции 441 int foo'() { int param = 0; int answer = 0; answer = ++(int&)param; return answer + param; } Даже обычный оптимизатор способен преобразовать этот код в следующий: int foo''() { return 2; } Из этого примера видно, что преимущество использования встраиваемых функций не только в том, что они помогают избежать выполнения машинного кода, в котором вызывается последовательность функций, но и в том (и зачастую это важнее), что благодаря им оптимизатор получает информацию о процессах, происходящих с переменными, которые передаются в функции. Причем же тут шаблоны? Скоро вы убедитесь, что с их помощью иногда можно организовать обратные вызовы, в процессе компиляции которых генерируется код, включающий в себя прямые, а иногда даже встраиваемые вызовы, в то время как обычные обратные вызовы привели бы к использованию косвенных вызовов. При этом мбжно достичь существенного повышения производительности работы полученного исполняемого файла. 22.2. Указатели и ссылки на функции Рассмотрим приведенное ниже довольно простое определение функции foo (). 'extern "C++" void foo() throw() { } Можно было бы охарактеризовать тип этой функции как "функция без аргументов со связыванием C++, которая не возвращает значений и не генерирует исключений". По историческим причинам в формальном определении функции в языке C++ спецификация исключения не входит в описание типа функции3. Однако в будущем ситуация может измениться. В любом случае разумно при создании новых функций указывать их спецификации исключений. Связывание имен (обычно "С" и "C++") представляет собой, по сути, часть типа, но во многих реализациях языка C++ это условие соблюдается недостаточно строго. В частности, в таких системах вполне допустимо указателю на функцию со Исторические причины этого не совсем понятны, и в данной области стандарт C++ несколько непоследователен.
442 Глава 22. Объекты-функции и обратные вызовы связыванием С присваивать значение указателя на функцию со связыванием C++ и наоборот. Это следствие того, что в большинстве платформ соглашения о вызовах, применимые для функций С и C++, идентичны, если эти функции обладают одинаковым подмножеством параметров и возвращают значения одних и тех же типов. В большинстве ситуаций выражение f оо неявным образом преобразуется в указатель на функцию f оо (). Заметим, что сам идентификатор f оо не является обозначением указателя, точно так же, как выражение ia после объявления int ia[10]; не обозначает указатель на массив (или на его первый элемент). Такое неявное преобразование функции (или массива) в указатель часто называют сведением (decay). Чтобы проиллюстрировать это преобразование, рассмотрим завершенную программу на C++, которая приведена ниже. // functors/funcptr.cpp #include <iostream> #include <typeinfo> void foo() { stditcout « "foo() called" « std::endl; } typedef void FooTO; // FooT — это тип функции, // совпадающий с типом функции fоо() int main() { fоо(); // Прямой вызов // Вывод типов foo и FooT std::cout « "Types of foo: " « typeid(foo).name() « '\n'; std::cout « "Types of FooT: " « typeid(FooT).name() « 'Xn1; FooT* pf = foo; // Неявное преобразование (сведение); pf(); // косвенный вызов с помощью указателя; (*pf)(); // эквивалент вызова pf() // Вывод типа указателя pf std::cout « "Types of pf: " « typeid(pf).name() « ■\n'; F00T& rf = foo; // Без неявного преобразования;
22.2. Указатели и ссылки на функции 443 rf(); // косвенный вызов по ссылке // Вывод типа rf std::cout << "Types of rf: " « typeid(rf).name() « *\n' ; } Эта функция демонстрирует разнообразные варианты использования типов функций, включая некоторые не совсем необычные. В рассмотренном примере используется оператор type id, возвращающий статический тип std: :type_info, функция name () которого выводит типы некоторых выражений (см. раздел 5.6, стр. 79). Когда оператор type id применяется к типу функции, сведения типа не происходит. Ниже приведен вывод рассматриваемой демонстрационной программы, скомпилированной с помощью одного из компиляторов C++. foo() called Types of foo: void() Types of fooT: void() foo() called foo() called Types of pf: FooT * foo() called Types of rf: void() Как видите, в этом компиляторе определения типов имен сохраняются в том виде, в котором они возвращаются функцией name () (например, вместо расширенной формы типа void (*) О выводится тип FooT *), однако это не является требованием языка и в других компиляторах может не выполняться. В этом примере также показано, что ссылки на функции существуют как концепция языка, но вместо них почти всегда используются указатели на функции (чтобы избежать неоднозначности, по-видимому, лучше всего продолжать использовать именно их). Обратите внимание, что на самом деле выражение foo — это так называемое 1-значение (lvalue), поскольку его можно связать со ссылкой на неконстантный тип. Однако модифицировать данное lvalue невозможно. Заметим, что имя указателя на функцию (например, pf) или имя ссылки на функцию (например, rf) можно использовать точно так же, как и имя самой функции. Таким образом, указатель на функцию — это функтор, т.е. такой объект, который при вызове функции может быть использован вместо ее имени. С другой стороны, поскольку ссылка — это не объект, ссылка на функцию функтором не является. Если вспомнить описанные ранее особенности прямых и косвенных вызовов, то станет понятно, что использование этих идентичных обозначений может привести к значительным различиям в производительности программы.
444 Глава 22. Объекты-функции и обратные вызовы 22.3. Указатели на функции-члены Чтобы понять, чем различаются указатели на обычные функции и указатели на функции-члены, полезно разобраться, как происходит вызов функции-члена в типичной реализации C++. Такой вызов может иметь синтаксис p->mf () или подобный, где р — указатель на объект класса, членом которого является функция mf (), или на объект производного класса. Он передается в функцию mf () как скрытый параметр, который в теле функции известен как this. Функция-член mf () может быть определена в подобъекте, на который указывает р, или может наследоваться этим подобъектом. class B1 { private: int bl; public: void mf 1(); } void Bl::mfl() { std::cout « "bl = " « bl « std::endl; } Поскольку mf 1 () — это функция-член класса Bl, то ожидается, что она будет вызываться объектом этого класса. Таким образом, this — это указатель на объект класса В1. Добавим к коду следующий фрагмент: class B2 { private: int Ь2; public: ч void mf 2(); } void B2::mf2() { std::cout « Mb2 = " « b'2 « std: :endl; }; Аналогично предыдущему случаю, функция-член f 2 () нуждается в скрытом параметре this, указывающем на объект класса В2. А теперь создадим класс, производный от классов В1 и В2. class D: public Bl, public B2 { private: ^ int d; };
22.3. Указатели на функции-члены 445 В силу приведенного выше объявления объект типа D может вести себя как объект типа В1 или объект типа В2. Для этого объект класса D содержит в себе как подобъект класса В1, так и подобъект класса В2. Почти во всех известных сегодня 32-битовых реализациях организация объекта класса D будет иметь вид, проиллюстрированный на рис. 22.1. Таким образом, если размер члена int составляет 4 байта, то адрес переменной-члена Ы совпадает со значением указателя this, адрес переменной-члена Ь2 равен this, увеличенному на 4 байта, а адрес переменной-члена d равен значению this, увеличенному на 8 байтов. Заметим, что подобъект В1 расположен по тому же адресу, что и объект D в целом, в то время как адрес подобъекта В2 отличается от адреса объекта D. Подобъект типа int Ы: В1: Подобъект типа int Ь2: В2: Подобъект типа int d: D: this Рис. 22.1. Типичная организация объекта типа D Теперь рассмотрим следующие элементарные вызовы функций-членов: int main () { D ob j ; obj .mf 1 () ; obj .mf2 () ; } Для вызова obj . mf 2 () в функцию mf 2 () нужно передать адрес подобъекта типа В2 в объекте obj. Если предположить, что объект obj реализован так, как описано выше, то нужный адрес равен адресу объекта obj плюс 4 байта. Компилятору C++ несложно сгенерировать код для такой корректировки. Заметим, что для вызова функции mf 1 () такая коррекция не нужна, поскольку адрес объекта obj совпадает с адресом его подобъекта типа В1. Однако, когда дело касается указателя на функцию-член, компилятор не знает, какую корректировку нужно выполнить. Чтобы понять, что это действительно так, заменим основную функцию main () приведенной ниже. void call_memfun(D obj, void D::*pmf()) { obj .*pmf () ; } int mainO { D obj ; call_memfun(obj, &D::mf1);
446 Глава 22. Объекты-функции и обратные вызовы call_memfun(obj, &D::mf2); } Эта ситуация станет для компилятора еще запутаннее, если поместить функции call_memfun() nmain() в разные единицы трансляции. Вывод такой: указатель на функцию-член должен передавать не только информацию об адресе этой функции, но и сведения о корректировке значения указателя this для данной функции-члена. Если функция-член приводится к другому типу, эта коррекция может измениться. В нашем примере это выглядит, как показано ниже. void D::*pmf_a() = &D::mf2; // Коррекция: увеличение // на 4 байта void B2::*pmf_b() = // Коррекция стала равна (void (B2::*)())pmf_a; // нулю Этот пример приведен, чтобы продемонстрировать различия между указателем на функцию-член и указателем на обычную функцию. Однако данная схема является неполной, если дело касается виртуальных функций. На практике во многих реализациях для хранения указателей на функции-члены используется структура, состоящая из трех величин. 1. Адрес функции-члена или значение NULL, если функция виртуальная. 2. Необходимая коррекция значения указателя this. 3. Индекс виртуальной функции. Подробности этого вопроса выходят за рамки настоящей книги. Читателям, которые интересуются данной темой, рекомендуем обратиться к книге Стэна Липпмана (Stan Lippman) Inside the C++ Object Model [21]. Там же можно прочитать, что указатели на данные-члены — это, как правило, совсем не указатели, а величины смещений относительно указателя this, которые нужны, чтобы получить доступ к данному полю (для их представления обычно достаточно одного машинного слова). Наконец, заметим, что доступ к функции-члену с помощью указателя на нее — это на самом деле бинарная операция. Для ее выполнения нужна информация не только об указателе, но и об объекте, к которому относится указатель. Именно поэтому в язык программирования пришлось ввести особые операторы разыменования указателей на данные-члены . * и ->*. obj.*pmf(...) // Вызов функции-члена, на которую // указывает pmf, для объекта obj ptr->*pmf(...) // Вызов функции-члена, на которую // указывает pmf, для объекта, на // который указывает ptr ' Доступ же к обычной функции с помощью указателя, напротив, операция унарная: (*ptr)О
22.4. Функторы-классы 447 Оператор разыменования можно опустить, поскольку он неявно подразумевается в операторе вызова функции. Поэтому приведенное выше выражение обычно записывают просто как ptr() Для указателей на функции-члены такая неявная форма отсутствует4. 22.4. Функторы-классы Несмотря на наличие в C++ указателей на функции, которые сами по себе являются функторами, все же часто встречаются ситуации, когда предпочтительнее использовать объект класса, в котором перегружен оператор вызова функции. Это позволяет повысить гибкость работы программы, ее производительность или обе эти характеристики. 22.4.1. Первый пример функторов-классов Приведем простой пример функтора-класса. // functors/functorl.cpp #include <iostream> // Класс объектов-функций, возвращающих константы class ConstantlntFunctor { private: int value; // Значение, возвращаемое // при "вызове функции" public: // Конструктор: инициализация возвращаемым значением ConstantlntFunctor(int с) : value(с) { } // "Вызов функции" int operator () () const { return value; } }; // Пользовательская функция, использующая объект-функцию void client (ConstantlntFunctor constfc cif) { Также не выполняется неявное сведение имени функции-члена, например МуТуре: : print, к указателю на эту функцию-член. Амперсанд в выражениях, подобных &МуТуре:: print, опускать нельзя, хотя хорошо известно, что для обычных функций происходит неявное сведение f к &f.
448 Глава 22. Объекты-функции и обратные вызовы std::cout << "calling back functor yields " << cif () << ' \n' ; } int main() { ConstantlntFunctor seven(7); ConstantlntFunctor fortytwo(42); client(seven); client(fortytwo); } Здесь ConstantlntFunctor— это класс, с помощью которого можно генерировать функторы. Таким образом, при создании объекта ConstantlntFunctor seven(7); // Создание объекта-функции выражение seven(); // Вызов operator() для объекта-функции представляет собой вызов operator () для объекта seven, а не вызов функции seven (). Того же эффекта можно достичь (косвенно), передавая в функцию client () объекты-функции seven и fortytwo с помощью параметра cif. В этом примере иллюстрируется чуть ли не самое важное преимущество класса функторов над указателями на функции: возможность ассоциировать с функцией некоторые состояния (данные). Это фундаментальное улучшение возможностей механизмов обратного вызова. У нас есть возможность получить в распоряжение несколько "экземпляров" функции, поведение которых (в определенном смысле) параметризовано. 22.4.2. Типы функторов-классов Следует отметить, что возможности функторов-классов не ограничиваются тем, что с их помощью передается информация о состоянии. По сути, если в функторе никакое состояние не инкапсулировано, его поведение полностью определяется его типом, а чтобы нужным образом изменить поведение библиотечного компонента, достаточно передать в него параметр типа в виде параметра шаблона. Классическая иллюстрация этого особого случая — классы контейнеров, в которых объекты остаются, отсортированными в определенном порядке. Критерий сортировки задается с помощью аргумента шаблона, и, поскольку он является частью типа контейнера, непреднамеренное смешивание контейнеров с разными критериями сортировки (например, при присвоении) выявляется системой контроля типов. Контейнеры set и тар, входящие в стандартную библиотеку C++, параметризованы именно таким образом. Например, если два разных объекта класса set определены с помощью одного и того же типа элементов Person, но с разными критериями сортировки, то сравнение этих объектов приведет к возникновению ошибки на этапе компиляции.
22.4. Функторы-классы 449 #include <set> class Person { }; class PersonSortCriterion { public: bool operator() (Person const& pi, Person const& p2) const { // Возвращает информацию, меньше ли pi, чем р2 } }; void foo() { std::set<Person, std::less<Person> > cO, cl; // Сортировка с помощью оператора < std::set<Person, std::greater<Person> > c2; // Сортировка с помощью оператора > std::set<Person, PersonSortCriterion> c3; // Сортировка по критерию пользователя cO = cl; // Корректная операция: типы идентичны cl = с2; // ОШИБКА: типы различны if (cl == сЗ) { // ОШИБКА: типы различны } } Во всех трех объявлениях объектов класса set тип элементов множества и критерий сортировки передаются в виде аргументов шаблонов. Стандартный шаблон std: : less определен таким образом, чтобы возвращать посредством "вызова функции" результат применения оператора <. Приведенная ниже упрощенная реализация этого шаблона поясняет суть дела5. namespace std { template <typename T> class less { public: bool operator()(T const& x, T const& y) const { 5 реальная реализация этого шаблона несколько отличается от приведенной, поскольку класс less является производным от класса std: :binary__function. Более подробную информацию по этому вопросу можно найти в разделе 8.2.4 [18].
450 Глава 22. Объекты-функции и обратные вызовы return х < у; } }; } Подобный вид имеет и шаблон std: : greater. Поскольку все три критерия сортировки различны, полученные в результате объекты класса set принадлежат разным типам. Поэтому любая попытка присвоения или сравнения двух таких объектов приводит к ошибке времени компиляции (операнды оператора сравнения должны принадлежать одному и тому же типу). Приведенный пример может показаться довольно простым, однако до появления шаблонов критерий сортировки можно было задавать только с помощью содержащегося в контейнере поля указателя на функцию. При этом любое несовпадение типов могло остаться незамеченным до тех пор, пока программа не запускалась на выполнение (а для его выявления в некоторых случаях требовались немалые усилия и ловкость детектива). 22.5. Определение функторов В предыдущем примере, демошИрирующем применение стандартного класса set, проиллюстрирован только один способ выбора вида функторов. В данном разделе рассматривается несколько других подходов. 22.5.1. Функторы в роли аргументов типа шаблонов Один из способов передачи функтора— сделать его аргументом типа в шаблоне. Однако тип сам по себе не является функтором, поэтому пользовательская функция или класс должны создавать объект-функтор данного типа. Конечно же, это возможно только для функторов- классов, но не для указателей на функции. Последние сами по себе не определяют поведение объекта. Аналогичным образом приходим к заключению, что такой механизм не подходит для передачи функтора-класса, в котором инкапсулирована некоторая информация о состоянии (поскольку в самих этих типах никакое состояние не инкапсулировано; для передачи информации о состоянии понадобился бы отдельный объект такого типа). Ниже приведен общий вид шаблона функции, для которой критерий сортировки задается в виде функтора-класса. template <typename F0> void my_sort (...) { F0 cmp; // Создание объекта-функции if (cmp(x,y)) { // Сравнение двух величин с // помощью объекта-функции" }
22.5. Определение функторов 451 } // Вызов функции с функтором my__sort<std: : less< . . . > > (...); В продемонстрированном подходе выбор кода, с помощью которого проводится сравнение, происходит на этапе компиляции. А поскольку операцию сравнения можно сделать встраиваемой, компиляторы с хорошей оптимизацией способны сгенерировать код, в котором вызовы функтора заменены необходимыми операциями. В идеале оптимизатор должен также обладать способностью избегать выделения памяти для объекта сщр, однако на практике редко встречаются компиляторы, обладающие такими возможностями. 22.5.2. Функторы в роли аргументов функций Другой способ передать функтор — Сделать это с помощью аргументов функции. Это позволяет создавать необходимый объект-функцию во время работы программы (возможно, для этого придется применить нетривиальный конструктор). По своей эффективности способы передачи функтора в виде аргумента и в виде параметра шаблона почти одинаковы. Различие состоит в том, что в первом случае объект-функтор необходимо копировать. Обычно количество затрачиваемых при этом дополнительных ресурсов незначительно, а если объект-функтор не содержит переменных-членов (часто именно так и бывает), это количество можно свести к нулю. Чтобы лучше понять сформулированное выше утверждение, рассмотрим модифицированную функцию my_sort. template <typename F> void my__sort (. . ., F cmp) { if (cmp(x,y)) { // Сравнение двух величин с // помощью объекта-функции } } // Вызов функции с функтором my_sort (..., std: :less<. ..>()) ; В теле функции my_sort () мы имеем дело с копией передаваемого в нее объекта стр. Если это значение представляет собой пустой объект класса, нет возможности отличить по состоянию локальный объект-функтор, сконструированный в самой функции, от передаваемой в эту функцию копии. Таким образом, вместо того чтобы передавать "пустой функтор" с помощью аргумента функции, компилятор может просто использовать его для разрешения перегрузки, а затем избежать выделения памяти, необходимой
452 Глава 22. Объектытфункции и обратные вызовы для размещения параметра и аргумента. При этом внутри инстанцированной функции в роли функтора может выступать фиктивный локальный объект. Этот метод работает почти всегда, но при условии, что конструктор копирования "пустого функтора" лишен побочных эффектов. На практике это означает, что любой функтор, в котором определен пользовательский конструктор копирования, не должен оптимизироваться таким образом. Как уже отмечалось, преимущество этого метода спецификации функтора состоит в том, что в аргументе функции можно передавать и указатель на обычную функцию. bool my__criterion() (T constfc x, T constfc у) ; // Вызов функции с объектом-функцией my__sort (. . . , my__criterion) ; Кроме того, многие программисты просто предпочитают синтаксис вызова функции синтаксису, включающему аргументы шаблонов. 22.5.3. Сочетание параметров функции и параметров типа шаблона Путем комбинирования двух описанных выше методов передачи функторов в функции и классы можно задавать аргументы функции, применяющиеся по умолчанию. template <typename F> void my_sort (..., F cmp = F()) { if (cmp(x,y)) { // Сравнение двух величин с // помощью объекта-функции } bool my_criterion ()(Т constfc х, T const& у); // Вызов функции с передачей функтора в аргументе шаблона my__sort<std: : less< . . . > > (...); // Вызов функции с передачей функтора в ее аргументе my_sort(. . ., std::less<...>()); // Вызов функции с указателем на функцию, // передаваемом в ее аргументе my_sort(. . . , my_criterion);
22.5. Определение функторов 453 Таким образом созданы, например, классы упорядоченных множеств элементов, входящие в состав стандартной библиотеки C++. При этом критерий сортировки можно передавать во время работы программы в виде аргумента конструктора. class RuntimeCmp { }; // Передача критерия сортировки на этапе компиляции в виде // аргумента шаблона (с использованием критерия сортировки, // который задается в конструкторе по умолчанию) set<int/RuntimeCmp> cl; // Передача критерия сортировки на этапе выполнения в виде // аргумента конструктора set<int,RuntimeCmp> c2(RuntimeCmp(...)); Более подробную информацию по этому вопросу можно найти в [18]. 22.5.4. Функторы в роли не являющихся типами аргументов шаблонов Еще один из возможных способов передачи функторов — через не являющиеся типами аргументы шаблонов. Как уже упоминалось в разделах 4.3, стр. 62, и 8.3.3, стр. 133, объект функтора-класса (и вообще объект класса) не может выступать в роли не являющегося типом аргумента шаблона. Например, приведенный ниже код неверен. class MyCriterion { public: bool operator() (SomeType const&, SomeType constfc) const; }; template <MyCriterion F> // ОШИБКА: MyCriterion - // это класс void my__sort (...); Однако в роли не являющегося типом аргумента можно использовать указатель или ссылку на класс. Это может привести к попытке создания кода, пример которого приведен ниже. class MyCriterion { public: virtual bool operator() {SomeType const&, SomeType const&) const = 0; }; class LessThan : public MyCriterion {
454 Глава 22. Объекты-функции и обратные вызовы public: virtual bool operator() (SomeType constfc, SomeType const&) const; . }; template<MyCriterion& F> void sort (...); LessThan order; sort<order> (...); // ОШИБКА: требуется привести // производный тип к базовому sort<(MyCriterion&)order> (...); // ОШИБКА: аргумент, не являющийся // параметром типа, не должен быть // ссылкой с использованием ^// приведения типа. Идея рассмотренного примера заключается в том, чтобы указать интерфейс критерия сортировки в абстрактном базовом классе, а затем использовать этот класс в качестве параметра шаблона. В идеальном мире затем можно было бы просто создать производные классы (например, класс LessThen), с помощью которых был бы доступен интерфейс базового класса (MyCriterion). К сожалению, в C++ такой подход недопустим, поскольку тип не являющихся типом аргументов шаблонов, содержащих ссылки или указатели, должен точно соответствовать типу параметра. Неявное преобразование производного типа к базовому не выполняется, а явное преобразование делает аргумент неприемлемым. Анализ предыдущего примера показывает, что объекты классов-функторов неудобно передавать с помощью не являющихся типами аргументов шаблонов. С другой стороны, в качестве таких аргументов вполне допустимо использовать указатели (и ссылки) на функции. В следующем разделе рассматриваются некоторые возможности, которые появляются при использовании этой концепции. 22.5.5. Инкапсуляция указателей на функции Предположим, есть схема, в которой используются функторы наподобие критериев сортировки из предыдущих разделов. Допустим также, что в нашем распоряжении есть несколько функций из старых (не шаблонных) библиотек и мы хотели бы использовать их в роли таких функторов. Чтобы решить данную проблему, можно просто обернуть вызов функции в класс. class CriterionWrapper { public: bool operator() (...) { return wrapped_function(...); }
22.5. Определение функторов 455 В приведенном фрагменте кода wrapped_function () — это имеющаяся в нашем распоряжении функция, которая будет использована в роли функтора. Часто такую интеграцию уже существующих функций в классы-функторы приходится выполнять неоднократно, поэтому удобно определить шаблон, упрощающий эту задачу. template<int (*FP)()> class FunctionReturninglntWrapper { public: int operator(). () { return FP () ; } }; Ниже приведен завершенный пример. // functors/funcwrap.cpp #include <vector> #include <iostream> #include <cstdlib> // Построение оболочки для указателей // на функции в виде объектов-функций template<int (*FP)()> class FunctionReturninglntWrapper { public: int operator() () { return FP () ; } 1; II Пример функции, которая берется в оболочку int random__int () { return std::rand(); // Вызов стандартной функции С } // Клиент, в котором тип функций-объектов используется //в качестве параметра шаблона template <typename F0> void initialize (std::vector<int>& coll) { FO fo; // Создание объекта-функции for (std: :vector<int>: :size__type i=0; i < coll.sizeO; ++i) { coll[i] = fo(); // Вызов функции для // объекта-функции }
456 Глава 22. Объекты-функции и обратные вызовы int main() { // Создание вектора с 10 элементами std::vector<int> v(10); // Инициализация значений с помощью функции в оболочке initialize<FunctionReturningIntWrapper<random_int> >(v); // Вывод элементов for (std::vector<int>::size_type i=0; i<v.size(); ++i) { std::cout « "coll[" « i « "]: " « v[i] « std::endl; } } Выражение FunctionReturningIn^Wrapper<random_int> используется при вызове функции initializeO и служит оболочкой для указателя на функцию random_int, позволяющей передавать его как параметр типа шаблона. Обратите внимание, что в этот шаблон невозможно передать указатель на функцию со связыванием С, например выражение initialize<FunctionReturningIntWrapper<std::rand> >(v); может не сработать, поскольку функция std: : rand () входит в состав стандартной библиотеки С (и поэтому может иметь связывание С6). Чтобы избежать этой проблемы, можно переименовать тип указателей на функции, чтобы у него был нужный тип связывания. // Тип указателей на функции со связыванием С extern "С" typedef int (*C__infe*i_FP) () ; // Построение оболочки для указателей //на функции в виде объектов-функций template<C_JLnt_FP FP> class FunctionReturninglntWrapper { public: int operator() () { return FP() ; } }; Возможно, здесь еще раз следует подчеркнуть, что шаблоны соответствуют механизму, работающему на этапе компиляции. Это означает, что компилятору известно, какое Во многих реализациях функции, входящие в стандартную библиотеку С, обладают связыванием С, однако возможна такая реализация C++, которая предоставит этим функциям связывание C++. Поэтому корректность этого примера зависит от используемой реализации языка.
22.6. Самотестирование 457 значение следует подставить вместо обычного параметра FP шаблона FunctionRe- tuminglntWrapper. Благодаря этому в большинстве реализаций C++ вызовы, которые на первый взгляд выглядят как косвенные, преобразуются в прямые вызовы. Если же функция встраиваемая и ее определение видимо в том месте, где вызывается функтор, логично ожидать, что компилятор сгенерирует встраиваемый код. 22.6. Самотестирование В контексте программирования термин самотестирование (introspection) означает способность программы проверять свою работу. Например, в главе 15, "Классы свойств и стратегий", были разработаны шаблоны, которые могут проводить проверку типа. Что касается функторов, то для них часто полезно иметь возможность определять количество параметров и возвращаемый ими тип (или, скажем, тип л-го параметра функтора). Встречаются функции, для которых самотестирование организовать не так просто. Например, как создать функцию, определяющую тип второго параметра приведенного ниже функтора? class SuperFunc { public: void operator() (int, char**); }; В некоторых компиляторах C++ есть специальные функции с именем typeof (), которые определяют тип передаваемых в них выражений, не вычисляя при этом самих выражений (они очень похожи на оператор sizeof). Сформулированная проблема во многих случаях решается с помощью подобного оператора, хотя и не всегда тривиальным образом. Концепция функций, подобных typeof (), обсуждается в разделе 13.8, стр. 241. Альтернативой использованию таких функций является разработка функторной конструкции, в которой с помощью функторов создается некоторая дополнительная информация, позволяющая осуществлять определенное самотестирование. 22.6.1. Анализ типа функтора В состав конструкции, которую мы собираемся создать, будут входить только функторы-классы7, способные обеспечить получение следующей информации: • количество параметров функтора (в виде константы NumParams, являющейся перечислением-членом) ; • тип каждого параметра (с помощью определений typedef, являющихся членом класса, — ParamlT, Param2T, РагатЗТ, ...); 7 Чтобы ослабить это ограничение, мы также разработаем инструмент для инкапсуляции в разрабатываемую конструкцию указателей на функции.
458 Глава 22. Объекты-функции и обратные вызовы • тип, возвращаемый функтором (с помйщью определения typedef, являющегося членом класса, — ReturnT). Например, таким образом можно переделать приведенный выше класс Person- SortCriterion. class PersonSortCriterion { public: enum { NumParams = 2 }; typedef bool ReturnT; typedef Person const& ParamlT; typedef Person const& Param2T; bool operator() (Person const& pi, Person const& p2) const { // Возвращает true, если pi меньше р2 } " ,; \ Упомянутых выше соглашений для нас достаточно. Они позволят разрабатывать шаблоны для создания новых функторов на основе существующих. Есть и другие свойства функторов, которые можно представить подобным образом. Например, можно было бы закодировать информацию о том, что функтор не обладает никакими побочными действиями; ее можно использовать для оптимизации определенных шаблонов. Подобные функторы иногда называют чистыми функторами (pure functor). Порой бывает полезно организовать проверку этого свойства на этапе компиляции: например, критерий сортировки, как правило, должен быть именно таким ; в противном случае результат операции сортировки может оказаться не имеющим смысла. 22.6.2. Доступ к типам параметров Функтор может иметь произвольное количество параметров. Если придерживаться соглашений, сформулированных в предыдущем разделе, то несложно получить доступ, скажем, к типу восьмого параметра Param8T. Однако, работая с шаблонами, полезно позаботиться об обеспечении максимальной гибкости. Как же в данном случае написать функцию, возвращающую тип N-ro параметра для функтора данного типа? Одна из возможностей состоит в использовании частичных специализаций такого шаблона. template<typename FunctorType, int N> class FunctorParam; По крайней мере в значительной степени. Допустимы некоторые побочные эффекты, связанные с кэшированием и журнальными записями, однако лишь в той степени, в которой они не влияют на возвращаемое функтором значение.
22.6. Самотестирование 459 При этом можно создать частичные специализации для каждого значения N, изменяющегося от единицы до некоторого довольно большого значения (скажем, до 20 — ведь у функторов редко бывает более 20 параметров). В каждой частичной специализации может быть определен член Туре, в котором содержится информация о типе соответствующего параметра. При таком подходе возникает одна трудность: непонятно, какой тип Должен быть результатом выражения FunctorParam<F,N>: :Type, если значение N превышает количество параметров функтора F. Одна из возможностей — позволить в этой ситуации генерироваться ошибке компиляции. Несмотря на то что это легко организовать, пользы от такой функции FunctionParam будет меньше, чем могло бы быть. Еще одна возможность — возвращать в подобных случаях тип void. Недостаток такого подхода в том, что тип void обладает некоторыми досадными ограничениями. Например, он не может выступать в роли типа параметра функции, и на этот тип невозможно создать ссылку. Поэтому отдадим предпочтение третьей возможности: воспользуемся типом, являющимся закрытым членом^класса. Объекты такого типа создать нелегко, а их применение связано с некоторыми синтаксическими ограничениями. Ниже приведен код, в котором реализована сформулированная идея. // functors/functorparaml.hpp #include "ifthenelse.hpp" template <typename F, int N> class UsedFunctorParam; template <typename F, int N> class FunctorParam { private: class Unused { private: class Private {}; public: typedef Private Type; }; public: typedef typename IfThenElse<F::NumParams>=N, UsedFunctorParam<F,N>, Unused>::ResultT::Type Type; }; template <typename F> class UsedFunctorParam<F, 1> { public: typedef typename F::ParamlT Type; };
460 Глава 22. Объекты-функции и обратные вызовы С шаблоном If ThenElse можно ознакомиться в разделе 15.2.4, стр. 298. Обратите внимание, что в коде введен вспомогательный шаблон UsedFunctorParam— именно тот, который должен быть специализирован с конкретными значениями N. Это удобно сделать с помощью макроса. // functors/functorparam2.hpp #define FunctorParamSpec(N) \ template<typename F> \ class UsedFunctorParam<F, N> { \ public: \ typedef typename F::Param##N##T Type; \ } FunctorParamSpec(2); FunctorParamSpec(3); FunctorParamSpec(20) ; \ #undef FunctorParamSpec 22.6.3. Инкапсуляция указателей на функции Требование поддержки функторным типом определенного самотестирования в форме членов-typedef исключает использование указателей на функции в этой схеме. Как уже отмечалось, это ограничение можно обойти путем инкапсуляции указателей на функции. Разработаем небольшой программный инструмент, позволяющий инкапсулировать функции, число параметров в которых не больше двух (большее количество параметров обрабатывается аналогично, однако для ясности изложения рассмотрим пример с двумя параметрами). Здесь описывается случай функций со связыванием C++; связывание С обрабатывается аналогично. Представленное решение состоит из двух компонентов: шаблона класса Func- tionPtr, экземпляры которого являются типами-функторами, инкапсулирующими указатель на функцию, и перегруженного шаблона функции func_ptr(), получающей указатель на функцию и возвращающей соответствующий нашим требованиям функтор. Шаблон класса параметризуется типом возвращаемого значения и типами параметров. template<typename RT, typename Pl=void, typename P2=void> class FunctionPtr; Подстановка параметра типа void равнозначна тому, что параметр на самом деле недоступен. Таким образом, существует возможность обрабатывать с помощью рассмотренного шаблона функторы с различным числом аргументов. Чтобы инкапсулировать указатель на функцию, понадобится программный инструмент для создания из типов параметров типа указателя на функцию. Это можно сделать с помощью частичной специализации.
22.6. Самотестирование 461 // functors/functionptrt.hpp // Первичный шаблон обрабатывает максимальное // количество параметров: template<typename RT, typename PI = void, typename P2 = void, typename P3 = void> class FunctionPtrT { public: enum { NumParams = 3 }; typedef RT (*Type)(PI,P2,P3); }; // Частичная специализация для двух параметров: template<typename RT, typename PI, typename P2> class FunctionPtrT<RT, PI, P2, void> { public: enum { NumParams = 2 }; typedef RT (*Type)(PI,P2); }; // Частичная специализация для одного параметра: template<typename RT, typename Pl> class FunctionPtrT<RT, PI, void, void> { public: enum { NumParams = 1 }; typedef RT (*Type)(PI); }; // Частичная специализация для отсутствия параметров: template<typename RT> class FunctionPtrT<RT, void, void, void> { public: enum { NumParams = 0 }; typedef RT (*Type)(); }; Обратите внимание, как один и тот же шаблон используется для "подсчета" количества параметров. В разрабатываемом функторном типе значения его параметров передаются инкапсулированному указателю на функцию. Передача аргумента вызова функции может иметь побочные эффекты: если соответствующий параметр является объектом класса (а не ссылкой на него), вызовется его конструктор копирования. Чтобы избежать этих накладных расходов, полезно создать функцию, оставляющую аргумент неизменным, за исключением того случая, когда он является классом. В этом случае создается ссылка на соответствующий тип с модификатором const. С помощью шаблона ТуреТ, разработ-
462 Глава 22. Объекты-функции и обратные вызовы ка которого описана в главе 15, "Классы свойств и стратегий", этого довольно просто достичь. // functors/forwardparam.hpp #ifndef FORWARD_HPP #define FORWARD_HPP #include "ifthenelse.hpp" #include "typet.hpp" #include "typeop.hpp" // ForwardParamT<T>::Type // — для классов — константная ссылка; // — почти для всех других типов — обычный тип; // — для типа void — фиктивный тип (Unused) template<typename T> class ForwardParamT k public: ' typedef typename IfThenElse<TypeT<T>::IsClassT, typename TypeOp<T>::RefConstT, typename TypeOp<T>::ArgT >::ResultT Type; }; templateo class ForwardParamT<void> { private: <** class Unused {}; public: typedef Unused Type; }; #endif // FORWAREL.HPP Обратите внимание на сходство приведенного выше шаблона с шаблоном RParam, описанным в разделе 15.3.1, стр. 302. Различие в том, что в данном случае нужно отобразить тип void (который, как упоминалось ранее, используется для обозначения типа неиспользуемого параметра) на тип, который может фигурировать в роли параметра типа. Теперь все готово для того, чтобы определить шаблон FunctionPtr. Так как заранее не известно, сколько у него параметров, перегрузим оператор вызова функции для каждого возможного количества параметров (в нашем случае их может быть не более трех). // functors/functionptr.hpp #include "forwardparam.hpp"
22.6^ Самотестирование 463 # inc IjUde " func t i onptrt. hpp" templ&te<typename RT, typename Pi = void, typename P2 = void, typename P3 = void> class punctionPtr { private: typedef typename FunctionPtrT<RT,Pl,P2,P3>::Type FuncPtr; // Инкапсулированный указатель: FuncPtr fptr; public: // Приведение в соответствие с имеющимися требованиями: enum {NumParams=FunctionPtrT<RT,Pl,P2,P3>: .-NumParams}; typedef RT ReturnT; typedef PI ParamlT; typedef P2 Param2T; typedef РЗ РагатЗТ; // Конструктор: FunctionPtr(FuncPtr ptr) : fptr(ptr) { } // "Вызовы функций": RT operator()() { return fptr(); } RT operator()(typename ForwardParamT<Pl>::Type al) { return fptr(al); } RT operator()(typename ForwardParamT<Pl>::Type al, typename ForwardParamT<P2>: :Type a2) { return fptr(al, a2); } RT operator()(typename ForwardParamT<Pl>::Type al, typename ForwardParamT<P2>::Type a2, typename ForwardParamT<P2>::Type a3) { return fptr(al, a2, a3); } }; Этот шаблон класса работает довольно хорошо, однако его непосредственное применение может оказаться громоздким. Несколько (встраиваемых) шаблонов функций позволят использовать механизм вывода аргумента шаблона для облегчения этой задачи. // functors/funcptr.hpp #include "functionptr.hpp"
464 Глава 22. Объекты-функции и обратные вызовы template<typename RT> inline FunctionPtr<RT> func_ptr (RT (*fp)0) { return FunctionPtr<RT>(fp); } template<typename RT, typename Pl> inline FunctionPtr<RT,Pl> func_ptr (RT (*fp)(Pl)) return FunctionPtr<RT,Pl>(fp); } template<typename RT, typename PI, typename P2> inline FunctionPtr<RT,Pl,P2> func_ptr (RT (*fp)(Pi,P2)) { return FunctionPtr<RT,Pl,P2>(fp); } template<typename RT, typename PI, typename P2, typename P3> inline FunctionPtr<RT,Pl,P2,P3> func_ptr (RT (*fp)(PI,P2,P3)) { return FunctionPtr<RT,Pl,P2,P3>(fp); } Теперь все, что осталось, — это испытать только что разработанный программный инструмент в небольшой демонстрационной программе. // functors/functordemo.cpp #include <iostream> #include <string> #include <typeinfo> #include "funcptr.hpp" double seven() { return 7.0; } std::string more() { return std: : string ("more"), ; template <typename FunctorT> void demo (FunctorT func)
22.7. композиции объектов-функций 465 st:d::cout « "Functor returns type " \ « typeid(typename FunctorT::ReturnT).name() \ « ' \n' ', « "Functor returns value " \ « f unc () « ' \n' ; int main() { demo(func_ptr(seven)); demo(func_ptr(more)); } 22.7. Композиции объектов-функций Предположим, у нас есть два простых математических функтора. // functors/mathl.hpp #include <cmath> #include <cstdlib> class Abs { public: // "Вызов функции": double operator() (double v) const { return std::abs(v); } >; class Sine { public: // "Вызов функции": double operator() (double a) const { return std::sin(a); } }; На самом деле нам нужен функтор, вычисляющий абсолютную величину синуса заданного угла. Новый функтор написать несложно. class AbsSine { public: double operator() (double a) { return std::abs(std::sin(a)); } };
466 Глава 22. Объекты-функции и обратные вызовы Однако неудобно для каждой новой комбинации функторов писать отдельное объявление; гораздо предпочтительнее иметь возможность использовать композиции функторов. В данном разделе будут разработаны некоторые шаблоны для решения этой задачи. По ходу изложения будут введены различные концепции, которые окажутся полезными в последующих разделах данной главы. 22.7.1. Простая композиция Начнем с реализации простой композиции. // functors/composel.hpp template <typename FOl, typename F02> class Composer { private: FOl fol^- // Первый (внутренний) вызываемый объект F02 f02; // Второй (внешний) вызываемый объект public: // Конструктор: инициализация объектов-функций Composer (FOl fl, F02 f2) : fol(fl), fo2(f2) { } // "Вызов функции": вложенный вызов объектов-функций double operator() (double v) { return fo2(fol(v)); } }; Заметим, что первой в списке параметров идет функция, которая первой применяется. Это означает, что запись Composer<Abs, Sine> соответствует функции sin(abs(x)) (обратите внимание на обратный порядок функций). Приведенный шаблон можно протестировать с помощью программы, представленной ниже. // functors/composel.срр #include <iostream> #include "mathl.hpp" #include "composel.hpp" template<typename FO> void print values (FO fo) { for (int i=-2; i<3; ++i) { std::cout << "f(" << i*0.1 ^ << ■■) = " << fo(i*0.1) << "\n";
22.7. Композиции объектов-функций 467 } } int main() { // Вывод значения sin(abs(-0.5)) std::cout « Composer<Abs,Sine>(Abs(),Sine())(0.5) « "\n\n"; // Вывод значений abs() print__values(Abs()); std::cout « '\n'; // Вывод значений sin() print__values(Sine()); std::cout « '\n'; // Вывод значений sin(absO) print_values(Composer<Abs, Sine>(Abs(), Sine())); std::cout « '\n'; // Вывод значений abs(sin()) print__values (Composer<Sine, Abs> (Sine () , Abs () ) ) ; } В этой программе демонстрируется общий принцип; разумеется, остаются возможности для дальнейших улучшений. Одно весьма полезное улучшение достигается путем разработки встраиваемой вспомогательной функции, обеспечивающей вывод аргументов шаблона Composer (этот метод уже стал привычным). // functors/composeconv.hpp template <typename FOl, typename F02> inline Composer^FO^FC^ compose (FOl fl, F02 f2) { return Composer<F01,F02> (fl, f2); } С учетом этого приведенную ранее демонстрационную программу можно переписать, как показано ниже. // functors/compose2.cpp #include <iostream> #include "mathl.hpp" #include "composel.hpp" #,include "composeconv.hpp" template<typename FO> void print_values(FO fo)
468 Глава 22. Объекты-функции и обратные вызовы { for (int i=-2; i<3; ++i) { std::cout « "f(" « i*0.1 « •») = » « fo(i*0.1) « "\nH; } } int main() { // Вывод значения выражения sin(abs(-0.5)) std::cout « compose(Abs(),Sine())(0.5) « "\n\n"; // Вывод значений abs() print_values(Abs()); std::cout « '\n'; // Вывод значений sin() print_values(Sine()); std::cout « ■\n'; // Вывод значений sin(abs(J) < print_values(compose(Abs(), Sine())); std::cout « '\nf; // Вывод значений abs(sinO) print_values(compose(Sine(),Abs())); } Выражение Composer<Abs, Sine>(Abs(), Sine()) теперь можно записать в более удобном виде: compose(Abs(),Sine()) Еще одно улучшение продиктовано желанием оптимизировать сам шаблон класса Composer. Точнее говоря, мы попытаемся избежать выделения памяти для функторов- членов first () и second (), если эти функторы являются пустыми классами (т.е. если в них не сохраняется информация о состоянии). Может показаться, что экономия памяти при этом будет незначительной, однако напомним, что при передаче в виде параметров функции пустые классы могут быть оптимизированы с использованием стандартного метода оптимизации пустого базового класса (см. раздел 16.2, стр. 315), при котором члены преобразуются к базовым классам. // functors/compose3.hpp template <typename FOl, typename F02> class Composer : private FOl, private F02 { public:
22.7. Композиции объектов-функций 469 // Конструктор: инициализация объектов-функций Composer(F01 fl, F02 f2) : FOl(fl), F02(f2) { } // "Вызов функции": вложенный вызов объектов-функций double operator() (double v) { return F02::operator()(F01::operator()(v)); } }; Однако такой подход нельзя назвать оптимальным. Применяя его, невозможно комбинировать функцию саму с собой. Действительно, вызов функции // Вывод sin(sin()) от некоторого значения print__values(compose(Sine(),Sine())); // ОШИБКА: повторение имени базового класса приводит к такому инстанцированию шаблона Composer, при котором он дважды наследуется от класса Sine, что недопустимо. Эту проблему легко решить, добавив еще один уровень наследования. // functors/compose4.hpp template <typename С, int N> class BaseMem : public С { public: BaseMem(C& c) : C(c) { } BaseMem(С const& c) : C(c) { } }; template <typename F01, typename F02> class Composer : private BaseMem<F01/1>, private BaseMem<F02,2> { public: // Конструктор: инициализация объектов-функций Composer(F01 fl, F02 f2) : BaseMem<F01,l>(fl), BaseMem<F02,2>(f2) { } // "Вызов функции": вложенный вызов объектов-функций double operator() (double v) { return BaseMem<F02,2>::operator() (BaseMem<F01,l>::operator()(v)); } }; Безусловно, эта реализация более запутанна, чем исходная, однако такое усложнение может быть приемлемой ценой, если она позволит оптимизатору понять, что результирующий функтор является "пустым". /
470 Глава 22. Объекты-функции и обратные вызовы Интересно, что оператор вызова функции можно объявить виртуальным. Если это сделать в функторе, участвующем в комбинировании, то оператор вызова функции, который относится к результирующему объекту класса Composer, тоже будет виртуальным. Это может привести к неожиданным результатам, поэтому в оставшейся части главы предполагается, что оператор вызова функции является невиртуальным. 22.7.2. Композиция разных типов Более существенным улучшением шаблона Composer может оказаться возможность более гибко работать с типами комбинируемых функторов. В реализации, рассмотренной в предьщущем разделе, допустимы только функторы, аргументы которых принадлежат типу double и которые возвращают значения типа double. Шаблон Composer выглядел бы элегантнее, если бы с его помощью можно было комбинировать функторы любых соответствующих типов. Например, такой шаблон позволял бы комбинировать функтор, аргументом которого является значение типа int и который возвращает значение типа bool, с функтбрем, аргументом которого является значение типа bool и который возвращает значение типа double. В этом случае пригодится использование конструкций typedef внутри классов. С учетом принятых соглашений шаблон, предназначенный для комбинирования двух функторов, можно переписать, как показано ниже. // functors/compose5.hpp #include "forwardparam.hpp" template <typename C, int N> class BaseMem : public С {v, public: BaseMem(C& c) : C(c) { } BaseMem(C constfc c) : C(c) { } }; template <typename FOl, typename F02> class Composer : private BaseMem<F01, 1>, private BaseMem<F02/2> { public: // Приведение в соответствие с требованиями: enum { NumParams = FOl: : NumParams } ; typedef typename F02::ReturnT ReturnT; typedef typename F01::ParamlT ParamlT; // Конструктор: инициализация объектов-функций Composer(FOl fl, F02 f2) : BaseMem<F01/l>(fl), BaseMem<F02,2>(f2) { }
22.7. Композиции объектов-функций 471 // "Вызов функции": вложенный вызов функций-объектов ReturnT operator() (typename ForwardParamT<ParamlT>::Type v) { return BaseMem<F02,2>::operator() (BaseMem<F01,l>::operator() (v)) ; } }; Во избежание излишнего копирования аргументов функторов повторно использован шаблон ForwardParamT (см. раздел 22.6.3, стр. 460). Чтобы с помощью разрабатываемого шаблона можно было сочетать вызовы рассмотренных функторов Abs и Sine, в них нужно включить соответствующую информацию о типах. // functors/math2.hpp #include <cmath> #include <cstdlib> class Abs { public: // Приведение в соответствие с требованиями: enum { NumParams = 1 }; typedef double ReturnT; typedef double ParamlT; // "Вызов функции": double operator() (double v) const { return std::abs(v); } }; class Sine { public: « I/ Приведение в соответствие с требованиями: enum { NumParams = 1 }; typedef double ReturnT; typedef double ParamlT; // "Вызов функции": double operator() (double a) const { return std::sin(a); } }; Альтернативный подход — реализовать функторы Abs и Sine в виде шаблонов. // functors/math3.hpp
472 Глава 22. Объекты-функции и обратные вызовы #include <cmath> #include <cstdlib> template <typename T> class Abs { public: // Приведение в соответствие с требованиями: enum { NumParams = 1 } ; typedef T ReturnT; typedef T ParamlT; // "Вызов функции": T operator() (T v) const { return std::abs(v); template <typename T> class Sine { public: // Приведение в соответствие с требованиями: enum { NumParams = 1 }; typedef T ReturnT; typedef T ParamlT; // "Вызов функции": T operator() (Та) const { return std::sin(a); } }; В последнем случае для использования этих функторов нужно, чтобы параметры типов передавались непосредственно в аргументах шаблонов. В приведенном ниже примере иллюстрируется несколько более сложный синтаксис. // functors/compose5.cpp #include <iostream> #include "math3.hpp" #include "compose5.hpp" #include "composeconv.hpp" template<typename FO> void print__values (FO fo) { ' ^ for (int i=-2; i<3; ++i) { std::cout « "f(" « i*0.1 « ») = •• « fo(i*0.1)
22.7. Композиции объектов-функций 473 « "\п" ; } } int main () { // Вывод значения sin(abs(-0.5)) std::cout « compose(Abs<double>(),Sine<double>())(0.5) « "\n\n"; x // Вывод значений abs() print__values (Abs<double> () ) ; std::cout « '\nf; // Вывод значений sin() print_values(Sine<double>()); std::cout « '\n'; // Вывод значений sin(absО) print__values (compose (Abs<double> () , Sine<double> () ) ) ; std::cout « '\n'; // Вывод abs(sin()) для некоторого значения print_values(compose(Sine<double>(),Abs<double>())); std::cout « '\n'; // Вывод значений sin(sin()) print_values(compose(Sine<double>(),Sine<double>())); 22.7.3. Функторы с несколькими параметрами До сих пор рассматривалась простая композиция двух функторов, у каждого из которых по одному аргументу, причем в роли аргумента одного из функторов выступает другой функтор. Однако функторы могут иметь и несколько аргументов, поэтому необходимо научиться комбинировать функторы с несколькими параметрами. Если первый агрумент-функтор шаблона Composer принимает несколько аргументов, то и сам класс Composer должен обладать таким же свойством. Это означает, что необходимо определить несколько членов-синонимов типа ParamWT, а также перегрузить оператор вызова функции (operator ()) для соответствующего числа параметров. Последнее выполнить не так сложно, как может показаться. Оператор вызова функции может быть перегруженным, поэтому достаточно задать его определение для каждого возможного количества параметров вплоть до некоторого довольно большого числа (в библиотеке функторов, предназначенной для промышленного применения, это количество может достигать 20). Любая попытка вызвать перегруженный оператор с числом
474 Глава 22. Объекты-функции и обратные вызовы параметров, для которого он не определен, приведет к возникновению ошибки компиляции, что вполне нас устраивает. Возможный код шаблона Composer приведен ниже. template <typename F01, typename F02> class Composer : private BaseMem<F01,1>, private BaseMem<F02/2> { public: // "Вызов функции" без аргументов: ReturnT operator() () { return BaseMem<F02/2>::operator() (BaseMem<F01,1>::operator()()); } // "Вызов,функции" с одним аргументом: ReturnT operator() (typename ForwardParamT<ParamlT>::Type vl) { return BaseMem<F02, 2>: :operator() (BaseMem<F01/l>::operator()(vl)); } // "Вызов функции" с двумя аргументами: ReturnT operator() (typename ForwardParamT<ParamlT>::Type vl, typename ForwardParamT<Param2T>::Type v2) { return BaseMem<F02, 2>: :operator() (BaseMem<FQl,1>::operator()(vl, v2)); } }; Теперь осталось определить члены ParamlT, Param2T и т.д. Задача усложняется тем, что эти типы используются в объявлении различных операторов вызова функции, поэтому они должны быть корректны, даже если сочетаемые функторы не имеют соответствующих о параметров . Например, комбинируя два функтора, у каждого из которых по одному параметру, необходимо позаботиться о том, чтобы тип Param2T был корректным параметром типа. Желательно, чтобы этот тип случайно не совпал с другим типом, применяемым в пользовательской программе. К счастью, эта проблема уже решена в шаблоне FunctorParam. Итак, шаблон Compose может быть оснащен различными членами-синонимами типов. template <typename F01, typename F02> class Composer : private BaseMem<F01,1>, Заметим, что в данной ситуации принцип SFINAE (см. раздел 8.3.1, стр. 129) неприменим, поскольку это обычные функции-члены, а не шаблоны функций-членов. Применение принципа SFINAE основано на выводе параметров шаблонов, который не используется для обычных функций-членов.
22.7. Композиции объектов-функций 475 private BaseMem<F02/2> { public: // Возвращаемый тип — это просто: typedef typename F02::ReturnT ReturnT; // Определяем ParamlT, Param2T и т.д. // Облегчаем определение с помощью макроса #define ComposeParamT(N) typedef typename \ FunctorParam<F01, N>::Type Param##N##T ComposeParamT(1); ComposeParamT(2) ; ComposeParamT(20); #undef ComposeParamT }; Наконец, нужно определить конструкторы класса Composer. В них должны сочетаться два функтора, однако составим определение так, чтобы можно было составлять различные комбинации из функторов с модификатором const и без него. template <typename F01, typename F02> class Composer : private BaseMem<F01,1>, private BaseMem<F02/2> { public: // Конструкторы: Composer(F01 const& fl, F02 constfc f2) : BaseMem<F01,l>(fl), BaseMem<F02,2>(f2) { } Composer(F01 const& fl, F02& f2) : BaseMem<F01,l>(fl), BaseMem<F02,2>(f2) { } Composer(F01& fl, F02 const& f2) : BaseMem<F01,l>(fl), BaseMem<F02,2>(f2) { } Composer(F01& fl, F02& f2) : BaseMem<F01,l>(fl), BaseMem<F02,2>(f2) { } }; Располагая библиотекой, в которой содержится такой код, программист может пользоваться простыми конструкциями, проиллюстрированными в приведенном ниже примере. // functors/compose6.cpp #include <iostream> #include "funcptr.hpp"
476 Глава 22. Объекты-функции и обратные вызовы #include "compose6.hpp" #include "composeconv.hpp" double add(double a, double b) { return a+b; } double twice(double a) { return 2*a; } int main () { std::cout <^ "compute (20+7)*2: " << compose(func_ptr(add),func_ptr(twice))(20,7) « '\n'; } Описанные программные инструменты могут быть улучшены. Например, полезно было бы дополнить шаблон compose таким образом, чтобы он мог непосредственно обрабатывать указатели на функции (при этом отпадет необходимость использования func_ptr). Однако для краткости предлагаем заинтересованным читателям рассмотреть этот вопрос самостоятельно. "■/ 22.8. Связывание значений Часто бывает так, что функтор с несколькими параметрами остается полезным и после того, как один из его параметров будет связан (bound) с определенным значением. Например, рассмотрим простой шаблон функтора Min. // functors/min.hpp template <typename T> class Min { public: typedef T Return?; typedef T ParamlT; typedef T Param2T; enum { NumParams = 2 } ; ReturnT operator() (ParamlT a, Param2T b) { return a<b ? b : a; ^ } };
22.8. Связывание значений 477 На основе этого шаблона можно создать новый функтор Clamp, в котором один из параметров связан с определенной константой. Эту константу можно задать в виде аргумента шаблона или в виде аргумента времени выполнения. Ниже приведен один из возможных вариантов определения нового функтора. // functors/clamp.hpp template <typename T, T max__result> class Clamp : private Min<T> { public: typedef T ReturnT; typedef T ParamlT; enum { NumParams = 1 }; ReturnT operator() (ParamlT a) { return Min<T>::operator() (a, max_result); } }; Хотя для связывания параметра "вручную" часто достаточно написать небольшой объем кода, удобно разработать шаблон, автоматизирующий эту задачу. 22.8.1. Выбор параметров связывания Связывающий шаблон (binder) связывает определенный параметр указанного функтора с заданным значением. Каждый из них можно выбирать во время работы (с помощью аргументов функции) или во время компиляции программы (с помощью аргументов шаблона). Например, в приведенном ниже шаблоне все параметры выбираются статически (во время компиляции). template<typename F, int P, int V> class BindlntStatically; // F — тип функтора; // P — связываемый параметр; // V — связываемая величина Каждый параметр связывания (вид функтора, связываемый параметр и связываемая величина) можно также с различной степенью удобства выбирать динамически. Труднее всего выбирать параметр, который будет связан динамически. По-видимому, для этого понадобится большое количество инструкций switch, с помощью которых вызов функтора будет преобразован в вызов одного из возможных вспомогательных функторов. При этом выбор конкретного вспомогательного функтора осуществляется в зависимости от того, какое значение имеет определенная величина во время работы программы. • switch (this->param_num) { case 1: return F::operator()(v, pi, p2); case 2:
478 Глава 22. Объекты-функции и обратные вызовы return F: .-operator () (pi, v, p2) ; case 3: return F::operator()(pi, p2, v); default: return F::operator()(pi, p2); // Или это ошибка? } Пожалуй, из всех трех вариантов связывания данный в наименьшей степени нуждается в динамическом осуществлении. Поэтому в дальнейшем выбор связываемого параметра будет осуществляться статически с помощью параметра шаблона. Чтобы организовать динамический выбор функтора, достаточно добавить конструктор, с помощью которого функтор будет передаваться в связывающий шаблон. Аналогично с помощью конструктора можно осуществить и передачу связываемого значения (в этом случае в шаблоне следует выделить память для хранения этого значения). С помощью приведенных ниже вспомогательных шаблонов такое выделение памяти можно выполнить как ва время компиляции, так и во время работы программы. // functors/boundval.hpp #include "typeop.hpp" template <typename T> class BoundVal { private: T value; public: typedef T ValueT;v BoundVal(T v) : value(v) { } typename TypeOp<T>::RefT get() { return value; } }; template <typename T, T Val> class StaticBoundVal { public: typedef T ValueT; T get() { return Val; } }; Как и ранее, здесь мы полагаемся на оптимизацию пустого базового класса (см. раздел 16.2, стр. 315), которая помогает избежать ненужных накладных расходов в случае, когда представление функтора или связываемой величины не хранит состояния. Таким образом, на начальной стадии разработки шаблон Binder выглядит, как показано ниже.
22.8. Связывание значений 479 // functors/binder1.hpp template <typename FO, int P, typename V> class Binder : private FO, private V { public: // Конструкторы: Binder(F0& f): FO(f) {} Binder(FO& f, V& v): FO(f), V(v) {} Binder(FO& f, V constfc v): FO(f), V(v) {} Binder(FO const& f): FO(f) {} Binder(FO const& f, V& v): FO(f), V(v) {} Binder(FO const& f, V const& v): FO(f), V(v) {} template<class T> Binder(FO& f, T& v): FO(f), V(BoundVal<T>(v)) {} template<class T> Binder(FO& f# T const& v): FO(f), V(BoundVal<T const>(v)) {} }; Заметим, что кроме конструкторов, в которые передаются экземпляры вспомогательных шаблонов, определяются шаблоны конструкторов, которые автоматически создают оболочку вокруг связываемого значения в виде объекта BoundVal. 22.8.2. Сигнатура связывания В шаблоне Binder типы ParamWT определяются сложнее, чем в шаблоне Composer. Невозможно просто воспроизвести типы функтора, на основе которого производится связывание. Поскольку связываемый параметр больше не является параметром нового функтора, соответствующий тип ParamA/T следует опустить, а последующие типы сдвинуть на одну позицию. Придерживаясь принципа модульности, введем отдельный шаблон, в котором выполняется операция выборочного сдвига. // functors/binderparams.hpp #'include "ifthenels.e.hpp" template<typename F, int P> class BinderParams { public: //На один параметр меньше из-за связывания: enum { NumParams,= F: :NumParams-l }; #define ComposeParamT(N) typedef typename \ IfThenElse<(N<P), FunctorParam<F/ N>, \ FunctorParam<F/ N+l> >::ResultT::Type \ Param##N##T
480 Глава 22. Объекты-функции и обратные вызовы ComposeParamT(l); ComposeParamT(2) ; ComposeParamT(3); #undef ComposeParamT }; В шаблоне Binder приведенный шаблон используется, как показано ниже. // functors/binder2.hpp template <typename FO, int P, typename V> class Binder : private FO/ private V { public: //На один параметр меньше из-за связывания: enum { NumParams = FO: :NumParams-l }; // Возвращаемый тип: typedef typename FO::ReturnT ReturnT; // Типы параметров: typedef BinderParams<FO, P> Params; #define ComposeParamT(N) typedef typename \ ForwardParamT<typename Params::Param##N##T>::Type \ Param##N##T ComposeParamT(1); ComposeParamT(2) ; ComposeParamT(3); #undef ComposeParamT }; Как обычно, с помощью вспомогательного шаблона ForwardParamT удалось избежать излишнего копирования аргументов. 22.8.3. Выбор аргументов Чтобы завершить разработку шаблона Binder, осталось решить проблему реализации оператора вызова функции. Как и в шаблоне Composer, мы собираемся перегрузить этот оператор для вызова функторов с различным количеством параметррв. Однако на этот раз стоящая перед нами задача намного сложнее, чем для шаблона Composer, поскольку аргумент, который передается в связываемый функтор, может быть одного из трех видов: • соответствующий параметр связываемого функтора; • связываемое значение; ^ • параметр связываемого функтора, который находится на одну позицию левее от передаваемого аргумента.
22.8. Связывание значений 481 Выбор одного из трех перечисленных видов будет зависеть от значения Р и от позиции выбираемого аргумента. Идея, которая поможет достичь желаемого результата, состоит в том, чтобы разработать закрытую встраиваемую функцию-член, которая принимает три возможных значения, передаваемых по ссылке, и возвращает (также по ссылке) одно значение, которое соответствует позиции аргумента. Поскольку эта функция-член зависит от того, какой аргумент выбирается, она будет организована как статический член вложенного шаблона класса. Этот подход позволяет записать оператор вызова функции следующим образом (ниже приведен случай функтора с четырьмя параметрами; другие подобные операторы реализуются аналогично). // functors/binder3.hpp template <typename FO, int P, typename V> class Binder : private FO, private V { public: ReturnT operator()(ParamlT vl, Param2T v2, РагатЗТ v3) { return FO::operator()( ArgSelect<l>: :from(vl, vl,V: :get() ) , ArgSelect<2>: : from(vl, v2,V: .-get () ) , ArgSelect<3>::from(v2,v3,V::get()), ArgSelect<4>::from(v3,v3,V::get())); } }; Заметим, что в качестве первого и последнего аргумента могут выступать только два значения: сам первый или последний параметр оператора либо связываемое значение. Если А— положение аргумента при вызове соответствующего функтора (в данном примере оно может меняться от 1 до 3), то возможен один из перечисленных ниже вариантов. Если А-Р<0, то выбирается соответствующий параметр, если А-Р=0— связываемое значение, а если А-Р>0— параметр, который находится на одну позицию левее. Это наблюдение подтверждает правильность определения вспомогательного шаблона, в котором выбирается один из трех типов на основе знака не являющегося типом аргумента шаблона. // functors/signselect.hpp #include "ifthenelse.hpp" template <int S, typename NegT, typename ZeroT, typename PosT> struct SignSelectT { typedef typename IfThenElse<(S<0), NegT, typename IfThenElse<(S>0),
482 Глава 22. Объекты-функции и обратные вызовы PosT, ZeroT >::ResultT >::ResultT ResultT; }; На данном этапе все готово для определения шаблона класса ArgSelect. // functors/binder4.hpp template <typename FO, int P, typename V> class Binder : private FO, private V { private: template<int A> class ArgSelect { public: // Тип до\ связываемого аргумента typedef typename TypeOp< typename IfThenElse< (A<=Params: :NumParams) , FunctorParam<Params, A>, FunctorParam<Params, A-l> >::ResultT::Type>::RefT NoSkipT; // Тип после связываемого аргумента typedef typename TypeOp< typename^IfThenElse<(A>1), FunctorParam<Params, A-l>, FunctorParam<Params, A> >::ResultT::Type>::RefT SkipT; // Тип связываемого аргумента: typedef typename TypeOp<typename V::ValueT>::RefT BindT; // Три варианта выбора, // реализованные в разных классах: class NoSkip { public: static NoSkipT select (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return arg; } }; class Skip { public:
22.8. Связывание значений 483 static SkipT select (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return prev_arg; } }; class Bind { public: static BindT select (SkipT prev__arg, NoSkipT arg, BindT bound_val) { return bound_val; } }; // Функция выбора: typedef typename SignSelectT<A-P, NoSkipT, BindT, SkipT>::ResultT ReturnT; typedef typename SignSelectT<A-P, NoSkip, Bind, Skip>::ResultT SelectedT; static ReturnT from (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return SelectedT::select (prev_arg, arg, bound__val) ; } }; }; Приведенный код является, по-видимому, самым сложным в книге. Функция-член f rom() вызывается в операторах вызова функтора. Частично сложность возникает из-за необходимости выбора нужных типов параметров, из которых выбирается аргумент: типы SkipT и NoSkipT также удовлетворяют соглашениям, которым должен подчиняться первый и последний аргументы (т.е. повторяют аргументы vl и v3 проиллюстрированного выше оператора). Для определения этих типов можно применить конструкцию ТуреОро: : Ref Т. Можно было бы просто создать ссылку на тип с помощью символа &, однако большинство компиляторов еще не умеют обрабатывать "ссылки на ссылки". Функции, осуществляющие выбор, сами по себе довольно тривиальны, однако они инкапсулированы в типы-члены NoSkip, Skip и Bind, что облегчает статическую диспетчеризацию соответствующей функции. Поскольку эти функции сами по себе являются просто встраиваемыми передающими вызов функциями, компилятор с хорошей оптимизацией должен быть в состоянии "просмотреть" их все и сгенерировать код, близкий к оптимальному. На практике оказалось, что только самые лучшие оптимизаторы из тех, что были доступны во время написания данной книги, генерировали код с хорошей про-
484 Глава 22. Объекты-функции и обратные вызовы изводительностью. Однако большинство других компиляторов тоже выполняли неплохую оптимизацию программ, использующих класс Binder. Ниже приведена полная реализация шаблона Binder. // functors/binder5.hpp #include "ifthenelse.hpp" #include "boundval.hpp" #include "forwardparam.hpp" #include "functorparam.hpp" # include "binderparams.hpp" #include "signselect.hpp" template <typename FO, int P/ typename V> class Binder : private FO, private V { public: //На один параметр меньше из-за связывания: enum { NumParams = FO: :NumParams-l }; // Возвращаемый тип: typedef typename FO::ReturnT ReturnT; // Типы параметров: typedef BinderParams<FO/ P> Params; ttdefine ComposeParamT(N) , typedef typename \ ForwardParamT< typename Params::Param##N##T>::Type \ Param##N##T ComposeParamT(1); ComposeParamT(2); ComposeParamT(3); #undef ComposeParamT // Конструкторы: Binder(F0& f): FO(f) {} Binder(F0& f, V& v): FO(f), V(v) {} Binder(FO& f, V const* v): FO(f), V(v) {} Binder(FO constfc f): FO(f) {} Binder(FO const& f, V& v): FOff), V(v) {} Binder(FO const& £, V const& v): FOtf), V(v) {} template<class T> Binder(FO& f# T& v): FO(f), V(BoundVal<T>(v)) {} template<class T> Binder(FO& f, T const& v): FO(f), V(BoundVal<T const>(v)) {} // "Вызовы функций": ReturnT operator() () {
22.8. Связывание значений 485 return FO::operator()(V::get()); } ReturnT operator() (ParamlT vl) { return FO::operator()( ArgSelect<l>::from(vl,vl,V::get()), ArgSelect<2>::from(vl,vl,V::get())); } ReturnT operator() (ParamlT vl, Param2T v2) { return FO::operator()( ArgSelect<l>::from(vl,vl,V::get()), ArgSelect<2>::from(vl#v2,V::get()), ArgSelect<3>::from(v2/v2,V::get())); } ReturnT operator() (ParamlT vl, Param2T v2, РагатЗТ v3) { return FO::operator()( ArgSelect<l>::from(vl,vl,V::get()), ArgSelect<2>::from(vl,v2,V::get()), ArgSelect<3>::from(v2,v3,V::get()), ArgSelect<4>:: from(v3, v3,V::get())); } private: template<int A> class ArgSelect { public: // Тип до связываемого аргумента typedef typename TypeOp< typename IfThenElse<(A<=Params::NumParams), FunctorParam<Params, A>, FunctorParam<Params, A-l> >::ResultT::Type>::RefT NoSkipT; // Тип после связываемого аргумента typedef typename TypeOp< typename IfThenElse<(A>1) , FunctorParam<Params, A-l>, FunctorParam<Params, A> >::ResultT::Type>::RefT SkipT; // Тип связываемого аргумента: typedef typename TypeOp<typename V::ValueT>::RefT BindT; // Три варианта выбора, // реализованные в разных классах:
486 Глава 22. Объекты-функции и обратные вызовы }; class NoSkip { public: static NoSkipT select (SkipT prev__arg, NoSkipT arg, BindT bound_val) { return arg; } }; class Skip { public: static SkipT select (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return prev_arg; } }; class Bind { public: \ static^indT select (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return bound_val; } }; // Функция выбора: typedef typename SignSelectT<A-P, NoSkipT, BindT, SkipT>::ResultT ReturnT; typedef typename SignSelectT<A-P, NoSkip, Bind, Skip>::ResultT SelectedT; static ReturnT from (SkipT prev_arg, NoSkipT arg, BindT bound_val) { return SelectedT::select (prev_arg, arg, bound.val); } 22.8.4. Вспомогательные функции Как и в случае с шаблонами, предназначенными для комбинирования функторов, для шаблона Binder полезно написать шаблоны функций, облегчающих составление выражений, с помощью которых происходит связывание параметров функтора. Определение таких шаблонов функций несколько усложняется из-за необходимости выражать тип связываемого значения.
22.8. Связывание значений 487 // functors/bindconv.hpp #include "forwardparam.hppn #include "functorparam.hpp" template <int P, // Положение связываемого параметра typename F0> // Функтор, параметр // которого связывается inline Binder<FO,P/BoundVal<typename FunctorParam<FO,P>::Type> > bind (FO const& fo, typename ForwardParamT <typename FunctorParam<FO,P>::Type>::Type val) { return Binder<FO, P, BoundVal<typename FunctorParam<FO,P>::Type> >(fо, BoundVal<typename FunctorParam<FO,P>::Type>(val)); } Первый параметр шаблона не выводится, так что его значение должно точно задаваться при использовании шаблона bind (), что проиллюстрировано в приведенном ниже примере. // functors/bindtest. срр #include <string> iinclude <iostream> #include "funcptr.hpp" #include "binder5.hpp" # include "bindconv.hpp" bool func(std::string constfc str, double d, float f) { std::cout « str « ": " « d « (d < f ? "<": ">=") « f « '\n'; return d < f; } int main() { bool result = bind<l>(func_ptr(func), "Comparing")(1.0, 2.0); std::cout « "bound function returned " « result « '\nf; }
488 Глава 22. Объекты-функции и обратные вызовы Заманчиво было бы упростить шаблон bind, добавив в него выводимый параметр шаблона, соответствующий связываемому значению. Таким образом удалось бы избежать громоздких выражений, подобных тем, что встречаются в приведенном выше примере. Однако при этом часто возникают сложности. Например, в рассматриваемом примере литерал типа double (2 .0) передается параметру близкого, но отличающегося типа float. Кроме того, во многих ситуациях удобно было бы выполнять непосредственное связывание функции (которая передается в виде указателя на функцию). Получаемые в результате определения шаблонов bindf p () лишь немного сложнее определения шаблона bind. Ниже приведен код, соответствующий функции с двумя параметрами. // functors/bindfp2.hpp // Вспомогательная функция для связывания функции //с двумя параметрами с помощью указателя template<int PNum, typename RT, typename PI, typename P2> inline Binder<FunctionPtr<RT,Pi,P2>, PNum, \ BoundVal<typename FunctorParam<FunctionPtr<RT,PI,P2>, PNum >::Type > > bindfp (RT (*fp)(P1,P2), typename ForwardParamT <typename FunctorParam<FunctionPtr<RT,PI,P2>, PNum >::Type >::Type val) { return Binder<FunctionPtr<RT,PI,P2>, PNum, BoundVal <typename FunctorParam <FunctionPtr<RT,PI,P2>, PNum >::Type > ' >(func_ptr(fp), BoundVal<typename FunctorParam <FunctionPtr<RT,Pl,P2>, PNum >::Type >(val) ); }
22.9. Операции с функторами: полная реализация 489 22.9. Операции с функторами: полная реализация Чтобы проиллюстрировать суммарный эффект, достигаемый с помощью рассмотренного усовершенствованного подхода к композиции функторов и связыванию значений, представим полную реализацию этих операций для функторов, число параметров в которых не превышает трех. Обобщение на случай 10 или большего количества параметров выполняется по аналогии и не вызывает принципиальных затруднений. Небольшое количество параметров выбрано лишь из соображений экономии места. Сначала рассмотрим пользовательскую программу. // functors/functorops.cpp #include <iostream> tinclude <string> #include <typeinfo> #include "functorops.hpp" bool compare(std::string debugstr, double vl, float v2) { if (debugstr != "") { std::cout « debugstr « ": " « vl « (vl<v2? '<• : '>') « v2 « '\n'; } return vl<v2; void print_name_value (std::string name, double value) std::cout « name « ": " « value « ' \n'; double sub (double a, double b) return a-b; double twice (double a) return 2*a; int main() using std::cout; // Демонстрация композиции:
490 Глава 22. Объекты-функции и обратные вызовы cout << "Composition result: " << compose(func_ptr(sub), funcj?tr(twice))(3.0, 7.0} « '\n»; // Демонстрация связывания: cout << "Binding result: " << bindfp<l>(compare, "mainO ->Compare() ") (1.02,1.03) « »\n'; cout << "Binding output: "; bindfp<l> (print_name__value, "the ultimate answer to life") (42); // Комбинация композиции и связывания: cout << "Mixing composition and binding (bind<l>): " << bind<l> (compose (func__ptr (sub) , f unc__ptr (twice)) , 7.0) (3.0) « '\n'; v cout << "Mixing composition and binding (bind<2>): " << bind<2>(compose(funcjptr(sub) , func_jptr(twice)) , 7.0) (3.0) « '\n-; } Вывод этой программы имеет такой вид: Composition result: -8 Binding result: main()->compare(): 1.02<1.03 1 Binding output: the ultimate answer to life: 42 Mixing composition and binding (bind<l>): 8 Mixing composition and binding (bind<2>): -8 Рассмотрев эту небольшую программу, можно сделать заключение, что использовать операции с функторами, реализация которых описана в данном разделе, очень просто (несмотря на то, что сама реализация оказалась нелегкой задачей). Заметим также, что связывание и композиция шаблонов органично сочетаются друг с другом. Это достигается благодаря небольшому набору соглашений, сформулированному для функторов в разделе 22.6.1 (стр. 457). Они не идут вразрез с требованиями, установленными для итераторов в стандартной библиотеке C++. Для функторов, которые не подчиняются принятым правилам, легко создать оболочку из класса-адаптера (как проиллюстрировано на примере шаблонов f unc__ptr ()). Кроме того, наше построение позволяет современным компиляторам избежать ненужных задержек в работе сгенерированного кода по сравнению с функторами, код которых написан вручную. Наконец, приведем содержимое файла f unctorops . hpp, в котором перечислены заголовочные файлы, необходимые для компиляции предыдущего примера.
22.10. Заключение 491 // functors/functorops.hpp #ifndef FUNCT0R0PS_HPP #define FUNCT0R0PS_HPP // Определение func_ptr(), FunctionPtr, and FunctionPtrT #include "funcptr.hpp" // Определение шаблона Composero #include "composes.hpp" // Определение вспомогательной функции compose() #include "composeconv.hpp" // Определение шаблона Bindero: // — включает файл boundval.hpp с определением шаблонов // BoundValo и StaticBoundValo; // — включает файл forwardparam.hpp с определением // шаблона ForwardParamTo; // — включает файл functorparam.hpp с определением // шаблона Func tor Paramo; // — включает файл binderparams.hpp с определением // шаблона BinderParamso ; // — включает файл signselect.hpp с определением // шаблона SignSelectTo #include "binder5.hpp" // Определение вспомогательных функций bind() и bindfpO #include "bindconv.hpp" #include "bindfpi.hpp" #include "bindfp2.hpp" # include "bindfp3.hpp" #endif // FUNCT0R0PS_HPP 22.10. Заключение В библиотеке STL, входящей в состав стандартной библиотеки C++, широко используется концепция функторов. Например, с помощью функторов настраивается поведение всех алгоритмов. Многие из этих функторов представляют собой так называемые предикаты (predicates). Предикаты— это функторы или объекты-функции, возвращающие значения логического типа (т.е. такие, которые можно преобразовать к типу bool). Вообще говоря, предикаты должны быть чистыми функторами, иначе результаты работы алгоритмов могут оказаться непредсказуемыми (см. раздел 8.1.4 [18]).
492 Глава 22. Объекты-функции и обратные вызовы В состав стандартной библиотеки C++ входит несколько стандартных функторов и адаптеров для осуществления композиции. По сути, для всех распространенных унарных и бинарных операторов в библиотеке имеется объект-функция. Более подробную информацию по этому вопросу можно найти в [18] (разделы 8.2 и 8.3). Однако заметим, что в стандартной библиотеке C++ недостаточно адаптеров для того, чтобы обеспечить поддержку разнообразных композиций объектов-функций. Например, невозможно скомбинировать результаты двух унарных операций, чтобы сформулировать критерий наподобие "это и то". В библиотеках Boost содержатся дополнительные адаптеры, которые заполняют этот пробел [6].
Приложение А Правило одного определения Правило одного определения {one-definition rule — ODR) — это краеугольный камень хорошо оформленного структурирования программ на C++. Наиболее общие следствия этого правила достаточно просты для того, чтобы их запомнить и использовать: следует определять невстраиваемые функции ровно один раз для всех файлов, а классы и встраиваемые функции — не более одного раза на единицу трансляции. При этом нужно следить за тем, чтобы все определения одного и того же объекта были идентичны. Однако основные проблемы заключены в деталях, которые в сочетании с инстанци- рованием шаблона могут оказаться обескураживающими. Данное приложение поможет заинтересованному читателю получить всестороннее представление об ODR. Если отдельные темы подробно описаны в той или иной главе книги, об этом обязательно упоминается в тексте приложения. АЛ. Единицы трансляции На практике программы на C++ пишутся путем заполнения файлов "кодом". Однако границы, устанавливаемые файлом, не так важны в контексте ODR. Вместо этого важную роль играют так называемые единицы трансляции. По сути, единица трансляции представляет собой результат препроцессорной обработки файла, переданного компилятору. Препроцессор удаляет части кода в соответствии с директивами условной компиляции (#if, #if def и связанными с ними), удаляет комментарии, вставляет (рекурсивно) файлы, определяемые директивой #include, и разворачивает макросы. Следовательно, в контексте ODR файлы // Файл header.hpp: #ifdef DO_DEBUG #define debug(x) std::cout « x « ' \n' #else #define debug(x) #endif void debug_init();
494 Приложение А. Правило одного определения и // Файл myprog;cpp: #include "header.hpp" int main() { { debug__init () ; debug (" ma in () ") ; } эквивалентны одному файлу: // Файл myprog.cpp: void debug_init(); int main() { debug_JLnit () ; ч } \ Связи между единицами трансляции устанавливаются с помощью соответствующих объявлений с внешним связыванием в двух единицах трансляции (например, двух объявлений глобальной функции debug__init ()) или с помощью поиска, зависящего от аргумента, выполняемого во время инстанцирования экспортированных шаблонов. Следует отметить, что концепция единицы трансляции несколько более абстрактна, чем просто концепция "файла, обработанного препроцессором". Например, если дважды передать компилятору файл после обработки препроцессором для формирования единой программы, он выдаст две отдельные единицы трансляции (хотя, надо сказать, для этого нет причин). А.2. Объявления и определения В разговоре между собой программисты часто не делают различия между терминами объявление и определение. Тем не менее в контексте ODR важно точное значение этих слов . Объявление— это конструкция C++, которая первый раз (или повторно) вводит в программу какое-либо имя. Объявление может одновременно быть и определением, в зависимости от того, какой объект и как вводится. • Пространства имен и псевдонимы пространств имен. Объявления пространств имен и псевдонимов пространств имен являются определениями, хотя термин "определение" в этом контексте необычен, поскольку список членов пространства имен позднее может быть расширен (в отличие, например, от классов и перечислимых типов). Нам кажется, что при обмене идеями из С и C++ признаком хорошего тона является крайне осторожное употребление этих терминов. Такой подход используется и в данной книге.
А.З. Детали правила одного определения 495 • Классы, шаблоны классов, функции, шаблоны функций, функции-члены и шаблоны функций-членов. Объявление является определением тогда и только тогда, когда объявление включает связанное с именем тело, ограниченное фигурными скобками. Это правило включает объединения, операторы, операторы-члены, статические функции-члены, конструкторы и деструкторы, а также явные специализации шаблонных версий таких объектов (т.е. любого объекта типа класса и типа функции). • Перечисления. Объявление является определением тогда и только тогда, когда оно включает список перечисленных значений, заключенный в фигурные скобки. • Локальные переменные и нестатические данные-члены. Эти объекты всегда рассматриваются как определения, хотя изредка встречаются и исключения. • Глобальные переменные. Если непосредственно перед объявлением не стоит ключевое слово extern или переменная инициализирована, такое объявление глобальной переменной является одновременно ее определением. В ином случае это не определение. • Статические данные-члены. Объявление является одновременно и определением тогда и только тогда, когда оно дается за пределами класса или шаблона класса, членом которого оно является. • Конструкция typedef, объявления using, директивы using. Никогда не могут быть определениями, хотя конструкции typedef могут сочетаться с определениями класса или объединения. • Директивы явного инстанцирования. Их можно считать определениями. А.З. Детали правила одного определения Как уже упоминалось во введении к данному приложению, практическое использование этого правила связано с массой тонкостей. Рассмотрим ограничения данного правила в соответствии с областью их действия. А.3.1. Ограничения "одно на программу" Перечисленные ниже объекты могут иметь лишь одно определение на программу. • Настраиваемые функции и невстраиваемые функции-члены. • Переменные с внешним связыванием (по сути, переменные, объявленные в области видимости пространства имен или в глобальной области видимости, а также с использованием спецификатора static). • Статические данные-члены. • Шаблоны невстраиваемых функций, шаблоны невстраиваемых функций-членов, невстраиваемые члены шаблонов классов в случае, если они объявлены с ключевым словом export.
496 Приложение А. Правило одного определений • Статические данные-члены шаблонов классов в случае, если они объявлены с ключевым словом export. Например, приведенная ниже программа на C++, состоящая из двух единиц трансля- ции, неработоспособна . // Единица трансляции 1: int counter; // Единица трансляции 2: int counter; // ошибка: определено дважды! // (нарушение ODR) Это правило не применяется к объектам с внутренним связыванием (объекты, объявленные в области видимости безымянного пространства имен или в глобальной области видимости с использованием спецификатора static), поскольку, даже когда два таких объекта имеют одно и то же имя, они считаются разными. Объекты, объявленные в безымянных пространствах имен, считаются различными, если они находятся в разных единицах трансляции. Например, следующие две единицы трансляции можно объединить в корректную программу на C++: // Единица трансляции 1: static counter =2; // не имеет отношения к другим // единицам трансляции namespace { void unique() // не имеет отношения к другим { // единицам трансляции } } // Единица трансляции 2: static counter =0; // не имеет отношения к другим // единицам трансляции namespace { void unique() // не имеет отношения к другим { // единицам трансляции ++counter; } } int main() { unique(); } Интересно, что эта программа работает на С, поскольку в С есть понятие пробного определения, которое является определением переменной без инициализации и может присутствовать в программе неоднократно.
A3. Детали правила одного определения 497 Кроме того, если упомянутые выше объекты используются, в программе должен быть только один из них. У термина "используются" в данном контексте точное значение. Он означает, что к данному объекту где-то в программе есть обращение. Это обращение может быть доступом к значению переменной, вызовом функции или получением адреса такого объекта. Это может быть явное обращение в исходном тексте, но может быть и неявное. Например, выражение с оператором new может создавать неявный вызов ассоциированного оператора delete для обработки ситуаций, когда конструктор генерирует исключение, которое требует очистки неиспользуемой (но выделенной) памяти. Другой пример — конструкторы копирования, которые должны быть определены, даже если в конечном итоге они удаляются оптимизатором. Виртуальные функции также используются неявно (с помощью внутренних структур, которые обеспечивают их вызовы), если только это не чисто виртуальные функции. Существует несколько других видов неявного использования, однако для краткости изложения здесь они не рассматриваются. Существует два типа обращений, которые не являются использованием в данном контексте. Первый тип — это обращение к объекту в операторе sizeof. Второй тип подобен первому, но с определенным отклонением: если обращение является частью оператора typeid (см. раздел 5.6, стр. 79), то оно не является использованием в предыдущем контексте, если только аргумент оператора typeid не заканчивается указанием полиморфного объекта (объекта с (возможно унаследованными) виртуальными функциями). Рассмотрим, например, программу, содержащуюся в одном файле. #include <typeinfo> class Decider { #if defined(DYNAMIC) virtual -Decider() { } #endif }; extern Decider d; int main() { const char* name = typeid(d).name(); return (int)sizeof(d); } Эта программа работает тогда и только тогда, когда не определен символ препроцессора DYNAMIC. На самом деле переменная d не определена, однако обращение к d в операторе sizeof (d) не является использованием, а обращение в операторе typeid (d) может быть использованием, только если d— объект полиморфного типа (поскольку в общем случае не всегда удается определить результат применения полиморфного оператора typeid до момента выполнения).
498 Приложение А. Правило одного определения В соответствии со стандартом C++ ограничения, описанные в данном разделе* не требуют диагностических сообщений от компилятора C++. На практике же компоновщик почти всегда сообщает о них как о даойных или пропущенных определениях. А.3.2- Ограничения "одно на единицу трансляции" Ни один объект не может быть определен в единице трансляции более одного раза. Так, приведенный ниже код на C++ работать не будет. inline void f() {} inline void fО Л) // ошибка: двойное определение Это одна из причин для того, чтобы окружить код в заголовочных файлах так называемыми предохранителями, II Файл guard_ demo.hpp: #ifndef GUARD_DEMO_HPP #define GUARD_DEMO_HPP #endif // GUARDJDEtoO_HPP Такие предохранители обеспечивают игнорирование содержимого файла при его повторном включении директивой #include, что позволяет избежать двойного определения любого класса, встроенной функции или шаблона, содержащихся в нем. Правило ODR также указывает, что в определенных обстоятельствах некоторые объекты должны быть определены. Это требуется, например, в случае типов классов, встроенных функций и ^экспортированных шаблонов. Рассмотрим вкратце конкретные правила. Класс типа X (включая struct и union) должен быть определен в единице трансляции до того, как в этой единице трансляции будет выполнена какая-либо из описанных ниже операций. • Создание объекта типа X (например, путем объявления переменной или с помощью оператора new). Создание может быть непрямым, например когда создается объект, который содержит в себе объект типа X. • Объявление данных-члена класса X. • Применение оператора sizeof или typeid к объекту типа X. • Явный или неявный доступ к объекту типа X. • Преобразование выражения в тип X или из типа X с помощью процедуры преобразования любого вида; преобразование выражения в указатель или ссылку на тип X либо из указателя или ссылки на тип X (за исключением void*) с помощью операции неявного приведения, а также с помощью операторов static_cast или dynamic_cast. • Присвоение значения объекту типа X. • Определение или вызов функции с аргументом либо возвращаемым значением типа X. Вместе с тем простое объявление такой функции не требует определения типа.
А.З. Детали правила одного определения 499 Правила для типов применяются также к типам, генерируемым из шаблонов классов. Это означает, что в тех ситуациях, когда должен быть определен такой тип X, должны быть определены и соответствующие шаблоны. Эти ситуации создают так называемые точки инстанцирования (см. раздел 10.3.2, стр. 171). Встраиваемые функции должны быть определены в каждой единице трансляции, в которой они используются (в которой они вызываются или в которой определяются их адреса). Однако, в отличие от типов классов, их определение может идти после точки использования. inline int not_so_fast(); int main () { not_so__fast () ; } inline int not_so_fast () { } Хотя это правильный код на C++, некоторые компиляторы не встраивают вызов функции с телом, которого еще не видно; поэтому желаемого эффекта можно не достичь. Как и в случае шаблонов классов, использование функции, сгенерированной из параметризованного объявления функции (шаблона функции или функции-члена либо функции-члена шаблона класса), создает точку инстанцирования. Однако, в отличие от типов классов, соответствующее определение может идти после этой точки (или его вообще может не быть, если функция экспортирована). Аспекты ODR, о которых идет речь в этом приложении, как правило, легко проверить с помощью компиляторов C++. Поэтому стандарт C++ требует, чтобы компиляторы выполняли определенную диагностику при нарушении этих правил. Исключением является отсутствие определения для неэкспортированной параметризованной бункпии. Такие ситуации обычно не диагностируются. А.3.3. Ограничения эквивалентности единиц перекрестной трансляции Способность определять некоторые виды объектов в более чем одной единице трансляции создает потенциальную возможность возникновения ошибки нового вида: многократные определения, которые не согласуются друг с другом. К сожалению, такие ошибки трудно обнаружить с помощью традиционной технологии компиляции, в которой единицы трансляции обрабатываются по одной за раз. Таким образом, стандарт C++ не требует обнаружения или диагностирования различий во многократных определениях (но, конечно, позволяет делать это). Если это ограничение на единицы перекрестной трансляции нарушается, стандарт C++ квалифицирует это как фактор, ведущий к неопределенному поведе-
500 Приложение А. Правило одного определения нию, что означает возможность любых предсказуемых и непредсказуемых событий . Обычно такие ошибки, не поддающиеся диагностике, могут вызывать аварийные ситуации или приводить к неправильным результатам, но в принципе они могут повлечь за собой и другие, более прямые виды ущерба (например, повреждение файлов). Ограничения на единицы перекрестной трансляции указывают, что, когда объект определен в двух различных местах кода, эти два места должны состоять из одинаковой последовательности лексем (ключевых слов, операторов и т.п., оставшихся после обработки препроцессором). Кроме того, в одинаковом контексте эти лексемы должны иметь одинаковое значение (например, может потребоваться, чтобы идентификаторы ссылались на одну и ту же переменную). Рассмотрим пример. // Единица трансляции 1: "static int counter = 0; inline void increase_counter() { ++counter; ' \ int main() { } // Единица трансляции 2: static int counter = 0; inline void increase_counter() { ++counter; } Этот пример выдает сообщение об ошибке, поскольку, хотя последовательность лексем для встроенной функции increase_counter () выглядит идентично в обеих единицах трансляции, они содержат лексему counter, которая указывает на два разных объекта. Действительно, поскольку две переменные counter имеют внутреннее связывание (спецификатор static), они никак не связаны между собой, несмотря на одинаковое имя. Отметим, что это ошибка, несмотря на то что в данном случае не используется ни одна из встроенных функций. Размещение в заголовочных файлах (при необходимости подключаемых к программе с помощью директивы #included) определений объектов, которые могут быть определены в нескольких единицах трансляции, обеспечивает идентичность последовательно- Забавно, что версия 1 компилятора gcc действительно делает это, запуская в таких ситуациях игру Rogue.
А.З. Детали правила одного определения 501 сти лексем почти во всех ситуациях . При таком подходе ситуации, в которых две идентичные лексемы ссылаются на различные объекты, становятся довольно редкими. Но, если такая ситуация все же случается, возникающие при этом ошибки часто загадочны и их сложно отследить. Ограничения на перекрестные единицы трансляции касаются не только объектов, которые могут быть определены в нескольких местах, но и аргументов по умолчанию в объявлениях. Иными словами, приведенная ниже программа обладает непредсказуемым поведением. // Единица трансляции 1: void unused(int = 3); int main() // Единица трансляции 2: void unused(int = 4) ; Следует отметить, что с эквивалентностью потоков лексем иногда могут быть связаны неявные тонкие эффекты. Следующий пример (слегка измененный) взят из стандарта C++. // Единица трансляции 1: class X { public: X(int); X(int, int); }; X::X(int = 0) { } class D : public X { }; D d2; //D() вызывает X(int) // Единица трансляции 2: class X { public: X(int); X(int, int); }; X::X(int = 0, int = 0) Иногда директивы условной компиляции по-разному вычисляются в разных единицах трансляции. Такие директивы следует использовать осторожно. Возможны также и другие отличия, но они встречаются реже.
502 Приложение А. Правило одного определения { } class D : public X { // D() вызывает X(int,int) }; // Неявное определение D() нарушает ODR В этом примере проблема связана с тем, что неявные конструкторы по умолчанию класса D, генерируемые в двух единицах трансляции, отличаются. В одной из них вызывается конструктор X, принимающий один аргумент, в другой вызывается конструктор X, принимающий два аргумента. Несомненно, данный пример — это еще один стимул ограничить аргумент по умолчанию одним местом в программе (при возможности это место должно быть в заголовочном файле). К счастью, размещение аргумента по умолчанию в определениях за пределами класса — довольно редкое явление. Существует также исключение из правила о том, что идентичные лексемы должны ссылаться на идентичные объекты. Если идентичные лексемы ссылаются на не связанные между собой константы, которые имеют одно и то же значение, а адрес результирующего выражения не используется, то такие лексемы считаются эквивалентными. Это исключение позволяет использовать следующие программные структуры: // Файл header.hpp: #ifndef HEADER_HPP #define HEADER HPP int const length = 10; class MiniBuffer { char buf[length]; }; #endif //HEADER_HPP В принципе, когда этот заголовочный файл включается в две разные единицы трансляции, создаются две отдельные константные переменные под названием length, поскольку в данном контексте const означает static. Однако такие константные переменные часто предназначены для определения постоянных значений во время компиляции, а не конкретных адресов памяти во время выполнения программы. Следовательно, если н.ас ничто не заставляет иметь конкретное место в памяти (например, обращение к переменной по адресу), то вполне достаточно иметь одно и то же значение для двух констант. Это исключение из правила эквивалентности ODR применимо только к целочисленным и перечислимым типам (в эту категорию не попадают типы данных с плавающей запятой и указатели). И наконец, замечание о шаблонах. Имена в шаблонах связываются в две фазы. Так называемые независимые имена связываются в момент, когда определяется шаблон. Для них правила эквивалентности обрабатываются таким же образом, как и для других не-
А.З. Детали правила одного определения 503 шаблонных определений. Для имен, которые связываются в точке инстанцирования, правила эквивалентности должны быть применимы в этой точке и связывания должны быть эквивалентны. Это приводит к тонкому эффекту. Хотя экспортированные шаблоны определяются только в одном месте, они могут иметь многочисленные инстанцирования, которые должны подчиняться правилам эквивалентности. Ниже приведен пример чрезвычайно замысловатого нарушения правила ODR. // Файл header.hpp: #ifndef HEADERJHPP #define HEADER_HPP enum Color { red, green, blue }; // Пространство имен, связанное с // Color — глобальное пространство имен export template<typename T> void highlight(T); void init(); #endif //HEADERJHPP // Файл tmpl_def.cpp: #include "header.hpp" export template<typename T> void highlight(T x) { paint (x); // (1) Зависимый вызов: требуется поиск, // зависящий от аргумента } // Файл init.cpp: # include "header.hpp" namespace { // Безымянное пространство имен! void paint(Color c) // (2) { } } void init() { highlight(blue); // Зависящий от аргумента поиск // (1) приводит к (2) }
504 Приложение А. Правило одного определения // Файл main.cpp: #include "header.hpp" namespace { // Безымянное пространство имен! void paint (Color с) // (3) { } } int main() { init(); highlight (red) ; // Зависящий от аргумента поиск (1) // приводит к (3) ■ / Чтобы понять этот пример, необходимо помнить, что функции, определенные в безымянном просфанстве имен, имеют внешнее связывание. Однако они отличаются от любых функций, определенных в безымянном пространстве имен других единиц трансляции. Следовагельно, две функции paint () различны. Однако у вызова paint () в экспортированном шаблоне есть аргумент, зависящий от шаблона. Следовательно, он не связывается до точки инстанцирования. В нашем примере есть две точки инстанцирова- ния функции highlight<Со1ог>, однако они дают различное связывание имени paint. Следовательно, программа некорректна.
Приложение Б Разрешение перегрузки Разрешение перегрузки — это процесс выбора функции, к которой обращается выражение вызова. Рассмотрим простой пример. void display_num(int); // (1) void display__num(double) ; // (2) int main () { display_jium(399); // соответствует (1) лучше, чем (2) display_num(3.99); // соответствует (2) лучше, чем (1) } Принято говорить, что в этом примере имя функции display_num() перегружается. Когда это имя используется в вызове, компилятор C++ должен выбирать вызываемую функцию среди различных кандидатов с использованием дополнительной информации, главным образом о типах аргументов вызова. В нашем примере интуиция подсказывает целесообразность вызова int-версии, когда функция вызывается с целочисленным аргументом, и double-версии, когда используется аргумент с плавающей точкой. Формальный процесс попытки моделирования этого интуитивного выбора — это и есть процесс разрешения перегрузки. Общие идеи, лежащие в основе принципов, регулирующих разрешение перегрузки, достаточно просты. Однако подробности их реализации значительно усложнились в процессе стандартизации C++. Эта сложность — в основном результат желания обеспечить поддержку реально существующих примеров, которые интуитивно кажутся человеку "наилучшим выбором". Однако при формализации такого интуитивного выбора возникают различные тонкости. В этом приложении достаточно подробно рассматриваются правила разрешения перегрузки. Однако этот процесс настолько сложен, что мы не можем претендовать на полноту охвата данной темы.
506 Приложение Б. Разрешение перегрузки Б.1. Когда используется разрешение перегрузки Разрешение перегрузки — это только одна часть процесса обработки вызова функции. В действительности разрешение перегрузки не является частью любого вызова функции. Во-первых, для вызовов с помощью указателей и вызовов с помощью указателей на функции-члены не требуется разрешения перегрузки, поскольку вызываемая функция полностью определена (во время работы программы) с помощью указателей. Во-вторых, макросы функционального типа нельзя перегружать, поэтому они не попадают под действие разрешения перегрузки. На очень высоком уровне вызов именованной функции может быть обработан следующим образом. • Имя образует начальный набор перегрузки. • При необходимости этот набор может быть уточнен различными способами (например, путем вывода шаблонов). • Любая функция-кандидат, которая совсем не соответствует вызову (даже после учета неявных преобразований и аргументов по умолчанию), удаляется из набора перегрузки. В результате формируется набор так называемых жизнеспособных функций-кандидатов. • Для поиска наилучшего кандидата выполняется разрешение перегрузки. Если таковой есть, выбираем его; в противном случае исход вызова неоднозначен. • Проверяется выбранный кандидат (например, если это закрытый член, выводится соответствующая диагностика). Каждый из этих шагов имеет свои тонкости, однако можно утверждать, что разрешение перегрузки — самый сложный из них. К счастью, несколько простых принципов позволяют прояснить большинство ситуаций. Далее эти принципы будут подробно рассмотрены. Б.2. Упрощенное разрешение перегрузки Разрешение перегрузки располагает жизнеспособные функции-кандидаты по рангу путем сравнения того, насколько каждый аргумент вызова совпадает с соответствующим параметром кандидата. Один из кандидатов лучше другого, если любой из его параметров соответствует вызову не хуже, чем соответствующий параметр другого кандидата. Данный подход можно проиллюстрировать приведенным ниже примером. void combine(int, double); void combine(long, int); int main() { combined, 2); // Неоднозначность! }
Б.2. Упрощенное разрешение перегрузки 507 В этом примере вызов combine () неоднозначен, поскольку первый кандидат лучше соответствует первому аргументу (литерал 1 типа int), тогда как второй кавдидат лучше соответствует второму аргументу. Можно утверждать, что int в некотором смысле ближе к long, чем к double (который поддерживает выбор в пользу второго кандидата). Однако C++ не пытается определить меру близости для нескольких аргументов вызова. Для этого принципа необходимо указать, насколько хорошо данный аргумент совпадает с соответствующим параметром жизнеспособного кандидата. В первом приближении возможные соответствия можно расположить по рангу (от лучшего к худшему), как описано ниже. • Точное соответствие. Тип параметра совпадает с типом выражения, или его тип является ссылкой на тип выражения (возможно, с добавлением спецификаторов const и/или volatile). • Соответствие, достигаемое минимумом настроек. Включает, например, сведение переменной массива к указателю на его первый элемент или добавление спецификатора const для обеспечения соответствия аргумента типа int** параметру типа int const* const*. • Соответствие, достигаемое продвижением (promotion) типа. Продвижение — это вид неявного преобразования, которое включает преобразование меньших интегральных типов (таких, как bool, char, short, и иногда перечислимых типов) в тип int, unsigned int, long или unsigned long, а также преобразование типа float в тип double. • Соответствие, достигаемое за счет стандартного преобразования. Оно включает любой вид стандартного преобразования (например, типа int в тип float), но исключает неявное обращение к оператору преобразования или конструктору преобразования. • Соответствие за счет преобразования, определяемого пользователем. Позволяет выполнять любой тип неявного преобразования. • Соответствие за счет многоточия (...) в объявлении функции. Параметр, подставляемый на место многоточия, может совпадать почти с любым типом (однако классы, не являющиеся простыми типами данных, приводят к непредсказуемому поведению). Приведенный ниже запутанный пример иллюстрирует некоторые из этих соответствий. int fl(int); // U) int fl(double); // (2) fl(4); // Вызов (1): точное соответствие // ((2) требует стандартного преобразования) int f2(int); // (3) int f2(char); // (4) f2(true); // Вызов (З): соответствие с // продвижением типа // ((4) требует более строгого // стандартного преобразования)
508 Приложение Б. Разрешение перегрузки class X { public: X(int); }; int f3(X); //(5) int f3(...); //(6) f3(7); // Вызов (5): соответствие за счет // пользовательского преобразования // ((6) требует соответствия //за счет многоточия) Отметим, что разрешение перегрузки осуществляется после вывода аргумента шаблона, и этот вывод не учитывает все перечисленные виды преобразований. Проиллюстрируем это на примере. template <typename T> class MyString { public: / MyString(T const*); // Конструктор преобразования }; template<typename T> MyString<T> truncate(MyString<T> const&, int); int main() { MyString<char> strl, str2; strl = truncate<char>("Hello World", 5); // OK str2 = truncate("Hello World", 5); // Ошибка } Неявное преобразование, предусмотренное в конструкторе преобразования, не учитывается при вьшоде аргумента шаблона. Инициализация str2 не обнаруживает жизнеспособной функции truncate (), так что разрешение перегрузки не выполняется вообще. Изложенные принципы — это только первое приближение, но они охватывают очень много случаев. Однако имеется ряд распространенных ситуаций, которые нельзя адекватно объяснить этими правилами. Далее кратко обсуждаются самые важные уточнения этих правил. Б.2.1. Неявный аргумент для функций-членов Вызовы нестатических функций-членов имеют скрытый параметр, доступ к которому возможен в определении функции-члена как *this. Для функции-члена класса Му- Class скрытый параметр обычно бывает типа MyClassfc (для функций-членов типа от-
Б.2. Упрощенное разрешение перегрузки 509 личного от const) или MyClass constfc (для функций-членов типа const)1. Несколько удивляет то, что this имеет тип указателя. Лучше было бы сделать его эквивалентным параметру, который сейчас определен как *this. Однако он был частью первых версий языка C++ еще до того, как в нем появился ссылочный тип данных. К моменту введения этого типа данных было уже написано слишком много кода, который зависел от параметра this, являющегося указателем. Скрытый параметр * this принимает участие в разрешении перегрузки точно так же, как и явные параметры. Почти всегда это вполне естественно, но иногда — неожиданно. В следующем примере приведен строковый класс, который работает не так, как ожидалось (такой код приходится видеть и в реальной жизни). #include <stddef.h> class BadString { public: BadString(char const*); // Доступ к символу через индексацию char& operator[](size_t); // (1) char const& operator[](size_t) const; // Неявное преобразование в С-строку operator char* (); operator char const* (); >; int main() { BadString str("correkt"); str[5] = 'с'; // Возможна неоднозначность // разрешения перегрузки! } Вначале кажется, что в выражении str [5] нет ничего неоднозначного. Оператор индексации в строке (1) выглядит совершенно корректно. Однако это не совсем так, поскольку аргумент 5 — это тип int, а оператор ожидает целое число без знака (size_t и std: : size_t обычно имеют тип unsigned int или unsigned long, но никогда не int). При этом простое стандартное преобразование целочисленного типа легко делает (1) жизнеспособным. Вместе с тем есть и другой жизнеспособный кандидат: встроенный оператор индексации. Действительно, если применить оператор неявного преобразования к типу str (который является неявным аргументом функции-члена), получится указатель. И теперь можно применять встроенный оператор индексации. Этот 1 Это может быть также тип MyClass volatile& или MyClass const volatile&, если функция-член описана как volatile, но это чрезвычайно редкий случай.
510 Приложение Б. Разрешение перегрузки встроенный оператор принимает аргумент типа ptrdif f_t, который на многих платформах эквивалентен int, и потому полностью соответствует аргументу 5. Поэтому, даже если встроенный оператор индексации плохо соответствует неявному аргументу (с помощью пользовательского преобразования типов), это все же лучшее соответствие, чем оператор, определенный в (1) для действительной индексации! Это источник потенциальной неоднозначности2. Чтобы решить эту проблему для конкретной платформы, необходимо объявить оператор [] с параметром ptrdiff_t либо заменить неявное преобразование типа в char* явным (что обычно рекомендуется делать в любом случае, независимо от прочих моментов). Набор жизнеспособных кандидатов может содержать как статические, так и нестатические члены. При сравнении статического члена с нестатическим качество соответствия неявных аргументов игнорируется (только нестатический член имеет неявный аргумент * this). Б.2.2. Улучшение точного соответствия Для аргумента типа int есть три общих типа параметра, которые дают точное соответствие: int, int& и int const&. Тем не менее функция чаще всего перегружается по ссылкам. ' void report(int&); // (1) void report(int const&); // (2) int main() { for (int k = 0; k<10; ++k) { report (k); // Вызов (1) } report(42); // Вызов (2) } В таких случаях для lvalue предпочитается версия без дополнительного спецификатора const, тогда как для rvalue предпочтительна версия с const. Заметим, что то же касается и неявного аргумента функции-члена. class Wonder { public: void tick(); //(1) void tick() const; //(2) void tack() const; //(3) } Отметим, что неоднозначность существует только на платформах, где size_t— синоним для unsigned int. На платформах, где это синоним для unsigned long, тип ptrdif f__t — синоним типа long и неоднозначности нет, поскольку встроенный оператор индексации также требует преобразования индексного выражения.
Б.З. Детали перегрузки 511 void run(Wonder& device) { device.tick(); // Вызов (1) device.tack(); // Вызов (З), поскольку неф // версии Wonder::tack() без // спецификатора const } Наконец, изменив немного предыдущий пример, покажем, как два точных соответствия могут создавать неоднозначность, если выполнять перегрузку со ссылками и без ссылок. void report(int); // (1) void report(int&); // (2) void report(int const&); // (3) int main() { for (int k = 0; k<10; ++k) { report (k) ; // Неоднозначность: (1) и (2> // соответствуют одинаково // хорошо } report (42); // неоднозначность: (1) и (3) // соответствуют одинаково // хорошо } Выводы: • ТиТ constfc одинаково хорошо соответствуют rvalue типа Т; • Т и Т& одинаково хорошо соответствуют lvalue типа Т. Б.З. Детали перегрузки Предыдущий раздел охватывает большую часть ситуаций перегрузки, которые встречаются в повседневном программировании на C++. К сожалению, есть еще очень много правил и исключений из этих правил — больше, чем было бы разумно включать в книгу, которая не посвящена перегрузке функций в C++. Тем не менее обсудим некоторые из них, поскольку они используются несколько чаще, чем другие правила, в том объеме, насколько имеет смысл углубляться в подробности. Б.3.1. Предпочтение нешаблонных функций Если все прочие аспекты разрешения перегрузки равны, нешаблонная функция предпочтительнее экземпляра шаблонной функции (не имеет значения, сгенерирован ли эк-
512 Приложение Б. Разрешение перегрузки земпляр последней из определения обобщенного шаблона или же получен в результате явной специализации). template<typename T> int £(Т); // (1) void f(int); // (2) int main () { return f(7); // Ошибка: выбирается функция (2), II которая не возвращает значение } Этот пример также ясно показывает, что разрешение перегрузки обычно не включает возвращаемый тип выбранной функции. Если делается выбор из двух шаблонов, то предпочтение отдается более специализированному (при условии, что один из них более специализирован, чем другой). Подробное объяснение этого подхода дано в разделе 12.2.2, стр. 212. Б.3.2. Последовательности преобразований Неявное преобразование в общем случае может быть последовательностью элементарных преобразований. Рассмотрим приведенный ниже код. class Base { public: operator short() const; }; class Derived : public Base { }; void count(int); void process(Derived constfc object) { count(object) ; // Соответствие за счет // пользовательского преобразования } Вызов count (object) работает, поскольку object может быть неявно преобразован в int. Однако это преобразование требует, чтобы были выполнены перечисленные ниже шаги. 1. Преобразование object из Derived const в Base const. 2. Пользовательское преобразование полученного объекта из типа Base const в тип short. 3. Преобразование с продвижением типа из short в int.
Б.З. Детали перегрузки 513 Это наиболее общий тип последовательности преобразования: стандартное преобразование (в данном случае из производного типа в базовый), затем пользовательское преобразование, после чего другое стандартное преобразование. Хотя в последовательности может быть не более одного пользовательского преобразования, возможна также последовательность, в которую входят только стандартные преобразования. Важный принцип разрешения перегрузки состоит в том, что если есть последовательность, являющаяся подпоследовательностью другой последовательности преобразования, то предпочтительно использовать подпоследовательность. Так, если бы в рассмотренном примере имелась дополнительная функция-кандидат void count (short), она была бы предпочтительнее из-за отсутствия необходимости третьего шага (продвижения типа) в последовательности преобразований. Б.3.3. Преобразования указателей Указатели и указатели на члены класса могут подвергаться различным специальным стандартным преобразованиям, включая следующие: • преобразования в тип boo 1; • преобразования из типа произвольного указателя в тип void*; • преобразования указателей на производный тип в указатель на базовый тип; • преобразования указателей на члены базового класса в указатели на члены производного типа. Хотя все они могут обеспечивать "соответствие за счет стандартных преобразований", рейтинг у них разный. Прежде всего, преобразование в тип bool (как из обычного указателя, так и из указателя на член класса) считается менее предпочтительным, чем любой другой стандартный тип преобразования. Например: void check(void*); // (1) void check(bool); // (2) void rearrange (Matrix* m) { check(m); // Вызов (1) } В категории преобразований стандартных указателей преобразование в тип void* считается менее предпочтительным, чем преобразование из указателя на производный класс в указатель на базовый класс. Кроме того, если есть указатели на различные классы, связанные наследованием, то предпочтительнее преобразование в указатель на ближайший базовый класс. Приведем небольшой пример, class Interface {
514 Приложение Б. Разрешение перегрузки }; class CommonProcesses : public Interface { class Madhine : public CommonProcesses { }; char* serialize(Interface*); // (1) char* serialize(CommonProcesses*); // (2) void dump (Machine& machine) char* buffer = serialize(machine); // Вызов (2) Преобразование из Machine* в CommonProcesses* предпочтительнее, чем преобразование в Interface*, что вполне^понятно интуитивно. Схожее правило применяется к указателям на члены класса: из двух преобразований связанных типов указателей на члены класса предпочтительнее преобразование с более близким родством в диаграмме наследования. Б.3.4. Функторы и функции-суррогаты Ранее упоминалось, что после поиска имени функции для формирования начального набора перегрузки этот набор обрабатывается различными способами. Интересная ситуация возникает, когда выражение вызова ссылается на объект типа класса, а не на функцию. В этом случае возможны два потенциальных дополнения к набору перегрузки. Первое дополнение очевидно: к набору можно добавить любой оператор-член () (оператор вызова функции). Объекты с такими операторами обычно называются функторами (см. главу 22, "Объекты-функции и обратные вызовы"). Менее очевидное дополнение возникает в ситуации, когда объект типа класса содержит оператор неявного преобразования в указатель на тип функции (или в ссылку на тип функции) . В такой ситуации к набору перегрузки добавляется фиктивная функция (так называемая функция-суррогат). Эта функция-суррогат рассматривается как имеющая неявный параметр, тип которого определяется функцией преобразования, в дополнение к параметрам, типы которых соответствуют таковым в целевой функции. Приведенный ниже пример значительно проясняет ситуацию. Оператор преобразования должен также быть применим, в том смысле, что, например, оператор без спецификатора const не может использоваться с объектами, имеющими спецификатор const.
Б.З. Детали перегрузки 515 typedef void FuncType(double,- int); class IndirectFunctor { public: • ? • operator()(double, double); operator FuncType*() const; >; void activate(IndirectFunctor const& funcObj) { funcObj(3, 5); // Ошибка: неоднозначность! } Вызов funcObj (3,5) рассматривается как вызов с тремя аргументами: funcObj, 3 и 5. Жизнеспособные функции-кандидаты включают член operator () (который рассматривается как имеющий параметры типа IndirectFunctorfc, double и double) и функцию-суррогат с параметрами типа FuncType*, double и int. Суррогат обладает худшим соответствием для неявного параметра (поскольку требуется пользовательское преобразование), однако лучшим соответствием для последнего параметра. Следовательно, невозможно отдать предпочтение какому-либо из этих кандидатов, поэтому вызов будет неоднозначным. Функции-суррогаты относятся к одной из самых "темных" областей C++ и редко используются на практике (к счастью). Б.3.5. Другие контексты перегрузки Мы уже обсудили перегрузку в контексте определения того, к какой функции должно идти обращение в выражении вызова. Однако существует несколько других контекстов, в которых также необходимо сделать подобный выбор. Первый контекст возникает, когда требуется адрес функции. Рассмотрим пример. int n_elements(Matrix constfc); // (1) int n_elements(Vector const&); // (2) void compute() { int (*funcPtr)(Vector const&) = n_elements; // Выбирается (2) } Здесь имя n_elements обращается к набору перегрузки, однако необходим адрес только одной функции. Затем разрешение перегрузки проверяет соответствие нужного типа функции (тип f uncPtr в этом примере) одному из доступных кандидатов.
516 Приложение Б. Разрешение перегрузки Другой контекст, который требует разрешения перегрузки, — это инициализация. К сожалению, в этой теме есть масса тонкостей, которые невозможно охватить в данном приложении. Тем не менее приведем простой пример, который хотя бы проиллюстрирует этот дополнительный аспект разрешения перегрузки. #include <string> class BigNum { public: BigNumdong n) ; BigNum (double n) ; BigNum(std::string const&); operator double(); operator long(); }; void initDemo() { BigNum bnl(100103); BigNum bn2("7057103224.095764"); int in = bnl; } В этом примере разрешение перегрузки необходимо для выбора соответствующего конструктора или оператора преобразования. В подавляющем большинстве случаев правила перегрузки дают интуитивно понятный результат. Тем не менее подробности этих правил довольно сложны и некоторые приложения основаны на менее понятных принципах этой области языка C++ (например, класс std: : auto_ptr).
Библиография В этом приложении перечислены в£е информационные ресурсы, упомянутые в данной книге. Немалая часть информации о современных достижениях в области программирования доступна только в электронном виде, так что не удивляйтесь обилию различных Web-узлов, перечисленных наряду с традиционными книгами и статьями. Мы не претендуем на полноту и завершенность представленного здесь списка, но все перечисленное в нем тем или иным образом имеет отношение к тематике нашей книги. Web-узлы гораздо более изменчивы, чем книги и статьи, а потому указанные здесь ссылки могут оказаться недействительными. Поэтому мы приняли решение поддерживать обновляемый список ссылок на узле по адресу: http: / /www. j osuttis. com/ tmplbook. Прежде чем перейти к книгам, статьям и Web-узлам, перечислим интерактивные ресурсы информации, а именно группы новостей. Группы новостей Многие современные технологии, о которых идет речь в данной книге, были впервые упомянуты в группах новостей, посвященных C++. В ряде случаев такие технологии оказались плодом коллективного творчества участников дискуссий. Приведем небольшой список групп новостей, посвященных C++. • Изучение C++ (немодерируемая): alt. comp. lang. learn. с-C++ • Общие вопросы C++ (немодерируемая): comp. lang. C++ • Общие вопросы C++ (модерируемая): comp. lang. C++. moderated • Вопросы стандарта C++ (модерируемая): comp. std. C++ Если у вас нет доступа к серверу групп новостей, можете воспользоваться архивом по адресу: http: / /groups. google. com. Книги и Web-узлы 1. Andrei Alexandrescu. Modern C++ Design. — Reading, MA : Addison-Wesley, 2001. (Русский перевод: Александреску А. Современное проектирование на C++. — М.: Издат. дом "Вильяме", 2002). 2. Matthew Н. Austern. Generic Programming and the STL. — Reading, MA : Addison- Wesley, 1999.
518 Библиография 3. Jeremy Siek. The Boost Concept Check Library. http://www.boost.org/libs/concept_check/concept_check.htm 4. Todd Veldhuizen. Z?//fz++; Object-Oriented Scientific Computing. http://www.oonumerics.org/blitz 5. The Boost Repository for Free, Peer-Reviewed C++ Libraries. http://www.boost.org 6. Boost Compose Library, http: //www.boost. org/1 ibs/compose 7. Smart Pointer Library, http: //www. boost .org/ libs/ smart_ptr 8. Type Traits Library, http://www.boost.org/libs/type_traits 9. Tom Cargill. Exception Handling: A False Sense of Security II C++ Report. — November- December 1994. Доступно по адресу: http: //www. awprof essional. com/ meyerscddemo/demo/magazine/index.htm 10. James O. Coplien. Curiously Recurring Template Patterns II C++ Report. — February 1995. 11. Core Issue 115 of the C++ Standard. http://anubis.dkuug.dk/jtcl/sc22/wg21/docs/cwg_toc.html 12. Krzysztof Czarnecki, Ulrich W. Eisenecker. Generative Programming. — Reading, MA : Addison-Wesley, 2000. 13. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns. — Reading, MA: Addison-Wesley, 1995. 14. Edison Design Group. Compiler Front Ends for the OEM Market. http://www.edg.com 15. Margaret A. Ellis, Bjarne Stroustrup. The Annotated C++ Reference Manual (ARM). — Reading, MA: Addison-Wesley, 1990. 16. Nicolai M. Josuttis. autojptr and autq_ptr_ref. http://www.josuttis.com/libbook/auto_j?tr.html 17. Nicolai M. Josuttis. Object-Oriented Programming in C++. — John Wiley and Sons Ltd, 2002. 18. Nicolai M. Josuttis. The C++ Standard Library. —Reading, MA : Addison-Wesley, 1999. 19. Andrew Koenig, Barbara E. Moo. Accelerated C++. — Reading, MA: Addison-Wesley, 2000. (Русский перевод: Кёниг Э., My Б.Э. Эффективное программирование на C++. — М.: Издат. дом "Вильяме", 2002.) 20. Jaakko J,orvi, Gary Powell. LL, The Lambda Library. http://www.boost.org/libs/lambda/doc 21. Stanley B. Lippman. Inside the C++ Object Model.—Reading, MA: Addison-Wesley, 1996. 22. Scott Meyers. Counting Objects In C++ // C/C++ Users Journal. — April 1998. 23. Scott Meyers. Effective C++. 50 Specific Ways to Improve Your Programs and Design (2nd Edition). — Reading, MA : Addison-Wesley, 1998 24. Scott Meyers. More Effective C++. 35 New Ways to Improve Your Programs and Designs. — Reading, MA : Addison-Wesley, 1996.
Книги и Web-узлы 519 25. Andrew Lumsdaine, Jeremy Siek. MTL, The Matrix Template Library: http://www•osl.iu.edu/research/mtl 26. D,R. Musser, C. Wang. Dynamic Verification of C++ Generic Algorithms II IEEE Trans, on Software Engineering. — 1997. — Vol. 23, No. 5. 27. Nathan C. Myers. Traits: A New and Useful Template Technique. http://www.cantrip.org/traits.html 28. Robert Davies. NewMatIO, A Matrix Library in C++, http://www.robertnz.com/nm_intro.htm 29. Leslie Brown, et al. The New Shorter Oxford English Dictionary (fourth edition). — Oxford : Oxford University Press, 1993. 30. POOMA: A High-Performance C++ Toolkit for Parallel Scientific Computation. http://www.pooma.com 31. ISO. Information Technology—Programming Languages—C++. Document Number ISO/IEC 14882-1998, ISO/IEC, 1998. 32. ISO. Information Technology—Programming Languages—C++ (as Amended by the first technical corrigendum). Document Number ISO/IEC 14882-2002, ISO/IEC, 2002. 33. Bjame Stroustrup. The C++ Programming Language, Special ed. — Reading, MA : Addison-Wesley, 2000. 34. Bjarne Stroustrup. The Design and Evolution of C++. — Reading, MA: Addison-Wesley, 1994 35. Bjarne Stroustrup. Bjarne Stroustrup's C++ Glossary. http://www.research.att.com/~bs/glossary.html 36. Herb Sutter. Exceptional C++. 47 Engineering Puzzles, Programming Problems, and Solutions. — Reading, MA: Addison-Wesley, 2000. (Русский перевод: Саттер Г. Решение сложных задач на C++. — М.: Издат. дом "Вильяме", 2002.) 37. Herb Sutter. More Exceptional C++. 40 New Engineering Puzzles, Programming Problems, and Solutions. — Reading, MA: Addison-Wesley, 2001. (Русский перевод: Саттер Г. Решение сложных задач на C++. — М.: Издат. дом "Вильяме", 2002.) 38. Erwin Unruh. Original Metaprogramfor Prime Number Computation. http://www.erwin-unruh.de/primorig.html 39. David Vandevoorde. C++ Solutions. — Reading, MA: Addison-Wesley, 1998. 40. Todd Veldhuizen. Using C++ Template Metaprograms II C++ Report. — May 1995. 41. Todd Veldhuizen. Todd Veldhuizen* s Papers and Articles about Generic Programming and Templates, http: //osl. iu. edu/~tveldhui/papers
Глоссарий Данный глоссарий представляет собой набор наиболее важных технических терминов, встречающихся в этой книге. Наиболее полный на текущий момент толковый словарь используемых в C++ терминов представлен в [35]. Abstract class Абстрактный класс ADL Поиск, зависящий от аргументов Angle bracket hack Коррекция угловых скобок Angle brackets Угловые скобки ANSI ANSI Класс, для которого невозможно создание конкретных объектов (экземпляров). Абстрактные классы могут использоваться для объединения общих свойств различных классов в одном типе либо для определения полиморфного интерфейса. Поскольку абстрактные классы используются в качестве базовых, зачастую употребляется аббревиатура А ВС, означающая abstract base class (абстрактный базовый класс) Аббревиатура от argument dependent lookup (поиск, зависящий от аргументов). ADL представляет собой процесс поиска имени функции (или оператора) в пространствах имен и классах, тем или иным способом связанных с аргументами вызова функции, в котором упоминается данная функция или оператор. По историческим причинам иногда называется расширенным поиском Кёнига (или просто поиском Кёнига; данное название применимо в основном к поиску имен операторов) Нестандартная способность компилятора воспринимать два последовательных символа > как две закрывающие угловые скобки (хотя обычно они должны быть разделены пробелом). Например, выражение vector<list<int» в C++ некорректно, но данная возможность позволяет компилятору трактовать его как vector<list<int> > Символы < и >, используемые для выделения списка аргументов или параметров шаблонов American National Standard Institute (Американский национальный институт стандартов). Некоммерческая организация, координирующая усилия по выработке различных стандартов. Подкомитет J16 руководит стандартиза-
522 Глоссарий Argument Argument- depended lookup Class Class template Class type Collection class Constant- expression const member function Container Поиск, зависящий от аргументов Класс Шаблон класса цией C++, тесно сотрудничая с Международной организацией стандартов (ISO) Аргумент Значение (в широком смысле), которое замещает параметр. Например, в вызове функции abs (-3) аргументом является значение -3. В некоторых сообществах программистов аргумент называется фактическим параметром, в то время как параметр называется формальным параметром См. ADL (Поиск, зависящий от аргументов) Описание категории объектов. Класс определяет множество характеристик любого объекта. Сюда входят его данные (атрибуты, данные-члены) и его операции (методы, функции-члены). В C++ классы — это структуры с членами, которые могут представлять собой функции и являются субъектами ограничения доступа. Классы объявляются с использованием ключевого слова class или struct Конструкция, представляющая семейство классов. Определяет шаблон, по которому могут быть сгенерированы действительные классы путем подстановки вместо параметров шаблона конкретных аргументов. Шаблоны классов иногда называют параметризованными классами, хотя данный термин имеет более общий характер Тип, объявленный в C++ с использованием ключевых слов class, struct или union Класс, используемый для управления группой объектов. В C++ классы коллекций называются также контейнерами Выражение, значение которого вычисляется компилятором во время компиляции. Иногда называется также истинной константой, для того чтобы избежать путаницы с константным выражением. Последнее включает выражения, которые являются константами, но не могут быть вычислены в процессе компиляции Константная Функция-член, которая может быть вызвана для функция-член константного или временного объектов, поскольку обычно она не модифицирует объект * this Контейнер См. Collection class (Класс коллекции) Тип класса Класс коллекции Выражение- константа
Книги и Web-узлы 523 Conversion operator Оператор преобразования типов Curiously recurring Модель необыч- template pattern ного рекуррентно- CRTP го шаблона Decay Сведение Declaration Deduction Definition Объявление Вывод Определение Dependent base Зависимый class базовый класс Dependent name Зависимое имя Специальная функция-член, определяющая, каким образом объект может быть неявно (или явно) преобразован в объект другого типа. Объявляется в виде operator type () Аббревиатура от curiously recurring template pattern (модель необычного рекуррентного шаблона). Обозначает код, в котором класс X порождается из базового класса, для которого X выступает в роли аргумента шаблона Неявное преобразование массива или функции в указатель. Например, строковый литерал " Hello" имеет тип char const [ б ], но во многих контекстах C++ он неявно преобразуется в указатель char const* (который указывает на первый символ строки) Конструкция C++, которая вводит (возможно, повторно) имя в область видимости C++. См. Definition (Определение) Процесс, который неявно определяет аргументы шаблона из контекста, в котором используется шаблон. Полностью термин звучит как template argument deduction (вывод аргумента шаблона) Объявление, которое раскрывает детали объявляемого объекта или в случае использования переменных приводит к выделению памяти для объявляемого объекта. Для определений типов классов и функций — объявление, включающее тело, заключенное в фигурные скобки. Для объявлений внешних переменных—либо объявление без ключевого слова extern, либо объявление с инициализатором Базовый класс, который зависит от параметра шаблона. Особое внимание должно уделяться доступу к членам зависимого базового класса. См. Two-phase lookup (Двухфазный поиск) Имя, которое зависит от параметра шаблона. Например, А<Т>:: х является зависимым именем, если А или Т представляют собой параметр шаблона. Имя функции в вызове функции также является зависимым, если любой из аргументов вызова имеет тип, зависящий от параметра шаблона. Например, f в вызове f ((Т*) 0) является зависимым именем, если Т — параметр шаблона. Однако само имя параметра шаблона не рассматривается как зависимое. См. Two-pilose lookup (Двухфазный поиск)
524 Глоссарий Digraph Диграф Dot-C file .С-файл Empty base class Оптимизация optimization ЕВСО пустого базового класса Explicit instantiation directive Директива явного инстанцирования Explicit specializa- Явная специали- tion зация Expression template Friend name injection Full specialization Шаблон выражения Внесение дружественных имен Полная специализация Комбинация из двух последовательных символов, эквивалентная некоторому отдельному символу в программе C++. Назначение диграфов — обеспечить возможность ввода кода на C++ на клавиатурах, на которых отсутствуют некоторые символы. Хотя диграфы используются относительно редко, в тексте может встретиться диграф <:, который образуется, если после открывающей угловой скобки без пробела следует оператор разрешения области видимости : :. Файл, в котором находятся определения переменных и невстраиваемых функций. Большая часть выполнимого (в отличие от декларативного) кода программы размещается в . С-файлах. Эти файлы называются так потому, что обычно имеют такие расширения, как . срр, .С, .с, .ее или . схх. См. также Header file (Заголовочный файл) и Translation unit (Единица трансляции) Аббревиатура от empty base class optimization (оптимизация пустого базового класса). Оптимизация, выполняемая большинством современных компиляторов, когда подобъект "пустого" базового класса не занимает место в памяти Конструкция C++, единственная цель которой — создание точки инстанцирования Конструкция, которая объявляет или дает альтернативное определение шаблона с подстановкой конкретных параметров. Исходный (обобщенный) шаблон называется первичным шаблоном. Если альтернативное определение продолжает зависеть от одного или нескольких параметров шаблона, такое определение называется частичной специализацией, в противном случае это полная специализация Шаблон класса, используемый для представления части выражения. Шаблон сам по себе представляет операцию определенного вида. Параметры шаблона обозначают типы операндов, к которым применима операция Процесс, который делает имя функции, объявленной в качестве дружественной, видимым См. Explicit specialization (Явная специализация)
Книги и Web-узлы 525 Function object Function template Объект-функция Шаблон функции Functor Функтор Header file Заголовочный файл Include file Indirect call Initializer Включаемый файл Косвенный вызов Инициализатор Initializer list Список инициализаторов См. Functor (Функтор) Конструкция, которая представляет семейство функций. Шаблон функции определяет модель, из которой генерируются действительные функции путем подстановки вместо параметров шаблонов конкретных аргументов. Заметим, что шаблон функции является шаблоном, но не функцией. Шаблоны функций иногда называют "параметризованными " функциями, хотя исходный термин является более общим Объект (именуемый также объектом-функцией), который может быть вызван с использованием синтаксиса вызова функции. В C++ это указатели или ссылки на функции и классы с оператором-членом () Файл, предназначенный быть частью единицы трансляции с помощью директивы #include. Такие файлы часто содержат объявления переменных и функций, которые используются более чем в одной единице трансляции, а также определения типов, встраиваемых функций, шаблонов, констант и макросов. Такие файлы обычно имеют расширения .hpp, .h, .H, .hhnim .hxx и иногда называются включаемыми файлами. См. также Dot-Cfile (.С-файл) и Translation unit (Единица трансляции) См. Header file (Заголовочный файл) Вызов функции, в котором вызываемая функция не известна до момента реального вызова в процессе работы программы Конструкция, которая указывает, каким образом должен быть инициализирован именованный объект. Например, в std::complex<float> zl = 1.0, z2(0. 0, 1.0); инициализаторами являются =1.0 и (0.0, 1.0) Список выражений, разделенных запятыми, помещенный в фигурные скобки и используемый для инициализации объектов (или массивов). В конструкторе может использоваться для определения значений для инициализации членов и базовых классов
526 Глоссарий Injected class name Внесенное имя Имя класса в виде, видимом в пределах собст- класса венной области видимости. Для шаблонов классов имя шаблона рассматривается в пределах области видимости шаблона как имя класса, если за этим именем не следует список аргументов Instance Экземпляр Термин instance (экземпляр) имеет в C++ два значения. Значение, взятое из объектно-ориентированной терминологии, означает экземпляр класса, т.е. объект, являющийся реализацией класса. Например, в C++ s td:: cout является экземпляром класса std:: os trearn. Другое значение (используемое практически повсеместно в книге) этого термина— экземпляр шаблона, т.е. класс, функция или функция-член, получающиеся в результате подстановки 1 вместо всех параметров шаблонов конкретных зна- ! чений. В этом смысле экземпляр называется также специализацией, хотя последний термин часто ошибочно употребляется вместо явном специализации Instantiation Инстанцирование Процесс создания обычного класса, функции или функции-члена из шаблона путем подстановки вместо параметров шаблона конкретных значений. Альтернативный смысл — создание экземпляра (объекта) класса — в данной книге не используется. См. Instance (Экземпляр) ISO ISO Международная организация стандартизации (International Organization for Standardization). Рабочая группа ISO с именем WG21 представляет собой движущую силу по стандартизации и развитию C++ Iterator Итератор Объект, который обеспечивает обход последовательности элементов. Зачастую эти элементы принадлежат коллекции. См. Collection class (Класс коллекции) Linkable entity Связываемый Невстраиваемая функция или функция-член, гло- объект бальная переменная или статические данные- член, включая сгенерированные из шаблонов lvalue lvalue, 1-значение В исходном языке программирования С выражение называлось lvalue, если оно могло находиться с левой стороны от оператора присвоения. Напротив, выражения, которые могут располагаться только справа от оператора присвоения, называются rvalue. Это определение в современных С и C++ не вполне точное, и сейчас lvalue означает выражение, предназначение которого — указывать объект по имени или адресу
Книги и Web-узлы 527 Member function Шаблон функции- template члена означает выражение, предназначение которого — указывать объект по имени или адресу (посредством указателя^, ссылки или массива), а не представлять собой вычисление. Современные lvalue не обязаны быть модифицируемы (например, имя константного объекта представляет собой немодифицируемое lvalue). Все выражения, не являющиеся lvalue, являются rvalue. В частности, временные объекты, как создаваемые явно (ТО), так и получаемые в результате вызова функций, являются rvalue. Member class Шаблон класса- Конструкция, которая представляет семейство чле- template члена нов классов. Это шаблон класса, объявленный внутри другого класса или шаблона класса. Он обладает собственным множеством параметров шаблона (в отличие от члена класса шаблона класса) Конструкция, которая представляет семейство функций-членов. Она имеет собственное множество параметров шаблона (в отличие от функции-члена шаблона класса). Эта конструкция очень похожа на шаблон функции, но при подстановке всех ее параметров шаблонов в результате получается функция- член, а не обычная функция. Шаблоны функций- членов не могут быть виртуальными Member class template {Шаблон класса-члена) или Member junction template {Шаблон функции-члена) Имя, которое не зависит от параметра шаблона. См. Dependent name {Зависимое имя) и Two- phase lookup (Двухфазный поиск) Аббревиатура от one-definition rule {правило одного определения). Это правило накладывает определенные ограничения на определения, находящиеся в программе на C++. См. раздел 7.4 и Приложение А, "Правило одного определения" Процесс, который выбирает, какая конкретно функция будет вызвана при наличии нескольких кандидатов (обычно имеющих одно и то же имя) Parameter Параметр Заполнитель, вместо которого в некой точке будет подставлено фактическое "значение" {аргумент). В случае параметров макросов и параметров шаблонов данная подстановка выполняется во время компиляции. В случае параметров вызова функции эта замена осуществляется в Member template Шаблон члена Nondependent Независимое имя name ODR Правило одного определения Overload resolution Разрешение перегрузки
528 Глоссарий Parametrized class Parametrized function Параметризованный класс Параметризованная функция Partial specialization Частичная специализация POD Обычные данные POI Точка инстанци- рования . Policy class Класс стратегии процессе выполнения программы. В некоторых сообществах программистов параметр называется формальным параметром у в то время как аргумент называется фактическим параметром. См. также Argument {Аргумент) Шаблон класса или класс, вложенный в шаблон класса. Оба они называются параметризованными, поскольку не могут однозначно соответствовать некоторому единственному классу до тех пор, пока не будут определены аргументы шаблона Шаблон функции или функции-члена либо функция-член в шаблоне класса. Все они называются параметризованными, поскольку не могут однозначно соответствовать некоторой единственной функции (или функции-члену) до тех пор, пока не будут определены аргументы шаблона Конструкция, которая объявляет или дает альтернативное определение для некоторых подстановок шаблона. Исходный (обобщенный) шаблон называется первичным шаблоном. Альтернативное определение продолжает зависеть от параметров шаблона. В настоящее время эта конструкция применяется только для шаблонов классов. См. также Explicit specialization {Явная специализация) Аббревиатура от plain old data (type) (обычный старый тип данных). Типы POD представляют собой типы, которые могут быть определены без использования возможностей C++ (таких, как виртуальные функции-члены, модификаторы доступа и т.п.). Например, обычная структура языка С является POD Аббревиатура от point of instantiation (точка ин- станцирования). POI — это место в исходном коде, в котором шаблон (или член шаблона) мысленно развертывается путем подстановки аргументов шаблона вместо параметров шаблона. На практике такое развертывание не обязано выполняться в каждой точке инстанцирования. См. также Explicit instantiation directive {Директива явного инстанцирования) Класс или шаблон класса, члены которого описывают настраиваемое поведение обобщенного компонента. Стратегии, как правило, передаются в качестве аргументов шаблонов. Например, шаблон сортировки может иметь стратегию упо-
Книги и Web-узлы 529 Polymorphism Полиморфизм Precompiled header Предварительно скомпилированный заголовочный файл Primary template Qualified name Reference counting Первичный шаблон Полное (квалифицированное) имя Подсчет ссылок rvalue Source file Specialization rvalue Исходный файл Специализация рядочения. Классы стратегий называют также шаблонами стратегий или просто стратегиями. См. также Traits template (Шаблон свойств) Способность операции (идентифицируемой ее именем) быть примененной к объектам различных типов. В C++ традиционная объектно- ориентированная концепция полиморфизма (именуемая также полиморфизмом времени выполнения или динамическим полиморфизмом) достигается посредством виртуальных функций, переопределенных в производных классах. Кроме того, шаблоны C++ обеспечивают так называемый статический полиморфизм Исходный код в обработанном виде, быстро загружаемом компилятором. Исходный код предварительно скомпилированного заголовочного файла должен быть первой частью единицы трансляции (другими словами, он не может начинаться где-то в середине единицы трансляции). Зачастую предварительно скомпилированный заголовочный файл соответствует нескольким заголовочным файлам. Использование предварительно скомпилированных заголовочных файлов может существенно сократить время, необходимое для построения больших приложений, разработанных на C++ Шаблон, не являющийся частичной специализацией Имя, содержащее квалификатор области видимости : : Стратегия управления ресурсами, которая отслеживает количество объектов, ссылающихся на некоторый ресурс. Когда эта величина снижается до 0, ресурс может быть освобожден См. lvalue Header file (Заголовочный файл) или Dot-Cfile (.С-файл) Результат подстановки вместо параметров шаблона фактических значений. Специализация может быть создана путем инстанцирования или явной специализации. Данный термин иногда ошибочно путают с явной специализацией. См. также Instance (Экземпляр)
530 Глоссарий Template Шаблон Template argument Аргумент шаблона Конструкция, которая представляет семейство классов или функций. Она определяет модель, по которой путем подстановки вместо параметров шаблона конкретных аргументов могут быть сгенерированы действительные классы и функции. В этой книге данный термин не включает классы, функции и статические данные-члены, которые параметризованы постольку, поскольку являются членами шаблона класса. См. Class template (Шаблон класса), Parametrized class (Параметризован-ный класс), Function template (Шаблон функции) и Parametrized Junction (Параметризован-ная функция) "Значение", подставляемое вместо параметра шаблона. Это значение обычно представляет собой тип, хотя корректными аргументами могут быть также некоторые константные значения и шаблоны Template argument deduction Template-id Вывод аргумента См. Deduction (Определение) шаблона Идентификатор шаблона Template parameter Параметр шаблона Traits template Шаблон свойств Translation unit Единица трансляции Комбинация имени шаблона, за которым следуют его аргументы в угловых скобках (например, std::list<int>) Обобщенный заполнитель в шаблоне. Наиболее общий вид параметров шаблонов — параметры типа, которые представляют различные типы. Параметры, не являющиеся типами, представляют собой константные значения некоторого типа, а шаблонные параметры шаблонов являются шаблонами классов Шаблон, члены которого описывают характеристики (свойства) аргументов шаблона. Обычно цель шаблонов свойств — помочь избежать слишком большого количества параметров шаблона. См. также Policy class (Класс стратегии) Исходный . С-файл со всеми заголовочными файлами и заголовками стандартной библиотеки, включенными с помощью директив #include, исключая текст, который устранен из компиляции с помощью директив препроцессора типа #if. Для простоты можно считать единицу трансляции результатом обработки . С-файла препроцессором. См. Dot-Cfile (.С-файл) и Header file (Заголовочный файл)
Книги и Web-узлы 531 True constant Истинная См. Constant-expression (Выражение- константа константа) Tuple Two-phase lookup Кортеж Двухфазный поиск User-defined conversion Пользовательское преобразование типов Whitespace Пробельный символ Обобщение концепции структуры языка С, в котором обращение к членам может осуществляться по их номерам Механизм поиска имен, используемый для поиска имен в шаблонах. Две фазы представляют собой, во-первых, фазу, когда компилятор впервые встречается с определением шаблона, и, во- вторых, инстанцирование шаблона. Поиск независимых имен выполняется только во время первой фазы, но при этом не рассматриваются независимые базовые классы. Зависимые имена с квалифи- катором области видимости (: :) ищутся только во второй фазе. Поиск зависимых имен без квалифи- катора области видимости может проводиться в обеих фазах, но поиск, зависящий от аргументов, выполняется только во второй фазе Преобразование типов, определенное программистом. Оно может иметь вид конструктора, который может быть вызван с одним аргументом, или оператора преобразования типа. Если конструктор не объявлен с ключевым словом explicit, преобразование типов может выполняться неявно В C++ это символ, который служит разделителем лексем (идентификаторов, литералов, символов и т.п.) в исходном коде. Помимо традиционных символов пробела, начала новой строки и табуляции, сюда входят и комментарии. Другие пробельные символы (например, символ подачи страницы) также иногда являются корректными
Предметный указатель А Adamczyk, Steve, 229 ADL, 146 Alexandrescu, Andrei, 310; 386; 435 ANSI, 521 Barton, John J., 201 С Cargill, Tom, 46 Caves, Jonathan, 18 Coplien, James, 324 CRTP, 320 D Davies, Robert, 365 E EBCDIC, 275 EBCO, 316 export, 90 G Gibbons, Bill, 164; 203; 324 H Hartinger, Roland, 243 I inline, 94 ISO, 526 J Jrvi, Jaakko, 254; 365 Josuttis, Nicolai M., 29 к Koenig, Andrew, 164 L Lippman, Stan, 446 lvalue, 526 M Meyers, Scott, 524 Myers, Nathan, 286; 310; 324 N Nackman, Lee R., 201 О ODR, 527 P Pennello, Tom, 164 POI, 777 PoweU, Gary, 254; 365 R rvalue, 126; 526 s SFINAE, /57; 194; 293; 377
Предметный указатель 533 Shirk, Jason, 18 Sick, Jeremy, 109 Spicer, John, 229 STL, 267 Stroustrup, Bjarne, 101; 109; 183 Sutter, Herb, 18 T .template, 66 typeid, 81 typename, 32; 232 typeof, 241 и Unruh, Erwin, 229; 325; 342 using, 158 v Vandevoorde, David, 324; 365; 367 Veldhuizen, Todd, 344; 365 w WG21,526 A Абстрактный базовый класс, 258 Адамчик, Стив, 229 Александреску, Андрей, 310; 386; 435 Анрух, Эрвин, 229; 325; 342 Ассоциированное пространство имен, 147 Ассоциированный класс, 147 Б Бартон, Джон, 201 Блокировка, 402 В Вандевурд, Дэвид, 324; 365; 367 Вельдхаузен, Тодд, 344; 365 Встраиваемые функции, 94 Вывод аргументов шаблона, 36; 129; 193- 204; 530 Допустимые преобразования, 199 Выводимый контекст, 196 Выражение параметризации, 119 Выражения-константы, 327 г Гиббоне, Билл, 164; 203; 324 д Двухфазный поиск, 176 Джосаттис, Николаи, 29 Диграф, 757; 154; 232 Дружественные функции, 138 Дружественный шаблон, 141 Дэвис, Роберт, 365 Е Единица трансляции, 114; 493; 530 и Идентификатор, 144 Имя, 143 Зависимое, 144; 145; 154 Квалифицированное, 143 Независимое, 145 Поиск, 145 Полное, 143 Инициализация нулем, 78 Инстанцирование, 34; 112; 165; 526 Автоматическое, 165 Жадное, 180 Итеративное, 183 Мелкое, 100 Неявное, 165 По запросу, 757 По требованию, 165 Ручное, 186 Явное, 87; 186 Интеллектуальный указатель, 387 Исключение, 388 , Итератор, 267
534 Предметный указатель К Каргилл, Том, 46 Кейвс, Джонатан, 18 Кёниг, Эндрю, 164 Класс Ассоциированный, 147 Пустой базовый, 316 Свойств, 286 Стратегии, 310 Константное выражение, /75; 131 Контейнер, 267; 522 Контекст объявления, 176 Контекстно-зависимый язык программирования, 143Г152 Контекстно-свободный язык программирования, 152 Коплиен, Джеймс, 324 Коррекция угловых скобок, 231 Кортеж, 417 л Липпман, Стэн, 446 Локальный класс, 125 м Майерс, Натан, 286; 310; 324 Мейерс, Скотт, 324 Мелкое инстанцирование, 100 Метапрограммирование, 325 Переменные индукции, 333 Развертывание циклов, 338 Шаблонное, 326 Метод Бартона-Нэкмана, 201 Модель включения, 83; 86; 174; 178 Модель необычного рекуррентного шаблона, 320 Модель разделения, 89; 174 н Набор перегрузки, 506 Нэкман, Ли, 201 о Обобщенное программирование, 266 Объявление, 113; 494 Ограниченное расширение шаблонов, 201 Определение, 113; 494 Оптимизация пустого базового класса, 316 Отладка шаблонов, 98 п Параметр, 527 Вызова, 35 Типа, 32 Фактический, 522 Формальный, 522 Параметр шаблона, 31; 35 Не являющийся типом, 57; 125 Шаблонный, 72 Параметризованные объявления, 119 Пауэлл, Гэри, 254; 365 Пеннелло, Том, 164 Первичный шаблон, 226 Перегрузка, 206; 505 Частичное упорядочение, 27 J Поиск Поиск имен, 145 Двухфазный, 770 Зависящий от аргументов, 146; 147 Обычный, 146; 170 Поиск Кёнига, 146 Полиморфизм, 257; 528 Динамический, 257; 264 Статический, 260; 264 Полная специализация, 276; 220; 222 Шаблона класса, 276 Шаблона функции, 220 Шаблона члена класса, 222 Правило двухфазного поиска, 762 Правило одного определения, 108; 114; 174; 493 Предварительно откомпилированные заголовочные файлы, 95 Предварительное объявление, 166 Предикат, 491 Пространство имен, 494 Прототип, 108
Предметный указатель 535 Р Разбухание кода, 226 Разрешение перегрузки, 505 Распределитель памяти, 74; 136 Рефлексия, 386 с Самотестирование, 457 Саттер, Герб, 18 Сведение, 80; 442; 523 Свойство, 285 Значение, 278 Продвижения, 298 Стратегии, 301 Фиксированное, 281 Шаблон, 276 Связываемый объект, 114 Связывание шаблонов, 722 Сигнатура, 210 Сик, Джереми, 109 Спайсер, Джон, 229 Специализация, 772; 529 Инстанцируемая, 772 Полная, 276; 220; 222; 524 Частичная, 772; 276; 226; 528 Явная, 772; 275; 524 Список типов, 435 Стратегия, 283; 285; 401; 528 Страуструп, Бьерн, 101; 109; 183 Счетчик ссылок, 400 т Тип Класса, 777 Классификация, 371 Составной, 373 Фундаментальный, 371 Точка инстанцирования, 777; 499; 524; 528 Транзитивная, 775 Трассировщик, 103 ф Фактический параметр, 775 Формальный параметр, 775 Функтор, 437; 443; 514; 525 Композиция, 466 Чистый, 458 Функции значения, 290 Функции типов, 290 Функция Косвенный вызов, 438 Объект, 437 Прямой вызов, 438 Суррогат, 514 Типа, 424 X Хартингер, Роланд, 243 ч Частичная специализация, 124; 216; 226 ш Шаблон, 529 typedef, 238 Аргумент, 775; 128; 530 Аргумент по умолчанию, 727 Выражения, 347-67 Дружественный, 141 Идентификатор, 775; 725; 144; 530 Инртанцирование. См. Инстанцирование Класса, 111; 522 Объединения, 120 Отладка, 98 Параметр, 775; 124; 530 Не являющийся типом, 725 Шаблонный, 726 Первичный, 57; 772; 124; 226 Свойств, 276 Связывание, 722 Связывающий, 477 Функции, 772 Функции-члена, 772 Частичная специализация, 124
536 Предметный указатель Экспорт, 89 Шаблон класса, 43 Аргументы по умолчанию, 52; 127 Наследование, 160 Объявление, 44 Специализация, 49 Частичная специализация, 51 Шаблон функции, 31 Аргумент по умолчанию, 233 Вывод аргументов, 36 Перегрузка, 37 Шаблонная метапрограмма, 327 Шаблонная функция, 112 Шаблонная функция-член, 112 Шаблонные параметры шаблонов, 72 Шаблонный аргумент шаблона, 135 Шаблонный класс, 111 Шаблонный параметр шаблона, 126 Шаблоны-члены классов, 68 Ширк, Джесон, 18 э Экспорт шаблонов, 89 я Явная специализация, 154; 215 Явное инстанцирование, 87 Ярви,Яакко, 254; 365
Научно-популярное издание Дэвид Вандевурд, Николаи М. Джосаттис Шаблоны C++: справочник разработчика Литературный редактор Т.П. Кайгородова Верстка О.В.Линник Художественный редактор С.А. Чернокозинский Корректор Л.А. Гордиенко Издательский дом "Вильяме". 101509, Москва, ул. Лесная, д. 43, стр. 1. Изд. лиц. ЛР № 090230 от 23.06.99 Госкомитета РФ по печати. Подписано в печать 11.09.2003. Формат 70X100/16. Гарнитура Times. Печать офсетная. Усл. печ. л. 43,8. Уч.-изд. л. 21,37. Тираж 3500 экз. Заказ № 622. Отпечатано с диапозитивов в ФГУП "Печатный двор" Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникаций. 197110, Санкт-Петербург, Чкаловский пр., 15.