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 полезен для отделения реализаций интерфейсов, которые могут только быть функциями-членами (например, конструкторы, деструкторы