КулЛиб - Классная библиотека! Скачать книги бесплатно 

С++ для инженерных и научных расчетов [Питер Готтшлинг] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
для инженерных
научных расчетов

InDiscovering Modem C+4
An Intensive Course for Scientists, Engineers,
and Programmers

Peter Gottschling

Addison-Wesley
Boston ♦ Columbus • Indianapolis • New York • San Francisco • Amsterdam • Cape Town
Dubai • London • Madrid • Milan • Munich • Paris • Montreal • Toronto • Delhi • Mexico City
Sao Paulo • Sidney • Hong Kong • Seoul • Singapore • Taipei • Tokyo

C++
для инженерных
и научных расчетов

Питер Готтшлинг

Идцдластикд
Москва • Санкт-Петербург
2020

ББК 32.973.26-018.2.75
Г74
УДК 681.3.07
ООО “Диалектика”

Зав. редакцией С.Н. Тригуб
Перевод с английского и редакция канд. техн, наук И.В. Красикова

По общим вопросам обращайтесь в издательство “Диалектика” по адресу:
info@dialektika.com, http://www.dialektika.com

Г74

Готтшлинг, Питер.
C++ для инженерных и научных расчетов.: Пер. с англ. — СПб.: ООО “Диалектика”
2020. — 512 с.: ил. — Парал. тит. англ.
ISBN 978-5-907203-30-3 (рус.)
ББК 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 © 2016.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, elec­
tronic or mechanical, including photocopying, recording or by any information storage retrieval system, without
permission from the Publisher.

Научно-популярное издание

Питер Готтшлинг

C++ для инженерных и научных расчетов

ООО “Диалектика”, 195027, Санкт-Петербург, Маннитогорская ул., д. 30, лит. А, пом. 848
ISBN 978-5-907203-30-3 (рус.)

© ООО “Диалектика”, 2020

ISBN 978-0-13-438358-3 (англ.)

© Pearson Education, Inc., 2016

Оглавление
Предисловие

15

Благодарности

21

Об авторе

23

Глава 1. Основы C++

25

Глава 2. Классы

101

Глава 3. Обобщенное программирование

149

Глава 4. Библиотеки

215

Глава 5. Метапрограммирование

279

Глава 6. Объектно-ориентированное программирование

353

Глава 7. Научные проекты

393

Приложение А. Скучные детали

423

Приложение Б. Инструментарий для программирования

487

Приложение В. Определения языка

499

Библиография

506

Предметный указатель

509

Содержание
Предисловие
Причины для изучения C++
Причины для чтения данной книги
Красавица и чудовище
Языки в науке и технике
Соглашения об оформлении

Благодарности
Об авторе

15
15
16
16
18
19

21
23

Ждем ваших отзывов!

24

Глава 1 • Основы C++

25

1.1. Наша первая программа
1.2. Переменные
1.2.1. Константы
1.2.2. Литералы
1.2.3. Не сужающая инициализация в C++11
1.2.4. Области видимости
1.3. Операторы
1.3.1. Арифметические операторы
1.3.2. Булевы операторы
1.3.3. Побитовые операторы
1.3.4. Присваивание
1.3.5. Поток выполнения
1.3.6. Работа с памятью
1.3.7. Операторы доступа
1.3.8. Работа с типами
1.3.9. Обработка ошибок
1.3.10. Перегрузка
1.3.11. Приоритеты операторов
1.3.12. Избегайте побочных эффектов!
1.4. Выражения и инструкции
1.4.1. Выражения
1.4.2. Инструкции
1.4.3. Ветвление
1.4.4. Циклы
1.4.5. goto
1.5. Функции
1.5.1. Аргументы
1.5.2. Возврат результатов
1.5.3. Встраивание
1.5.4. Перегрузка
1.5.5. Функция main

25
28
30
31
33
34
36
37
40
41
42
42
43
43
44
44
44
45
46
48
48
48
49
52
55
56
56
58
59
60
62

1.6. Обработка ошибок
1.6.1. Утверждения
1.6.2. Исключения
1.6.3. Статические утверждения
1.7. Ввод-вывод
1.7.1. Стандартный вывод
1.7.2. Стандартный ввод
1.7.3. Ввод-вывод в файлы
1.7.4. Обобщенная концепция потоков
1.7.5. Форматирование
1.7.6. Обработка ошибок ввода-вывода
1.8. Массивы, указатели и ссылки
1.8.1. Массивы
1.8.2. Указатели
1.8.3. Интеллектуальные указатели
1.8.3.1. unique_ptr
1.8.4. Ссылки
1.8.5. Сравнение указателей и ссылок
1.8.6. Не ссылайтесь на устаревшие данные!
1.8.7. Контейнеры в качестве массивов
1.9. Структурирование программных проектов
1.9.1. Комментарии
1.9.2. Директивы препроцессора
1.10. Упражнения
1.10.1. Возраст
1.10.2. Массивы и указатели
1.10.3. Чтение заголовка файла Matrix Market

Глава 2. Классы
2.1. Программируйте универсальный смысл, а не технические детали
2.2. Члены
2.2.1. Переменные-члены
2.2.2. Доступность
2.2.3. Операторы доступа
2.2.4. Декларатор static в классах
2.2.5. Функции-члены
2.3. Установка значений. Конструкторы и присваивания
2.3.1. Конструкторы
2.3.2. Присваивание
2.3.3. Список инициализаторов
2.3.5. Семантика перемещения
2.4. Деструкторы
2.4.1. Правила реализации
2.4.2. Корректная работа с ресурсами
2.5. Резюме генерации методов
2.6. Доступ к переменным-членам

63
63
65
70
70
70
71
71
72
73
75
78
78
80
84
84
88
88
89
90
92
92
94
98
98
98
99

101
101
103
104
104
107
108
108
110
110
120
121
125
129
130
130
137
137

2.6.1. Функции доступа
2.6.2. Оператор индекса
2.6.3. Константные функции-члены
2.6.4. Ссылочная квалификация членов
2.7. Проектирование перегрузки операторов
2.7.1. Будьте последовательны
2.7.2. Вопросы приоритетов
2.7.3. Члены или свободные функции
2.8. Упражнения
2.8.1. Полиномы
2.8.2. Перемещающее присваивание
2.8.3. Список инициализаторов
2.8.4. Спасение ресурса

Глава 3. Обобщенное программирование
3.1. Шаблоны функций
3.1.1. Инстанцирование
3.1.2. Вывод типа параметров
3.1.3. Работа с ошибками в шаблонах
3.1.4. Смешение типов
3.1.5. Унифицированная инициализация
3.1.6. Автоматический возвращаемый тип
3.2. Пространства имен и поиск функций
3.2.1. Пространства имен
3.2.2. Поиск, зависящий от аргумента
3.2.3. Квалификация пространств имен или ADL
3.3. Шаблоны классов
3.3.1. Пример контейнера
3.3.2. Проектирование унифицированных интерфейсов классов и функций
3.4. Вывод и определение типа
3.4.1. Автоматический тип переменных
3.4.2. Тип выражения
3.4.3. decltype(auto)
3.4.4. Определение типов
3.5. Немного теории шаблонов: концепции
3.6. Специализация шаблонов
3.6.1. Специализация класса для одного типа
3.6.2. Специализация и перегрузка функций
3.6.3. Частичная специализация
3.6.4. Частично специализированные функции
3.7. Параметры шаблонов, не являющиеся типами
3.8. Функторы
3.8.1. Функциональные параметры
3.8.2. Составные функторы
3.8.3. Рекурсия
3.8.4. Обобщенное суммирование

137
139
140
141
143
143
144
145
147
147
148
148
148

149
149
150
152
156
157
158
159
159
159
162
166
168
168
170
177
177
178
179
180
182
183
183
186
187
189
191
194
196
197
199
202

3.9. Лямбда-выражения
3.9.1. Захват
3.9.2. Захват по значению
3.9.3. Захват по ссылке
3.9.4. Обобщенный захват
3.9.5. Обобщенные лямбда-выражения
3.10. Вариативные шаблоны
3.11. Упражнения
3.11.1. Строковое представление
3.11.2. Строковое представление кортежей
3.11.3. Обобщенный стек
3.11.4. Итератор вектора
3.11.5. Нечетный итератор
3.11.6. Нечетный диапазон
3.11.7. Стек bool
3.11.8. Стек с пользовательским размером
3.11.9. Вывод аргументов шаблона, не являющихся типами
3.11.10. Метод трапеций
3.11.11. Функтор
3.11.12. Лямбда-выражения
3.11.13. Реализация make_unique

Глава 4. Библиотеки
4.1. Стандартная библиотека шаблонов
4.1.1. Вводный пример
4.1.2. Итераторы
4.1.3. Контейнеры
4.1.4. Алгоритмы
4.1.5. За итераторами
4.2. Числовые алгоритмы
4.2.1. Комплексные числа
4.2.2. Генераторы случайных чисел
4.3. Метапрограммирование
4.3.1. Пределы
4.3.2. Свойства типов
4.4. Утилиты
4.4.1. tuple
4.4.2. function
4.4.3. Оболочка для ссылок
4.5. Время — сейчас!
4.6. Параллельность
4.7. Научные библиотеки за пределами стандарта
4.7.1. Иная арифметика
4.7.2. Арифметика интервалов
4.7.3. Линейная алгебра
4.7.4. Обычные дифференциальные уравнения

203
204
205
206
207
208
209
211
211
211
212
212
212
213
213
213
213
213
214
214
214

215
216
216
217
223
232
239
240
241
244
256
256
258
260
260
264
266
267
270
273
273
274
274
275

4.8.

4.7.5. Дифференциальные уравнения в частных производных
4.7.6. Алгоритмы на графах
Упражнения
4.8.1. Сортировка по абсолютной величине
4.8.2. Контейнер STL
4.8.3. Комплексные числа

Глава 5. Метапрограммирование
5.1. Пусть считает компилятор
5.1.1. Функции времени компиляции
5.1.2. Расширенные функции времени компиляции
5.1.3. Простота
5.1.4. Насколько константны наши константы
5.2. Предоставление и использование информации о типах
5.2.1. Свойства типов
5.2.2. Условная обработка исключений
5.2.3. Пример применения константности
5.2.4. Стандартные свойства типов
5.2.5. Свойства типов, специфичные для предметной области
5.2.6. enable_if
5.2.7. Еще о вариативных шаблонах
5.2.7.1. Вариативный шаблон класса
5.3. Шаблоны выражений
5.3.1. Реализация простого оператора
5.3.2. Класс шаблона выражения
5.3.3. Обобщенные шаблоны выражений
5.4. Метанастройка: написание собственной оптимизации
5.4.1. Классическое развертывание фиксированного размера
5.4.2. Вложенное развертывание
5.4.3. Динамическое развертывание: разминка
5.4.4. Развертывание векторных выражений
5.4.5. Настройка шаблона выражения
5.4.6. Настройки операций сверток
5.4.7. Настройка вложенных циклов
5.4.8. Резюме
5.5. Упражнения
5.5.1. Свойства типов
5.5.2. Последовательность Фибоначчи
5.5.3. Метапрограммирование НОД
5.5.4. Шаблон векторного выражения
5.5.5. Метасписок

Глава6. Объектно-ориентированное программирование
6.1.

Фундаментальные принципы
6.1.1. Базовые и производные классы
6.1.2. Наследование конструкторов

275
275
276
276
276
276

279
279
280
282
283
285
287
287
290
291
299
300
301
305
305
308
309
313
315
317
319
322
328
330
332
335
343
349
350
350
351
351
351
352

353
354
354
358

6.1.3. Виртуальные функции и полиморфные классы
6.1.4. Функторы и наследование
6.2. Устранение избыточности
6.3. Множественное наследование
6.3.1. Множественные родители
6.3.2. Общие прародители
6.4. Динамический выбор с использованием подтипов
6.5. Преобразования
6.5.1. Преобразование между базовыми и производными классами
6.5.2. const__cast
6.5.3. reinterpret_cast
6.5.4. Преобразования в стиле функций
6.5.5. Неявные преобразования
6.6. CRTP
6.6.1. Простой пример
6.6.2. Повторно используемый оператор доступа
6.7. Упражнения
6.7.1. Ромбовидное наследование без избыточности
6.7.2. Наследование класса вектора
6.7.3. Функция клонирования

Глава 7. Научные проекты
7.1. Реализация решателей ОДУ
7.1.1. Обыкновенные дифференциальные уравнения
7.1.2. Алгоритмы Рунге-Кутты
7.1.3. Обобщенная реализация
7.1.4. Дальнейшее развитие
7.2. Создание проектов
7.2.1. Процесс построения
7.2.2. Инструменты для построения приложений
7.2.3. Раздельная компиляция
7.3. Несколько заключительных слов

Приложение А. Скучные детали
А. 1. О хорошем и плохом научном программном обеспечении
А.2. Детали основ
А.2.1. О квалифицирующих литералах
А.2.2. Статические переменные
А.2.3. Еще немного об if
А.2.4. Метод Даффа
А.2.5. Еще немного о функции main
А.2.6. Утверждения или исключения?
А.2.7. Бинарный ввод-вывод
А.2.8. Ввод-вывод в стиле С
А.2.9. Сборка мусора
А.2.10. Проблемы с макросами

359
365
367
368
368
369
375
378
379
383
384
384
386
387
387
389
391
391
392
392

393
393
394
396
398
405
406
406
411
415
421

423
423
430
430
431
432
434
434
435
437
438
439
440

А.З. Реальный пример: обращение матриц
А.4. Больше о классах
А.4.1. Указатель на член
А.4.2. Примеры инициализации
А.4.3. Обращение к многомерным массивам
А.5. Генерация методов
А.5.1. Управление генерацией
А.5.2. Правила генерации
А.5.3. Ловушки и советы по проектированию
А.6. Подробнее о шаблонах
А.6.1. Унифицированная инициализация
А.6.2. Какая функция вызвана?
А.6.3. Специализация для определенного аппаратного обеспечения
А.6.4. Бинарный ввод-вывод с переменным числом аргументов
А.7. Использование std: : vector в С++03
А.8. Динамический выбор в старом стиле
A. 9. Подробности метапрограммирования
А.9.1. Первая метапрограмма в истории
А.9.2. Метафункции
А.9.3. Обратно совместимые статические утверждения
А.9.4. Анонимные параметры типа
А.9.5. Проверка производительности динамического развертывания
A. 9.6. Производительность умножения матриц

Приложение Б. Инструментарий для программирования
Б.1. дсс
Б.2. Отладка
Б.2.1. Текстовая отладка
Б.2.2. Отладка с графическим интерфейсом: DDD
Б.З. Анализ памяти
Б.4. gnuplot
Б.5. Unix, Linux и Mac OS

Приложение В. Определения языка
B. 1. Категории значений
В.2. Обзор операторов
В.З. Правила преобразования
B. 3.1. Повышение
В.3.2. Другие преобразования
В.3.3. Обычные арифметические преобразования
В.3.4. Сужение

442
453
453
453
454
457
459
460
465
469
469
470
473
474
475
476
476
476
478
480
481
484
485

487
487
488
489
491
493
494
496

499
499
499
502
503
503
504
505

Библиография

506

Предметный указатель

509

Моим родителям, Хельге и Гансу-Вернеру

Предисловие
Мир построен на C++ (и С — подмножество его).
— Герб Саттер

Инфраструктуры Google, Amazon и Facebook в значительной степени построены
на C++. Кроме того, на C++ реализована значительная часть лежащих в их основе
технологий. В области телекоммуникаций почти все подключения стационарных и
сотовых телефонов управляются с помощью программного обеспечения, написан­
ного на C++. Что еще более важно, все основные узлы передачи в Германии также
обрабатываются с помощью C++, а это означает, что мир в семье автора безогово­
рочно полагается на программное обеспечение, написанное на C++.
Даже программы, написанные на других языках программирования, зависят
от C++, поскольку именно на C++ реализованы самые популярные компилято­
ры — Visual Studio, clang, новейшие части Gnu и компилятор Intel. Тем более
это верно для программного обеспечения, работающего в Windows, которая также
реализована на C++ (как и пакет Office). Это вездесущий язык; даже ваш мобиль­
ный телефон и ваш автомобиль обязательно содержат компоненты, управляемые
C++. Его изобретатель, Бьярне Страуструп, создал веб-страницу, с которой и взя­
то большинство приведенных здесь примеров.
В области науки и техники многие пакеты высококачественного программного
обеспечения реализованы на C++. Сила этого языка проявляется в особенности
тогда, когда размеры проектов превышают некоторый определенный размер и ког­
да используемые структуры данных становятся достаточно сложными. Не удиви­
тельно, что сегодня многие — если не все — моделирующие программы в области
науки и техники реализуются на C++. Abaqus, deal.II, FEniCS, OpenFOAM — толь­
ко некоторые из известных названий; то же самое можно сказать и о таком веду­
щем программном обеспечении в области CAD, как САПА. Благодаря более мощ­
ным процессорам и улучшенным компиляторам (в которых могут использоваться
не все современные возможности и библиотеки), на C++ все чаще реализуются
даже встраиваемые системы. Наконец, мы не знаем, какое количество проектов
было бы реализовано на C++, а не на С, начнись они немного позже. Например,
хороший друг автора Мэтт Книпли (Matt Knepley), являющийся соавтором весьма
успешной научной библиотеки PETSc, как-то раз признался, что если бы это было
возможно, сегодня он переписал бы свою библиотеку на C++.

Причины для изучения C++
Как никакой иной язык, C++ охватывает весь спектр задач от программи­
рования на уровне, достаточно близком к аппаратному обеспечению, на од­
ном конце и до абстрактного программирования высокого уровня на другом.

16

Предисловие

Программирование низкого уровня — типа пользовательского управления памя­
тью — позволяет разобраться, что в действительности происходит во время вы­
полнения, а это, в свою очередь, помогает понять поведение программ на других
языках. В C++ с минимальными усилиями можно написать чрезвычайно эффек­
тивные программы, производительность которых лишь незначительно ниже про­
изводительности выполнимого кода, написанного на машинном языке. Однако не
стоит торопиться, пытаясь достичь максимальной производительности; сначала
лучше сосредоточиться на написании ясных и выразительных программ.
Здесь в игру вступают высокоуровневые возможности C++. Язык непосред­
ственно поддерживает широкий спектр программных парадигм, и среди проче­
го — объектно-ориентированное программирование (глава 6, “Объектно-ори­
ентированное программирование”), обобщенное программирование (глава 3,
“Обобщенное программирование ”), метапрограммирование (глава 5, “Метапрог­
раммирование”), параллельное программирование (раздел 4.6) и процедурное
программирование (раздел 1.5).
Ряд методов и идиом программирования — таких как RAII (раздел 2.4.2.1) или
шаблоны (раздел 5.3) — были изобретены в C++ и для применения C++. Язык про­
граммирования C++ настолько выразителен, что зачастую можно разработать по­
добные новые методы без внесения изменений в сам язык. И как знать, может быть,
в один прекрасный день именно вы изобретете новые методы программирования?

Причины для чтения данной книги
Материал этой книги проверен на реальных людях. Автор три года (три раза по
два семестра) читал своим студентам курс “C++ для ученых”. В конце такого кур­
са студенты (главным образом студенты математического факультета, но присут­
ствовали и студенты физического и некоторых технических факультетов), часто
до прохождения курса не имевшие о C++ никакого понятия, в конце курса вполне
могли использовать такие “продвинутые” средства программирования, как шабло­
ны. Вы можете читать эту книгу в собственном темпе и направлении, следуя по
основному пути от главы к главе или уделяя большее внимание дополнительным
примерам и справочной информации из приложения А, “Скучные детали”.

Красавица и чудовище
Программы на C++ могут быть написаны многими способами. В этой книге мы
постепенно познакомим вас со все более и более сложными стилями программиро­
вания. Это требует использования все более и более сложных возможностей языка
программирования, которые, на первый взгляд, могут показаться пугающими, но
как только вы к ним привыкнете, станут простыми и понятными. Фактически про­
граммирование на высоком уровне не только применимо для широкого диапазона
задач, но и обычно не менее, если не более, эффективно и удобочитаемо.

17

Красавица и чудовище

Чтобы получить первое впечатление, мы проиллюстрируем сказанное на прос­
том примере — методе градиентного спуска с постоянным шагом. Принцип очень
прост: вычисляем наиболее крутой спуск /(х) с использованием градиента, ска­
жем, g(x) и следуем в этом направлении с шагами фиксированного размера до
следующего локального минимума. Алгоритмической псевдокод так же прост, как
и это описание.

Алгоритм 1. Алгоритм градиентного спуска

1

Вход: начальное значение х, размер шага $, критерий остановки е, функция /, градиент g
Выход: локальный минимум х
do

2

|

x = x-sg(x)

3 while |А/(х)| > 8;

Мы напишем две простые реализации для этого простого алгоритма. Взгляни­
те на них, не пытаясь пока что вникать в технические детали.
void gradient-descent ( double * х,
double * у, double sf double eps,
double (*f) (double, double),
double (*gx)(double, double),
double (*gy)(double, double))
{
double val = f(*x, *y), delta;
do {
★x -= s * gx (*x, *y);
*y -= s * gy(*x, *y);
double new_val = f(*x, *y);
delta = abs (new_val - val) ;
val= new_val;
} while(delta > eps) ;
}

template
Value gradient-descent (Value x, Pl s,
P2 eps, F f, G g)
{

auto val = f(x), delta = val;
do {
x -= s * g(x);
auto new_val = f (x) ;
delta = abs (new_val - val) ;
val = new_val;
} while(delta > eps) ;
return x;

На первый взгляд, они очень похожи, и мы расскажем, какой нам нравится
больше. Первая версия в принципе представляет собой чистый С, т.е. этот код
компилируется и с помощью компилятора С. Преимущество заключается в не­
посредственной видимости оптимизации. Перед нами двумерная функция со зна­
чениями типа double. Мы предпочитаем вторую версию как более широко при­
менимую — для функций произвольной размерности с произвольными типами
значений. Как ни удивительно, такая универсальная реализация оказывается не
менее эффективной. Более того, функции F и G могут быть встраиваемыми (см.
раздел 1.5.3), так что экономятся накладные расходы на их вызовы, в то время как
явное использование (уродливых) указателей на функции в левой версии затруд­
няет применение этой оптимизации.

Предисловие

18

Более длинный пример сравнения старого и нового стилей терпеливый чита­
тель может найти в приложении А, “Скучные детали” (раздел А.1). В нем приме­
нение современных методов программирования куда более очевидно, чем в этом
игрушечном примере. Но не будем задерживать ваше знакомство с книгой таки­
ми предварительными примерами.

Языки в науке и технике
Было бы хорошо, если бы все численные программы могли
быть написаны на C++ без потери эффективности,
но если только вы не найдете чего-то, что позволяет
достичь этой цели без ущерба для системы типов C++,
то лучше уж воспользоваться языком Fortran, ассембле­
ром или архитектурно-зависимыми расширениями,
— Бьярне Страуструп

Научные и инженерные программы пишутся на разных языках программиро­
вания, и какой именно из них является наиболее приемлемым, как и везде, зави­
сит от целей и имеющихся ресурсов.
Математический инструментарий, такой как MATLAB, Mathematica или R, пот­
рясающ, если можно использовать имеющиеся в них алгоритмы. Реализуя собс­
твенные алгоритмы с мелкими (например, скалярными) операциями, мы можем
получить значительное снижение производительности. Это может не быть про­
блемой, например, для небольших задач (или бесконечно терпеливого пользовате­
ля); в противном случае необходимо рассмотреть альтернативные языки.
Python отлично подходит для быстрой разработки и уже содержит научные
библиотеки, такие как “scipy” и “numpy”; и приложения, основанные на этих
библиотеках (зачастую реализованных на С и C++), оказываются достаточно эф­
фективными. Но вновь определяемые пользователем алгоритмы с “мелкозернис­
тыми” операциями приводят к снижению производительности. Python является
отличным средством эффективной реализации задач малого и среднего размеров.
Когда проекты вырастают и становятся достаточно большими, все более важной
становится строгость компилятора (например, запрещение присваивания при не­
совпадении аргументов).
Fortran также является отличным выбором, если мы можем опираться на су­
ществующие, хорошо отлаженные операции, например на операции с плотными
матрицами. Он хорошо подходит для выполнения домашних заданий, которые
задает старый профессор (потому что он задает только то, с чем легко справится
с помощью Fortran). По опыту автора, добавление новой структуры данных ока­
зывается весьма громоздким, а написание большой моделирующей программы на
Fortran является довольно сложной задачей, на которую сегодня решается посто­
янно уменьшающееся меньшинство исследователей.

Соглашения об оформлении

19

С обеспечивает хорошую производительность, и кроме того, на С написано
большое количество программ. Язык этот относительно небольшой и прост в
изучении. Проблема заключается в написании без ошибок больших программ,
использующих простые и опасные возможности языка, в особенности указатели
(раздел 1.8.2) и макросы (раздел 1.9.2.1).
Языки наподобие С#, Java и РНР, вероятно, являются хорошим выбором, если
основная сфера применения приложения — веб или обеспечение графического
интерфейса при не слишком большом количестве вычислений.
C++ выделяется среди других языков программирования в особенности тог­
да, когда мы разрабатываем большие, высококачественные программы с высокой
производительностью. Тем не менее процесс разработки не обязан быть медлен­
ным и болезненным. При правильном абстрагировании программы на C++ мож­
но писать довольно быстро. Более того, мы оптимистично полагаем, что в буду­
щем в стандарт C++ будет включено больше научных библиотек.
Очевидно, что чем больше языков мы знаем, тем больший выбор у нас име­
ется. Кроме того, чем лучше мы знаем эти языки, тем более разумным будет наш
выбор. Большие проекты часто содержат компоненты на различных языках, и при
этом в большинстве случаев по крайней мере ядра, критичные к производитель­
ности, реализованы на С или C++. Все это говорит в пользу того, что изучение
C++ — не пустая трата времени, и его глубокое понимание в любом случае будет
не лишним, если вы хотите стать великим программистом.

Соглашения об оформлении
Новые термины и понятия выделены курсивом. Для исходных текстов на C++
использован моноширинный шрифт. Важные детали отмечены полужирным
шрифтом. Классы, функции, переменные и константы даны в нижнем регистре
и при необходимости содержат символы подчеркивания. Исключение составля­
ют матрицы, которые обычно именованы с первой заглавной буквой. Параметры
шаблонов и концепции начинаются с заглавной буквы и могут содержать допол­
нительные заглавные буквы (например, CamelCase). Команды и выход программы
выводятся в следующем виде.
Программы, требующие возможностей С++03, С++11 или С++14, помечены
соответствующей пиктограммой. Некоторые программы, использующие только
небольшое количество возможностей С++11, которые легко заменить выражения­
ми С++03, явно не помечаются.
=> directory/source_code.cpp

За исключением очень короткого иллюстрирующего кода, все примеры про­
грамм в этой книге были протестированы по крайней мере на одном компилято­
ре. Стрелкой указывается путь к полной программе, приводимый в начале абзаца
или раздела, в котором обсуждаются содержащиеся в ней фрагменты кода.

20

Предисловие

Все программы доступны на GitHub в открытом репозитории https://
github.com/petergottschling/discovering_modern_cpp , так что их можно
получить с помощью команды
gi t cl one https://github.com/petergottschli ng/di scoveri ng_modern_cpp.gi t

В Windows удобнее использовать TortoiseGit (см. tortoisegit.org).

Благодарности
Используя хронологический порядок, я хотел бы поблагодарить Карла Меербергена (Karl Meerbergen) и его коллег за первоначальный 80-страничный текст,
использовавшийся в качестве черновика лекций, прочитанных в 2008 году. Со вре­
менем этот текст был переписан, но исходный вариант послужил первоначальным
импульсом, запустившим весь процесс создания книги. Я также в большом долгу
перед Марио Муланаски (Mario Mulanasky) за его вклад в раздел 7.1.
Я чрезвычайно благодарен Кристиану Яну ван Винклю (Christiaan Jan van
Winkel) и Фабио Фракасси (Fabio Fracassi), которые тщательно проверили мель­
чайшие детали рукописи и внесли много предложений для соответствия стандар­
ту и повышения удобочитаемости текста.
Отдельная благодарность — Бьярне Страуструпу (Bjarne Stroustrup) за страте­
гические советы по формированию книги, помощь в контактах с Addison-Wesley,
щедрое разрешение использовать его хорошо подготовленный материал и (не за­
быть бы главное!) за создание C++. Все это заставило меня выполнить трудную
работу по обновлению старого лекционного материала новыми функциями и воз­
можностями С++11 и С++14.
Я признателен Карстену Ахнерту (Karsten Ahnert) за его рекомендации и Мар­
кусу Абелю (Markus Abel) за помощь в избавлении предисловия от чрезмерного
многословия.
Когда я искал интересное применение случайных чисел для раздела 4.2.2.6,
Ян Рудль (Jan Rudi) предложил поделиться эволюцией курса акций, которую он
использовал в своем классе.
Я в долгу перед Техническим университетом Дрездена, который позволил мне
преподавать C++ на факультете математики более 3 лет, и я высоко ценю отзывы
студентов, слушавших этот курс.
В еще большем долгу я перед моим редактором, Грегом Дончем (Greg Doench),
за то, что он принял мой полусерьезный, во многом бессистемный стиль этой
книги, за длительные дискуссии о стратегических решениях, за профессиональ­
ную поддержку, без которой книга никогда не увидела бы свет. Благодарность за­
служила и Элизабет Раян (Elizabeth Ryan), которая управляла всем производствен­
ным процессом и при этом терпеливо выслушивала все мои особые требования.
Последними в списке, но не по важности, идут родные мне люди — моя жена,
Ясмин, и мои дети, Янис, Анисса, Винсент и Даниела, — пожертвовавшие време­
нем, которое мы могли бы провести вместе, так что я смог посвятить его работе
над книгой.

Об авторе
Профессиональная страсть Питера Готтшлинга — написание передового на­
учного программного обеспечения, и он надеется заразить этим вирусом множес­
тво читателей. Эта страсть уже принесла свои плоды в виде разработанной биб­
лиотеки Matrix Template Library 4 и в виде соавторства в ряде других библиотек,
включая Boost Graph Library. Своим опытом программирования он делится, читая
лекции по C++ для студентов университетов и на профессиональных учебных
курсах, а теперь — ив этой книге, которую вы держите в руках.
Питер является членом Комитета по стандартизации ISO C++, заместителем
председателя Германского Комитета по стандартам языков программирования и ос­
нователем группы пользователей C++ в Дрездене. В молодости он учился в Техни­
ческом университете Дрездена, параллельно изучал компьютерные науки и матема­
тику и имеет ученую степень по математике. Закончив работу в ряде академических
учреждений, он основал собственную компанию, SimuNova, и вернулся в родной
Лейпциг, как раз когда город отмечал свое тысячелетие. Питер женат и имеет чет­
верых детей.

24

Об авторе

Ждем ваших отзывов!
Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и
хотим знать, что было сделано нами правильно, что можно было сделать лучше
и что еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши
замечания в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам
бумажное или электронное письмо либо просто посетить наш веб-сайт и оставить
свои замечания там. Одним словом, любым удобным для вас способом дайте нам
знать, нравится ли вам эта книга, а также выскажите свое мнение о том, как сде­
лать наши книги более интересными для вас.
Отправляя письмо или сообщение, не забудьте указать название книги и ее ав­
торов, а также свой обратный адрес. Мы внимательно ознакомимся с вашим мне­
нием и обязательно учтем его при отборе и подготовке к изданию новых книг.
Наши электронные адреса:
E-mail:
inf o@dialektika. com
WWW:
http://www.dialektika.com

Глава 1

Основы C++
Моим детям. Никогда не смейтесь, помогая мне осваивать
компьютер. В конце концов, я учила вас пользоваться горшком.
— Сью Фитцморис
В этой главе мы рассмотрим основные возможности C++. Как и во всей книге,
мы будем рассматривать их с разных точек зрения, но постараемся не вдавать­
ся во все детали, ведь это все равно невозможно. Для более подробной инфор­
мации о конкретных возможностях языка мы рекомендуем вам обратиться к
онлайн-руководствам по адресам http://www.cplusplus.com/ и http://ru.
cppref erence. com.

1.1. Наша первая программа
В качестве введения в язык программирования C++ рассмотрим следующий
пример:
tfinclude
int main ()
{
std::cout « ’’Ответом на Великий Вопрос о Жизни,\п"
« ’’Вселенной и Всяком Таком является: ”
« std::endl « 6 * 7 « std::endl;
return 0;
}

Вывод этой программы, в полном соответствии с Дугласом Адамсом (Douglas
Adams) [2], имеет вид:

Ответом на великий Вопрос о Жизни,
вселенной и всяком Таком является:
42
Этот короткий пример иллюстрирует несколько возможностей C++.
• Ввод и вывод не являются частью ядра языка и предоставляются библио­
текой. Она должна быть включена явно; в противном случае мы ничего не
сможем вводить и выводить.

26

Основы C++



Стандартный ввод-вывод имеет потоковую модель и потому именуется
. Для обеспечения ее функциональности мы в первой строке
программы указываем директиву ее включения #include .

• Каждая программа C++ начинается с вызова функции main. Она возвра­
щает с помощью оператора return целочисленное значение; возврат нуля
означает успешное завершение.
• Фигурные скобки {} означают блок/группу кода (именуемую также состав­
ной инструкцией).


std: :cout и std: :endl определены в . Первое из этих выра­
жений представляет собой выходной поток, который выводит текст на эк­
ран. std: :endl завершает строку. Мы можем также переходить на новую
строку с помощью специального символа \п.



Оператор « можно использовать для передачи объекта в выходной поток,
такой как std: : cout, для выполнения операции вывода.



std: : указывает, что использованный тип (или функция) взят из стандар­
тного пространства имен. Пространства имен помогают организовывать
имена и избегать конфликтов имен (см. раздел 3.2.1).



Строковые константы (более точно — литералы) заключены в двойные ка­
вычки.

• Выражение 6*7 вычисляется и передается в std: :cout как целочисленное
значение. В C++ каждое выражение имеет тип. Иногда мы как программис­
ты обязаны явно объявлять тип, а в других случаях компилятор сам выво­
дит его для нас. 6 и 7 являются константными литералами типа int, так что
их произведение также представляет собой int.

Прежде чем продолжить чтение, мы настоятельно рекомендуем вам скомпи­
лировать и запустить этот маленький пример на своем компьютере. После того
как он будет скомпилирован и запущен, вы сможете немного с ним поиграться,
например, добавляя больше арифметических операций и операций вывода (и рас­
смотреть получаемые сообщения об ошибках). В конце концов, единственным
способом действительно выучить язык является его использование. Если вы уже
знаете, как использовать компилятор или даже интегрированную среду разработ­
ки C++, можете пропустить оставшуюся часть этого раздела.
Linux. В каждом дистрибутиве имеется как минимум компилятор GNU C++,
обычно устанавливаемый по умолчанию (см. краткое введение в разделе Б.1).
Пусть мы назвали нашу программу hello42. срр; тогда ее легко скомпилировать
с помощью команды
g++ hello42.cpp

По традиции прошедшего века по умолчанию результирующий бинарный
файл называется а. out. Но поскольку программ у нас может быть больше, чем

1.1. Наша первая программа

27

одна, давайте используем более значимое имя, указав соответствующий флаг в
командной строке:
g++ hello42.cpp -о hello42
Можно также воспользоваться инструментом make (рассматривается в разде­
ле 7.2.2.1), который (в своих последних версиях) предоставляет правила построе­
ния бинарных файлов по умолчанию. Так что мы можем просто вызвать его ко­
мандой

make hello42

После этого инструмент make выполнит поиск в текущем каталоге исходного
файла программы с данным именем. Он найдет he Но 4 2. срр, а так как . срр яв­
ляется стандартным расширением файлов с исходными текстами C++, будет вы­
зван системный компилятор C++ по умолчанию. После компиляции программы
мы можем вызвать ее из командной строки как
./hello42

Наш бинарный файл может выполняться без привлечения какого-либо ино­
го программного обеспечения и может быть скопирован в любую совместимую
Linux-систему1 и выполнен в ней.
Windows. Если вы работаете с MinGW, можете компилировать программу точ­
но так же, как и в Linux. Если вы используете Visual Studio, то сначала создайте
проект2. Для начинающего простейший путь заключается в использовании шаб­
лона проекта для консольного приложения, как описано, например, по адресу
http://www.cplusplus.com/doc/tutorial/introduction/visualstudio.
При запуске такой программы у вас будет буквально несколько миллисекунд, что­
бы успеть прочесть ее вывод до того, как консольное окно закроется. Чтобы уве­
личить это время до секунды, можно просто добавить непереносимую команду
Sleep (1000) ; и включить заголовочный файл . При использова­
нии C++11 или более поздней версии ожидание можно реализовать переносимо
после включения заголовочных файлов и :
std::this_thread::sleep_for(std::chrono::seconds(1));

Microsoft предлагает бесплатные версии Visual Studio, называемые “Express”,
которые обеспечивают поддержку стандарта языка программирования, как и их
профессиональные аналоги. Разница заключается в наличии у профессиональных
выпусков большего количества библиотек и возможностей. Поскольку в этой кни­
ге они не используются, вы можете смело использовать для компиляции приме­
ров версию “Express”.
1 Зачастую стандартная библиотека компонуется динамически (см. раздел 7.2.1.4), и тогда частью
требований совместимости становится наличие той же ее версии на другой системе.
2 Вообще говоря, это не обязательно, так как Visual Studio, помимо интегрированной среды раз­
работки, предоставляет инструментарий командной строки. — Примеч. ред.

28

Основы C++

Интегрированная среда разработки. Короткие программы наподобие при­
меров из этой книги легко набрать в обычном редакторе. Большие проекты луч­
ше создавать с использованием интегрированных сред разработки (Integrated
Development Environment — IDE), которые позволяют увидеть, где определяется
или используется функция, просмотреть документацию, найти или заменить име­
на во всем проекте и т.д. Такой бесплатной интегрированной средой разработки
является KDevelop от KDE, написанная на C++. Это, вероятно, наиболее эффек­
тивная интегрированная среда разработки в Linux, хорошо интегрированная с
git и CMake. Eclipse разработана на Java и существенно более медленная. Однако
в последнее время было вложено много усилий в ее поддержку C++, так что мно­
гим разработчикам она нравится и они достаточно продуктивно с ней работают.
Visual Studio — это очень солидная интегрированная среда разработки с некото­
рыми уникальными возможностями.
Чтобы найти наиболее продуктивную и подходящую для себя интегрирован­
ную среду разработки, нужно потратить некоторое время на проведение экспери­
ментов; конечно же, огромную роль будут играть ваши личные предпочтения и
вкусы.

1.2. Переменные
C++ является строго типизированным языком программирования (в отличие
от множества языков сценариев). Это означает, что каждая переменная имеет тип,
и этот тип никогда не изменяется. Переменная объявляется с помощью инструк­
ции, начинающейся с типа, за которым следует имя переменной с необязательной
инициализацией (или список таких переменных):
int
int
float
double
double
char
bool

il = 2;
//
12, ±3= 5;
pi = 3.14159;
х = -1.5 еб;
//
у = -1.5е-6;
//
cl = ’а', с2= 35;
аир = il < pi, //
happy = true;

Выравнивание нужно только для удобочитаемости

-1500000
-0.0000015
-> true

Две косые черты // указывают начало однострочного комментария, т.е. все
начиная от этих двух символов и до конца строки компилятором игнорируется.
В принципе, это все, что надо знать о комментариях. Чтобы у вас не было ощу­
щения, что мы пропустили что-то важное по этой теме, рассмотрим их немного
подробнее в разделе 1.9.1.
Вернемся к переменным. Их основные типы — именуемые также встроенными
типами — перечислены в табл. 1.1.

1.2. Переменные

29

Таблица 1.1. Встроенные типы
Имя

Семантика

char

Буквы и очень небольшие целочисленные значения
Короткое целое число
Обычное целое число
Длинное целое число
Очень длинное целое число
Беззнаковая версия перечисленных значений
Знаковая версия перечисленных значений
Число с плавающей точкой одинарной точности
Число с плавающей точкой двойной точности
Длинное число с плавающей точкой
Логическое значение

short

int
long
long long

unsigned
signed

float
double
long double

bool

Первые пять типов представляют собой целые числа неуменыпающейся дли­
ны. Например, int имеет длину, не меньшую, чем длина short, т.е. обычно (но
не обязательно) оно длиннее. Точная длина каждого типа зависит от реализации;
например, тип int может быть длиной 16, 32 или 64 бита. Все эти типы могут
быть знаковыми (signed) или беззнаковыми (unsigned). Первый квалификатор
не оказывает никакого влияния на целые числа (за исключением char), поскольку
они являются знаковыми по умолчанию.
Объявляя целочисленный тип как unsigned, мы не разрешаем ему принимать
отрицательные значения, зато вдвое увеличиваем диапазон допустимых положи­
тельных значений (плюс одно, если нуль рассматривается ни как отрицательное,
ни как положительное число). Слова signed и unsigned можно рассматривать
как прилагательные для существительных short, int и других, где int — сущес­
твительное, применяемое по умолчанию, когда имеются только прилагательные.
Тип char может быть использован двумя способами — для букв и для очень
коротких чисел. За исключением действительно экзотических архитектур, он поч­
ти всегда имеет длину 8 битов. Таким образом, он может представлять значения
либо от -128 до 127 (signed), либо от 0 до 255 (unsigned), и над ним можно
выполнять все числовые операции, доступные для целых чисел. Если не указан
ни квалификатор signed, ни unsigned, то, какой из них используется по умолча­
нию, зависит от реализации. Мы можем также представить любую букву, код ко­
торой помещается в 8 битов. Их можно даже смешивать, например ' а ’ -1-7 обычно
дает ’ h ’ (в зависимости от используемой кодировки букв). Мы настойчиво реко­
мендуем не прибегать к таким способам, поскольку возможная путаница, скорее
всего, приведет к напрасной трате времени.
Применение char или unsigned char для небольших чисел может быть по­
лезным при наличии больших контейнеров с ними.
Логические значения лучше всего представимы с помощью типа bool. Такая
переменная может хранить значение true или false. Свойство неуменыпающей­
ся длины применимо и к числам с плавающей точкой:

Основы C++

30

Тип float короче или той же длины, что и тип double, который, в свою оче­
редь, короче или той же длины, что и тип long double. Типичными размера­
ми являются 32 бита для float, 64 бита — для double и 80 битов — для long
double.
В следующем разделе мы познакомимся с операциями, применимыми к цело­
численным типам и типам с плавающей точкой. В отличие от других языков про­
граммирования, таких как Python, в которых кавычки ’ и ” используются и для
символов, и для строк, C++ их различает. Компилятор C++ рассматривает ’ а ’
как символ “а” (с типом char), а ”а" — как строку, содержащую символ “а” и би­
нарный нуль в качестве завершающего символа (т.е. типом этой строки является
char [2]). Если вы работали ранее с Python, обратите на это особое внимание.
Совет

Объявляйте переменные как можно позже, обычно непосредственно перед их пер­
вым применением, и, насколько это возможно, не ранее чем вы сможете их ини­
циализировать.

Этот совет делает большие программы более удобочитаемыми. Кроме того, это
позволяет компилятору более эффективно использовать память при наличии вло­
женных областей видимости.
C++11 в состоянии вывести тип переменной, например:
auto i4= i3 + 7;

Тип i4 тот же, что и у i3+7, т.е. int. Хотя тип определен автоматически, он
остается неизменным, так что все, что впоследствии будет присвоенопеременной
i4, будет преобразовано в int. Позже мы увидим, насколько в современном про­
граммировании полезно ключевое слово auto. В простых объявлениях перемен­
ных, наподобие приводимых в этом разделе, обычно лучше объявлять тип явно.
Применение auto подробно рассматривается в разделе 3.4.

1.2.1. Константы
Синтаксически константы в C++ представляют собой специальные перемен­
ные с дополнительным атрибутом постоянства в C++.
const
const
const
const
const

int
int
float
char
bool

cil
ci3;
Pi
cc
cmp

= 2;

// Ошибка: не указано значение
= 3.14159;
= ' а ’;
= cil < pi;

Поскольку они не могут быть изменены, обязательным является установка их зна­
чений в объявлении. Второе объявление константы нарушает данное правило, и
компилятор не простит вам такой проступок.
Константы могут использоваться везде, где разрешено применение перемен­
ных, — конечно, до тех пор, пока они не изменяются. С другой стороны, кон-

31

1.2. Переменные

станты, как показанные выше, известны уже во время компиляции. Это позволяет
компилятору применять множество видов оптимизации, так что константы могут
даже использоваться в качестве аргументов типов (мы вернемся к этому позже, в
разделе 5.1.4).

1.2.2. Литералы
Литералы, такие как 2 или 3.14, точно типизированы. Проще говоря, целочисленные значения рассматриваются как имеющие тип int, long или unsigned
long — в зависимости от количества цифр. Любое число с точкой или показате­
лем степени (например, Зе12 = 3-1012) считается значением типа double.
Литералы других типов могут быть записаны путем добавления суффикса из
следующей таблицы.

Литерал

Тип

2

int
unsigned



long
unsigned long

21

2ul
2.0
2. Of

double
float

long double

2.01

В большинстве случаев нет необходимости явно объявлять тип литералов, так
как неявное преобразование между встроенными числовыми типами обычно дает
значения, ожидаемые программистом.
Однако есть три основные причины, по которым необходимо уделять внима­
ние типам литералов.
Доступность. Стандартная библиотека предоставляет тип для комплексных
чисел, в котором типы действительной и мнимой частей могут быть параметризо­
ваны пользователем:
std::complex z (1.3,2.4),

z2;

К сожалению, предоставляются только операции между самим типом комплекс­
ного числа и базовым типом (аргументы в данном случае не преобразуются)3.
В результате мы не можем умножить z на int или double, а только на float:
z2 =
z2 =
z2 =

2
*
2.0 *
2.Of *

z; // Ошибка: нет int * complex
z; // Ошибка: нет double * complex
z; // Все в порядке, float * complex

Неоднозначность. При перегрузке функции для других типов аргументов (раз­
дел 1.5.4) аргумент наподобие 0 может оказаться неоднозначным, в то время как
для квалифицированного аргумента наподобие Ou может иметься единственное
соответствие.
3 Но такая смешанная арифметика может быть реализована, как показано в [18].

Основы C++

32

Точность. Вопрос точности встает, когда мы работаем с типом long double.
Поскольку неквалифицированный литерал имеет тип double, возможна потеря
значащих цифр до присваивания переменной типа long double:
long double
thirdl = 0.3333333333333333333,
// Возможна потеря точности
third2 = 0.33333333333333333331; // Точно

Если вам кажется, что в предыдущих трех абзацах мы были слишком кратки­
ми, примите к сведению, что более подробное изложение представлено в разде­
ле А.2.1.
Не десятичные числа. Целочисленные литералы, начинающиеся с нуля, трак­
туются как восьмеричные числа, например:
int ol= 042;
int о2= 084;

// int ol= 34;
// Ошибка: ни 8, ни 9 не являются восьмеричными цифрами!

Шестнадцатеричные литералы записываются с использованием префикса Ох
или ОХ:
int hl= 0x42; // int hl= 66;
int h2= Oxfa; // int h2= 250;

В C++14 появились бинарные литералы, которые записываются с использова­
нием префикса 0Ь или 0В:
int Ы= 0Ы1111010; // int Ь1= 250;

Для повышения удобочитаемости длинных литералов C++14 допускает разде­
ление цифр апострофами:
long
unsigned long
int
const long double

d = 6’546'687’616’861’1291;
ulx = 0x139’аеЗЬ’2ab0’94f3;
b = OblOl’1001’0011’1010’1101’1010’0001;
pi = 3.141’592’653’589’793’238’4621;

Строковые литералы имеют тип массива символов char:
char sl[]= ’’Старый стиль С’’; // Лучше так не делать

Однако куда удобнее работать со строками типа string из библиотеки
. Такая строка может быть легко создана из строкового литерала:
#include
std::string s2= ”B C++ лучше делать так”;

Очень длинный текст можно разбить на несколько подстрок:
std::string s3= ’’Это очень длинный текст, который”
’’ не помещается в одну строку";

Более подробную информацию о литералах можно найти, например, в [43, §6.2].

33

1.2. Переменные

1.2.3. Не сужающая инициализация в С++11
Представим, что мы инициализируем переменную типа long длинным числом:
long 12= 1234567890123;

Этот код корректно компилируется и работает, если переменная типа long за­
нимает 64 бита, как это происходит на большинстве 64-битовых платформ. Если
тип long имеет длину всего 32 бита (этого можно добиться путем компиляции с
использованием флага наподобие -m32), то показанное выше значение оказыва­
ется слишком большим. Однако программа продолжает компилироваться (может
быть, и с предупреждениями) и выполняться, но с другим значением, в котором
отсечены ведущие биты.
В C++11 введена инициализация, обеспечивающая отсутствие потери данных,
или, иными словами, не допускающая сужения значений. Это достигается с помо­
щью унифицированной инициализации (uniform initialization) или инициализации с
фигурными скобками (braced initialization), которую мы только бегло затрагиваем
здесь, а более подробно будем изучать в разделе 2.3.4. Значения в фигурных скоб­
ках не могут быть сужены:
long 1= { 1234567890123 );

Теперь компилятор будет проверять, может ли переменная 1 хранить указан­
ное значение в целевой архитектуре.
Защита компилятора от сужения позволяет нам убедиться, что значения не
потеряют точности при инициализации. Обычная инициализация int числом с
плавающей точкой разрешается в силу выполнения неявного преобразования:
int il = 3.14;
int iln = {3.14);

// Компилируется, несмотря на сужение (на ваш риск)
// Ошибка сужения: теряется дробная часть

Новая разновидность инициализации во второй строке запрещает его, по­
скольку при этом будет отсечена дробная часть числа с плавающей точкой. Ана­
логично присваивание отрицательных значений беззнаковым переменным или
константам пропускается традиционной инициализацией, но отклоняется новой
разновидностью:
unsigned u2 = -3;
unsigned u2n={-3);

// Компилируется, несмотря на сужение (на ваш риск)
// Ошибка сужения: отрицательное значение

В предыдущих примерах мы использовали в инициализации литералы, и ком­
пилятор проверял, представимо ли конкретное значение указанным типом:
float fl= { 3.14 ); //OK

Значение 3.14 не может быть представлено с абсолютной точностью в двоич­
ном формате с плавающей точкой, но компилятор может установить значение
fl близким к 3.14. Когда float инициализируется переменной или константой
double (не литералом), необходимо рассмотреть все возможные значения double
и то, являются ли они преобразуемыми в тип float без потерь.

Основы С++

34
double d;
float f2= {d}; // Ошибка сужения

Обратите внимание, что сужение между двумя типами может быть взаимным:
unsigned
int
unsigned
int

u3=
i2=
u4=
i3=

{3};
{2};
{i2};
{u3};

// Ошибка сужения:
// Ошибка сужения:

возможно отрицательноезначение
возможно слишком большое значение

Типы signed int и unsigned int имеют одинаковый размер, но не все значе­
ния каждого типа представимы другим типом.

1.2.4. Области видимости
Области видимости определяют время жизни и видимость (не статических)
переменных и констант и способствуют установлению структуры в наших про­
граммах.
1.2.4.1. Глобальные переменные
Каждая переменная, которую мы намерены использовать в программе, должна
быть объявлена с указанием ее типа в некоторой более ранней точке кода. Пере­
менная может находиться в глобальной или локальной области видимости. Гло­
бальная переменная объявляется вне всех функций. После объявления обращать­
ся к глобальным переменным можно в любом месте кода, даже внутри функций.
Это выглядит очень удобным, в первую очередь, потому, что делает переменные
легко доступными, но с ростом размера программы отслеживать изменения гло­
бальных переменных становится более трудно и болезненно. В некоторый момент
каждое изменение кода потенциально способно вызвать лавину ошибок.

Совет

Не используйте глобальные переменные.

Если вы используете их, рано или поздно вы об этом пожалеете. Поверьте нам.
Глобальные константы вроде
const double pi= 3.14159265358979323846264338327950288419716939;

можно применять, поскольку они не вызывают побочных действий.
1.2.4.2. Локальные переменные

Локальная переменная объявляется внутри тела функции. Ее видимость/доступность ограничивается заключенным в фигурные скобки {} блоком, в кото­
ром она объявлена. Точнее, область видимости переменной начинается с ее объяв­
ления и заканчивается закрывающей фигурной скобкой блока, в котором она
объявлена.

1.2.

35

Переменные

Если мы определяем pi в функции main:
int main ()
{
const double pi= 3.14159265358979323846264338327950288419716939;
std::cout « "pi = " « pi « ”.\n";
}

то переменная pi существует только в функции main. Мы можем определять бло­
ки внутри функций и других блоков:
int main

()

{

{
const double pi= 3.14159265358979323846264338327950288419716939;

}
II Ошибка: pi вне области видимости:
std::cout « "pi = " « pi « ”.\n";
}

В этом примере определение pi ограничено блоком внутри функции, так что
попытка вывода этого значения в оставшейся части функции приводит к ошибке
времени компиляции, поскольку pi находится вне области видимости.

1.2.4.3. Сокрытие
Когда во вложенных областях видимости имеется переменная с тем же именем,
видна только одна переменная. Переменная во внутренней области видимости
скрывает одноименные переменные во внешних областях. Например:
int main

()

{

int а= 5;

// Определение а №1

{

а = 3;
int а;
а = 8;

// Присваивание а №1, а №2 еще не определена
// Определение а №2
// Присваивание а №2, а №1 скрыта

{

а = 7; // Присваивание а №2
}

}

а = 11;

// Конец области видимости а №2
// Присваивание а №1 (а №2 вне области видимости)

return 0;
}

Из-за сокрытия мы должны различать время жизни и видимость переменных.
Например, продолжительность жизни переменной а №1 — от ее объявления до
конца функции main. Однако она видима только от ее объявления до объявле­
ния а №2 и вновь после закрытия блока, содержащего переменную а №2. Факти­
чески видимость представляет собой время жизни минус время, когда перемен­
ная скрыта.

36

Основы C++

Определение одного и того же имени переменной дважды в одной области ви­
димости является ошибкой.
Преимуществом областей видимости является то, что нам не нужно беспоко­
иться о том, не определено ли имя где-то за пределами области видимости. Оно
будет просто скрыто, и не создаст конфликт имен4. К сожалению, сокрытие делает
недоступными одноименные переменные из внешней области видимости. В неко­
торой степени справиться с этим можно с помощью разумного переименования.
Лучшим решением для управления вложенностью и доступностью являются про­
странства имен (см. раздел 3.2.1).
Статические (static) переменные являются исключением, которое подтверж­
дает правило. Они живут до конца выполнения программы, но видны только в
своей области видимости. Опасаясь, что их подробное описание на этом этапе
знакомства с языком программирования будет более отвлекающим, чем полез­
ным, мы отложили их обсуждение до раздела А.2.2.

1.3. Операторы
C++ имеет множество встроенных операторов. Имеются различные виды опе­
раторов.

• Вычислительные
- Арифметические. ++, +,*,%,...

- Булевы
*

Сравнения. , [ ],*,...


Работы с типами. dynamic_cast, typeid, sizeof, alignof, ...



Обработки ошибок, throw

В этом разделе приведен обзор операторов. Некоторые операторы лучше опи­
сывать в контексте соответствующих возможностей языка; например, разреше­
ние области видимости лучше всего объяснять вместе с пространствами имен.
4 В противоположность макросам, устаревшей и безответственной возможности, унаследованной
от С, которой следует избегать любой ценой, поскольку она подрывает всю структуру и надежность
языка.

1.3. Операторы

37

Большинство операторов могут быть перегружены для пользовательских типов,
т.е. мы можем решать, какие вычисления выполняются, когда один или несколько
аргументов в выражении имеют созданные нами типы.
В конце этого раздела вы найдете краткую таблицу (табл. 1.8) приоритетов
операторов. Может оказаться полезным распечатать ее и хранить рядом с монито­
ром — так поступают многие программисты, и почти никто не знает весь список
приоритетов наизусть. Если вы не уверены в приоритетах или если вы считаете,
что так код будет более понятен программистам, работающим с вашими исходны­
ми текстами, без колебаний используйте скобки вокруг подвыражений. В разделе
В.2 имеется полный список всех операторов с краткими описаниями и ссылками.

1.3.1. Арифметические операторы
В табл. 1.2 перечислены все арифметические операторы, доступные в C++. Мы рассортировали их согласно приоритетам, но давайте рассмотрим их по одному.
Таблица 1.2. Арифметические операторы
Операция

Выражение

Пост-инкремент

х++

Пост-декремент

X—

Пре-инкремент

++х

Пре-декремент

—X

Унарный плюс



Унарный минус



Умножение

X * у

Деление

х / у

Остаток от деления (деление по модулю)

X % у

Сложение

X+у

Вычитание

X - у

Первой разновидностью операций являются инкремент и декремент. Эти опе­
рации могут использоваться для увеличения или уменьшения числа на 1. Так как
они изменяют значение числа, они имеют смысл только для переменных, но не
для временных результатов, например:
int i = 3;
i++;

// Теперь i равно 4

const int j= 5;
j++;
// Ошибка: j является константой
(3+5)++;
// Ошибка: 3+5 является временным результатом

Короче говоря, операциям инкремента и декремента нужно что-то, что изме­
няемо и адресуемо. Техническим термином для адресуемого элемента данных
является lvalue (см. определение В.1 в приложении В). В приведенном выше

Основы C++

38

фрагменте кода это верно только для переменной i. В противоположность ему j
является константой, а значение 3+5 не адресуемо.
Обе записи — префиксная и постфиксная — одинаково добавляют 1 к значе­
нию переменной или вычитают 1 из него. Однако смысл выражения инкремента
и декремента различается для префиксных и постфиксных операторов. Префик­
сные операторы возвращают измененное значение, а постфиксные — старое зна­
чение, например:
int i = 3, j= 3;
int k = ++i +4;
int 1 = j++ +4;

// i = 4, k = 8
// j = 4, 1=7

В конечном итоге и i, и j равны 4. Однако при вычислении 1 используется ста­
рое значение j, в то время как в первом сложении используется уже увеличенное
значение i.
В общем случае лучше воздерживаться от использования инкремента и декре­
мента в математических выражениях и заменять их выражениями j+1 и тому по­
добными или выполнять инкремент и декремент отдельно. Так исходный текст
легче читается и понимается человеком, а компилятору легче оптимизировать
код, когда математические выражения не имеют побочных эффектов. Вскоре мы
увидим, с чем это связано (раздел 1.3.12).
Унарный минус изменяет знак числового значения:
int i= 3;
int j= -i; // j = -3

Унарный плюс не выполняет никакого арифметического действия над стандарт­
ными типами. Для пользовательских типов мы можем определить свое поведение
для унарного плюса и минуса. Как показано в табл. 1.2, эти унарные операторы
имеют приоритет, совпадающий с приоритетом префиксных инкремента и декре­
мента.
Операции * и / являются естественными умножением и делением, и обе они
определяются для всех числовых типов. Когда оба аргумента деления являются
целыми числами, дробная часть результата отбрасывается (округление по направ­
лению к нулю). Оператор % возвращает остаток от целочисленного деления. Та­
ким образом, оба аргумента этого оператора должны иметь целочисленный тип.
Последними по очереди, но не по важности идут операторы + и -, которые
обозначают сложение и вычитание двух переменных или выражений.
Семантические сведения об операциях — как округляются результаты или как
обрабатывается переполнение — в языке не определены. По соображениям про­
изводительности C++ оставляет окончательное решение за используемым аппа­
ратным обеспечением.
В общем случае унарные операторы имеют более высокий приоритет, чем би­
нарные. В тех редких случаях, когда применяются и префиксный, и постфиксный
унарные операторы, префиксный оператор имеет более высокий приоритет, чем
постфиксный.

39

1.3. Операторы

Бинарные операторы ведут себя так же, как и в математике. Умножение и де­
ление имеют больший приоритет, чем сложение и вычитание, а сами операции
являются левоассоциативными, т.е.
X - у + Z

всегда трактуется как
(х - у) + Z

Есть кое-что, что действительно важно запомнить: порядок вычисления аргу­
ментов не определен. Взгляните на этот код:
int i= 3, j= 7, k;

k= f(++i) + g(++i) + j;

В этом примере ассоциативность гарантирует, что первое сложение выполняет­
ся до второго. Но какое выражение вычисляется первым — f (++i) или д (++i), —
зависит от реализации компилятора. Таким образом, к может принимать любое
значение — f (4) +g (5) +7, f (5) +g (4) +7 или даже f (5) +g (5) +7. Кроме того, не­
льзя полагать, что результат будет тем же самым на другой платформе. В общем
случае изменять значения в выражениях — опасная практика. При определенных
условиях это работает, но мы всегда должны обращать особое внимание на такой
код и тщательно его тестировать. Словом, лучше потратить время на ввод допол­
нительных символов и выполнять изменения отдельно. Подробнее об этом мы
поговорим в разделе 1.3.12.
=> c++03/num_l. срр

С помощью этих операторов мы можем написать нашу первую (завершенную)
числовую программу:
tfinclude
int main ()
{
const float rl=

3.5, r2 = 7.3, pi = 3.14159;

float areal = pi*rl*rl;
std::cout « "Круг с радиусом ” « rl « ” имеет площадь ”
« areal «
« std::endl;

std::cout « ’’Среднее ” « rl « ” и ” « r2 « ” равно ’’
« (rl + r2)/2 «
« std::endl;
}

Если аргументы бинарной операции имеют различные типы, один или не­
сколько аргументов автоматически приводятся к общему типу в соответствии с
правилами из раздела В.З.
Преобразование может приводить к потере точности. Числа с плавающей точ­
кой предпочтительнее целых чисел, и очевидно, что преобразование 64-разрядного
long в 32-разрядный float приводит к потере точности; даже 32-разрядные значе­
ния типа int не всегда могут быть представлены правильно в виде 32-разрядных

Основы C++

40

значений типа float, так как некоторые биты нужны для представления показа­
теля степени. Бывают также ситуации, когда целевая переменная может хранить
правильный результат, но точность уже потеряна при промежуточных расчетах.
Чтобы проиллюстрировать это поведение преобразования, давайте рассмотрим
следующий пример:
long 1= 1234567890123;
long 12= 1 + 1.Of - 1.0;
// Неточно
long 13= 1 + (1.Of - 1.0); // Верно

На платформе автора это приводит к следующему результату:
12 = 1234567954431
13 = 1234567890123

В случае 12 мы теряем точность из-за промежуточных преобразований, в то
время как 13 вычисляется правильно. Этот пример, правда, носит искусственный
характер, но вы должны быть осведомлены о риске, связанном с неточными про­
межуточными результатами.
К счастью, вопрос неточности не будет беспокоить нас в следующем разделе.

1.3.2. Булевы операторы
Булевы операторы включают логические операторы и операторы сравнения.
Все они, как и предполагается в названии, возвращают значения типа bool. Эти
операторы и их смысл перечислены в табл. 1.3 (сгруппированы в соответствии с
приоритетом).
Таблица 1.3. Булевы операторы

Операция

Выражение

Нет



Больше
Больше или равно
Меньше

х > у

Меньше или равно
Равно
Не равно

X = у
X <

у

X == у

Бинарные операторы сравнения и логические операторы имеют приоритеты,
меньшие, чем приоритеты всех арифметических операторов. Это означает, что
выражение наподобие 4 >=1+7 вычисляется так, как если бы оно было записано
как 4>= (1+7). И наоборот, унарный оператор ! для логического отрицания имеет
более высокий приоритет, чем приоритет любого бинарного оператора.

41

1.3. Операторы

В старом (или старомодном) коде вы можете увидеть логические операции, вы­
полняемые над значениями типа int. Воздержитесь от этого в своем коде — это
менее удобочитаемо и может вести к неожиданному поведению программы.
Совет

Для булевых выражений всегда используйте тип bool.

Пожалуйста, обратите внимание, что сравнения нельзя выстраивать цепочка­
ми наподобие следующей:
bool in_bound = min = 0.0

(постусловие)

Ветви инструкции if являются областями видимости, что делает следующие
инструкции неверными:

Основы C++

50
(х < 0.0)
int absx
else
int absx
cout « ”|x|

if

= -x;
= x;

« absx « ”\n"; // absx уже за областью видимости

Выше мы ввели две новые переменные, обе имеющие имя absx. Они не кон­
фликтуют, поскольку находятся в разных областях видимости. Ни одна из них не
существует после инструкции if, и обращение к absx в последней строке являет­
ся ошибкой. На самом деле переменные, объявленные в ветвях, могут использо­
ваться только внутри этих ветвей.
Каждая ветвь if состоит из одной инструкции. Для выполнения нескольких
операций мы можем использовать фигурные скобки, как в приведенной реализа­
ции метода Кардано:
double D= q*q /4.0 4- р*р*р /27.0;
if (D > 0.0) {
double zl= ...;
complex z2 = . .., z3 = ...;

} else if (D == 0.0)
double zl = ...,

{
z2 = ...,

z3 = ...;

} else { // D < 0.0
complex zl = ..., z2 = ...,

z3 = ...;

}

Всегда полезно вначале написать фигурные скобки. Многие руководства по
стилю программирования требуют применять фигурные скобки даже для одной
инструкции, тогда как автор предпочитает в этом случае обходиться без них.
В любом случае настоятельно рекомендуется использовать отступ ветви для луч­
шей удобочитаемости.
Инструкции if могут быть вложенными; при этом каждое else связано с по­
следним открытым if. Если вас интересуют конкретные примеры, обратитесь к
разделу А.2.3. И вот еще один совет.
Совет

Хотя пробелы никак не влияют на компиляцию программ C++, отступы должны
отражать структуру программы. Редакторы, понимающие C++ (наподобие интег­
рированной среды разработки Visual Studio или редактора emacs в режиме C++) и
автоматически добавляющие отступы, очень облегчают структурное программи­
рование. Всякий раз, когда строка имеет не тот отступ, который ожидается, скорее
всего, имеет место не та вложенность, которая предполагалась.

1.4. Выражения и инструкции

51

1.4.3.2. Условное выражение
Хотя в этом разделе описываются инструкции, мы бы хотели поговорить здесь
об условном выражении из-за его близости к инструкции if. Результатом выпол­
нения
condition ? result_for_true : result_for_false

является второе подвыражение (т.е. result_for_true), если вычисление
condition дает true, и result_for_false — в противном случае. Например,
min = х :
р->г =3.5;

// Выглядит куда лучше ;-)

Даже ранее упомянутое косвенное обращение больше не вызывает особых
проблем:
p->pm->m2 =11; // Просто и понятно

В C++ мы можем определить указатели на члены, которые, вероятно, приго­
дятся не всем читателям (автор до сегодняшнего дня ни разу не использовал их,
кроме как при написании этой книги). Если вы хотите посмотреть на пример их
применения, обратитесь к разделу А.4.1.

2.2.4. Декларатор static в классах
Переменные-члены, которые объявляются как статические (static), существу­
ют в единственном экземпляре (принадлежащем классу, а не конкретному объек­
ту класса), что позволяет совместно использовать этот ресурс всеми объектами
класса. Еще один вариант его использования — для создания синглтона, шаблона
разработки, гарантирующего, что существует только один экземпляр определен­
ного класса [14, с. 127-136].
Таким образом, данные-член, объявленный как static и const, существует в
единственном экземпляре и не может быть изменен. Как следствие он доступен во
время компиляции. Мы воспользуемся этим для метапрограммирования в главе 5,
“Метапрограммирование”.
Методы также могут быть объявлены как static. Это означает, что они могут
обращаться только к статическим данным и вызывать статические функции. Это
может обеспечить дополнительную оптимизацию, если методу не требуется обра­
щение к данным объекта.
Наши примеры используют статические данные-члены только в их констант­
ной форме и не используют статические методы. Однако последние появляются в
стандартных библиотеках в главе 4, “Библиотеки”.

2.2.5. Функции-члены
Функции в классах называются функциями-членами или методами. Типичны­
ми функциями-членами в объектно-ориентированном программировании явля­
ются функции чтения и установки данных-членов.
Листинг 2.1. Класс с функциями чтения и установки
class complex
{
public:
double get__r() { return r; }
void
set_r(double newr) { r = newr;

}

109

2.2. Члены
double get_i() { return i; }
void
set_i (double newi) { i = newi;
private:
double r, i;
};

}

Методы, как и любые члены класса, по умолчанию являются закрытыми,
т.е. могут вызываться только из функций в самом классе. Очевидно, что такие
функции чтения и установки не будут особенно полезными. Таким образом, обыч­
но они объявляются как public. Теперь мы можем писать с. get r (), а не с. г.
Показанный выше класс может использоваться следующим образом.
Листинг 2.2. Использование функций чтения и установки
int main ()
{

complex cl, c2;

// Установка cl
cl.set_r(3.0);
cl.set_i(2.0);

11 Неуклюжая инициализация

// Копирование cl в c2
c2.set_r (cl.get_r()); // Неуклюжее копирование

c2.set_i (cl.get_i());
return 0;
}

В начале нашей функции main мы создаем два объекта типа complex. Затем
мы устанавливаем значение одного из объектов и копируем его в другой. Это ра­
ботает, но выглядит несколько неуклюже, не правда ли?
Наши переменные-члены могут быть доступны только через функции. Это
дает разработчику класса максимальный контроль над его поведением. Например,
мы могли бы ограничить диапазон значений, которые принимаются функциями
установки. Мы могли бы подсчитывать для каждого комплексного числа, как час­
то оно считывается или записывается во время выполнения. Данные функции мо­
гут давать дополнительный отладочный вывод (правда, отладчик обычно являет­
ся лучшей альтернативой, чем отладочный вывод). Мы могли бы даже позволить
чтение значений данных-членов только в определенное время дня или записывать
их только тогда, когда программа запускается на компьютере с определенным IP.
Скорее всего, это никому не потребуется, по крайней мере не для комплексных
чисел, но это возможно. Если переменные открытые и доступны непосредственно,
такое поведение будет невозможным. Тем не менее такая работа с действительной
и мнимой частями комплексного числа достаточно громоздка, так что мы рас­
смотрим альтернативы получше.

по

Классы

Большинство программистов C++ не будут реализовывать комплексные числа
таким образом. Но что же программист на C++ сделает в первую очередь? Напи­
шет конструктор (или конструкторы).

2.3. Установка значений. Конструкторы и присваивания
Конструкторы и присваивания представляют собой два механизма для задания
значения объекта либо в момент его создания, либо позже для уже созданного
объекта. Таким образом, эти два механизма имеют много общего, и потому введе­
ны здесь вместе.

2.3.1. Конструкторы
Конструкторы — это методы, которые инициализируют объекты классов и со­
здают рабочую среду для функций-членов. Иногда такая среда включает ресурсы,
такие как файлы, память или блокировки, которые должны быть освобождены
после их использования. Мы возвратимся к этому позже.
Наш первый конструктор создает действительные и мнимые значения нашего
объекта типа complex:
class complex
{
public:

complex (double mew, double inew)
{

r = rnew; i = inew;
}

// ...
};

Конструктор — это функция-член с тем же именем, что и имя самого класса.
Он может иметь произвольное количество аргументов. Этот конструктор позволяет
нам установить значения комплексного числа cl непосредственно в определении:
complex cl (2.0z

3.0);

Имеется специальный синтаксис для установки переменных-членов и констант
в конструкторах, который называется списком инициализации членов или, для
краткости, просто списком инициализации:
class complex

{
public:
complex (double rnew, double inew)

:

r (mew) ,i (inew) {}

// ...
};

Список инициализации начинается с двоеточия после заголовка функции
конструктора. Это, в принципе, просто непустой список конструкторов, которые

2.3. Установка значений. Конструкторы и присваивания

111

вызываются для переменных-членов (и базовых классов), или его подмножество.
Некоторые компиляторы выдают предупреждения, когда порядок инициализации
членов не соответствует порядку их определения или когда не все члены пере­
числены в списке инициализации. Компилятор хочет удостовериться, что ини­
циализируются все переменные-члены, потому что для всех тех членов, которые
мы не инициализировали, он добавляет вызов конструкторов без аргументов. Та­
кой конструктор без аргументов называется конструктором по умолчанию (более
подробно рассматривается в разделе 2.3.1.1). Таким образом, наш первый пример
конструктора эквивалентен коду
class complex
{
public:
complex(double rnew, double inew)
: r(), i()
// Генерируется компилятором
{

г = rnew; i = inew;
}

};

Для простых арифметических типов, таких как int и double, не важно, где
мы задаем их значения: в списке инициализации или в теле конструктора. Дан­
ные-члены встроенных типов, которые не появляются в списке инициализации
членов, остаются неинициализированными. Для данных-члена типа класса, ко­
торый отсутствует в списке инициализации, неявно вызывается конструктор по
умолчанию.
Как инициализируются члены, становится более важным, когда сами члены
являются классами. Представьте себе, что мы написали класс, который решает
систему линейных уравнений с заданной матрицей, которая хранится в нашем
классе:
class solver
{
public:
solver(int nrows,

// :A()

int ncols)

№1. Вызов несуществующего конструктора no умолчанию

{

A(nrows, ncols); // №2. Вызов не конструктора
}

И ...
private:
matrix_type А;
};

Предположим, что наш класс матрицы имеет конструктор, устанавливающий
два ее измерения. Этот конструктор не может вызываться в теле функции кон­
структора (строка №2). Выражение в №2 интерпретируется не как конструктор, а
как вызов функции A.operator () (nrows, ncols) (см. раздел 3.8).

Классы

112

Как и все переменные-члены, которые строятся до того, как будет достигнуто
тело конструктора, наша матрица А будет построена по умолчанию (строка №1).
К сожалению, тип matrix_type не имеет конструктора по умолчанию, что приво­
дит к сообщению об ошибке, которое имеет приблизительно такой вид:
Operator "matrix-type: :rnatrix_typeO" not found.
т.е. указывает на отсутствие конструктора по умолчанию. Таким образом, для пра­
вильного вызова конструктора матрицы мы должны написать код
class solver
{
public:
solver(int nrows, int ncols)
// ...

: A(nrows, ncols) {}

};

В предыдущих примерах matrix_type является частью solver. Но более
вероятным сценарием представляется существование матрицы, когда создается
объект solver. В таком случае хотелось бы не тратить память на копирование
этой матрицы, а просто ссылаться на нее. Теперь наш класс содержит в качестве
члена ссылку, и мы вновь вынуждены задавать значение ссылки в списке инициа­
лизации (поскольку ссылки нельзя конструировать по умолчанию):
class solver
{
public:
solver(const matrix_type & A)
// ...
private:
const matrix_type & A;

: A(A) {}

};

Этот код также показывает, что мы можем давать аргументам конструктора
такие же имена, как имена переменных-членов. При этом встает вопрос, какие
имена имеются в виду, в нашем случае — имя А в разных местах? Правило состо­
ит в том, что имена в списке инициализации вне скобок всегда относятся к чле­
нам. Внутри скобок имена следуют правилам области видимости функции-члена.
Имена, локальные по отношению к функции-члену (включая имена аргументов),
скрывают имена в классе. То же самое относится и к телу конструктора: имена ло­
кальных переменных и аргументов скрывают имена класса. Вначале это сбивает с
толку, но вы привыкнете к этому быстрее, чем думаете.
Но вернемся к нашему примеру с классом complex. Пока что у нас есть конс­
труктор, который позволяет установить действительные и мнимые части при со­
здании объекта. Часто устанавливается только действительная часть, а мнимая по
умолчанию принимается равной 0.
class complex
{
public:

Установка значений. Конструкторы и присваивания

2.3.

complex(double г, double
complex(double г) : r (r),
// ...

113

i) : г(г), i(i) {}
i

(0)

{}

};

Можно также считать, что если в конструктор не передано никакое значение,
то получающееся комплексное число равно 0 4- 0/, т.е. конструктор по умолчанию
выглядит следующим образом:
complex ()

: r(0), i(0)

{}

Подробнее конструктор по умолчанию мы рассмотрим в следующем разделе.
Все эти три различных конструктора можно объединить в один с помощью
аргументов по умолчанию:
class complex
{
public:
complex(double r = 0, double i = 0)
// ...

: r(r), i(i) {}

};

Такой конструктор позволяет нам использовать разные формы инициализации:
complex zl,
z2 () ,
z3(4) ,
z4 = 4,

//По умолчанию
//По умолчанию ????????
// Сокращенная запись z3(4.0,
// Сокращенная запись z4(4.0,

0.0)
0.0)

z5(0,l);

Определение z2 — ловушка! Оно выглядит как вызов конструктора по умол­
чанию, но на самом деле таковым не является. Вместо этого данная запись ин­
терпретируется как объявление функции с именем z2, которая не принимает
аргументов и возвращает значение типа complex. Скотт Мейерс (Scott Meyers) на­
звал эту интерпретацию наиболее раздражающим синтаксическим анализом (most
vexing parse). Конструирование объекта с одним аргументом может быть записано
как присваивание, с использованием символа =, как это сделано для z4. В старых
книгах вы можете иногда прочесть, что это приводит к накладным расходам, по­
скольку сначала создается временный объект, а затем выполняется копирование.
Это не так. Может быть, что-то похожее и было в самые ранние дни C++, но ны­
нешний компилятор такими глупостями заниматься не станет.
C++ знает три специальных конструктора:

• уже упоминавшийся конструктор по умолчанию;


копирующий конструктор;



перемещающий конструктор (в С++11 и старше; раздел 2.3.5.1).

В следующих разделах мы познакомимся с ними поближе.

Классы

114
2.3.1.1. Конструктор по умолчанию

Конструктор по умолчанию — не более чем конструктор без аргументов, или
конструктор, у которого каждый аргумент имеет значение по умолчанию. Класс
не обязан иметь конструктор по умолчанию.
На первый взгляд, многим классам не требуется конструктор по умолчанию. Од­
нако в реальной жизни гораздо проще его иметь. Что касается класса complex, то
кажется, что мы могли бы прожить и без конструктора по умолчанию, так как мы
можем задержать его объявление до тех пор, пока не будем знать значение объекта.
Но отсутствие конструктора по умолчанию создает (по крайней мере) две проблемы.
• Переменные, которые инициализируются во внутренней области видимос­
ти, но по алгоритмическим причинам располагаются во внешней области
видимости, должны уже быть созданы без значащего значения. В этом слу­
чае более логично объявить переменную с помощью конструктора по умол­
чанию.
• Наиболее важной причиной является сложность (хотя и не невозможность)
реализации контейнеров — таких, как списки, деревья, векторы и матри­
цы — для элементов, у которых нет конструкторов по умолчанию.

Короче говоря, жить можно и без конструктора по умолчанию, но рано или
поздно такая жизнь становится нелегкой.

Совет

По возможности определяйте конструктор по умолчанию.
Однако для некоторых классов очень трудно определить конструктор по умол­
чанию, например, когда некоторые из членов являются ссылками или содержат
их. В таких случаях может быть предпочтительнее принять упомянутые выше не­
достатки вместо того, чтобы создавать плохо спроектированные конструкторы по
умолчанию.

2.3.1.2. Копирующий конструктор
В функции main нашего примера функций получения и установки значений
(листинг 2.2) мы определили два объекта, один из которых являлся копией друго­
го. Операция копирования была реализована путем чтения и записи каждой пере­
менной-члена в приложении. Однако для копирования объектов лучше использо­
вать копирующий конструктор:
class complex

{
public:
complex (const
// ...

coonplex& c) : i(c.i), r(c.r) {}

2.3. Установка значений. Конструкторы и присваивания

115

int main ()
{

complex zl(3.0, 2.0),
z2(zl); // Копирование
z3{zl}; // С ++11: не сужающее копирование
}

Если пользователь не напишет копирующий конструктор, компилятор сгенери­
рует стандартный копирующий конструктор, который вызывает копирующие
конструкторы всех членов (и базовых классов) в порядке их определения, так же,
как мы сами сделали в нашем примере.
В таких случаях, как рассмотренный выше, когда копирование всех членов —
это именно то, чего мы хотим от нашего копирующего конструктора, мы должны
использовать копирующий конструктор по умолчанию по следующим причинам:



он менее многословен;



он менее подвержен ошибкам;

• другие программисты понимают, как выполняется копирование, даже не
читая наш исходный текст;
• компилятор может лучше его оптимизировать.

В общем случае не рекомендуется использовать изменяемую ссылку в качестве
аргумента:
complex

(complex^ с) : i(c.i), г(с.г) {}

В таком случае копирующий конструктор может копировать только изменяе­
мые объекты. Однако могут быть ситуации, когда необходимо именно такое пове­
дение копирующего конструктора.
Аргумент копирующего конструктора не может быть передан по значению:
complex(complex с)

// Ошибка!

Попробуйте немного подумать и объяснить, почему это так. Если вы не разбе­
ретесь в этом самостоятельно, то сможете прочесть ответ в конце этого раздела.
■=> c++03/vector_test .срр

Бывают случаи, когда копирующий конструктор по умолчанию не работает,
особенно если класс содержит указатели. Пусть, например, у нас есть простой
класс вектора с копирующим конструктором:
class vector
{
public:
vector(const vectors v)
: my_size (v.my_size), data (new double [my_size])
{
for(unsigned i = 0; i < my_size; ++i)
data[i] = v.data[i];

Классы

116
// Деструктор, см. раздел 2.4.2
^vector () { delete[] data; }
// ...
private:
unsigned my_size;
double
*data;
};

Если мы опустим этот копирующий конструктор, компилятор не будет жало­
ваться на жизнь и молча сгенерирует его для нас. Да, наша программа стала коро­
че и привлекательнее, но рано или поздно мы столкнемся с ее странным поведе­
нием — изменение одного вектора приведет к изменению содержимого другого.
Оказавшись в такой ситуации, мы должны найти ошибки в нашей программе.
Это будет особенно трудно сделать, потому что ошибка не в том, что мы написа­
ли, а в том, чего не написали.
Дело в том, что мы копируем не сами данные, а только их адрес. Это проил­
люстрировано на рис. 2.1: когда мы скопируем vl в v2 с помощью сгенерирован­
ного конструктора, указатель v2. data будет указывать на те же данные, что и
vl .data.

Рис. 2.1. Сгенерированное копирование вектора
Еще одна проблема, с которой мы при этом столкнемся, — будет предпринята
попытка освободить одну и ту же память дважды1. Для иллюстрации мы добавили
здесь деструктор из раздела 2.4.2, освобождающий память, на которую указывает
член data. Так как оба указателя содержат один и тот же адрес памяти, второй
вызов деструктора завершится ошибкой.
•=> c++ll/vector_unique_ptr.срр

1 С этой ошибкой каждый программист встречался в своей практике по крайней мере однажды
(или он никогда не писал ничего серьезного). Правда, мой друг и корректор Фабио Фракасси (Fabio
Fracassi) оптимистично полагает, что в будущем программисты, использующие современный C++, не
будут сталкиваться с такими проблемами. Поживем — увидим...

23. Установка значений. Конструкторы и присваивания

117

C++11I Поскольку наш vector предполагается единственным владельцем своих
данных, применение unique_ptr выглядит лучшим выбором (в С++11),
чем обычный указатель:
class vector
{
// ...
std: ’.unique_jptr data;

};

В результате не только выполняется автоматическое освобождение памяти, но
и компилятору не удается создать копирующий конструктор автоматически, по­
скольку копирующий конструктор у unique ptr удален. Это заставляет нас пре­
доставлять пользовательскую реализацию копирующего конструктора.
Вернемся к нашему вопросу, почему аргумент копирующего конструктора не
может передаваться по значению. К этому моменту вы уже, определенно, долж­
ны были самостоятельно разобраться в этом вопросе. Для того чтобы передать
аргумент по значению, его нужно скопировать, а для этого нужен копирующий
конструктор, который мы только собираемся определить. Таким образом, мы со­
здаем зависимость копирующего конструктора от самого себя, которая могла бы
привести компилятор к бесконечному циклу. К счастью, компиляторы достаточно
разумны, чтобы не попадаться в такие ловушки, и даже выводят осмысленное со­
общение об ошибке.
2.3.1.3. Преобразование и явные конструкторы

В C++ мы различаем явные и неявные конструкторы. Неявные конструкторы
разрешают неявные преобразования и конструирование объектов с использовани­
ем оператора присваивания. Вместо
complex cl{3.0};
complex cl (3.0);

// С ++11 и выше
// Все стандарты

можно также записать
complex cl =

3.0;

или
complex cl = pi*pi/6.0;

Такая запись более удобочитаема для многих людей с научным образованием,
в то время как современные компиляторы генерируют для обеих записей один и
тот же код.
Неявное преобразование выполняется, когда требуется один тип, а в наличии
имеется другой, например double вместо complex. Предположим, у нас есть фун­
кция2

2 Определения real и imag будут даны немного ниже.

Классы

118
double inline complex_abs(complexc)
{

return std::sqrt(real(c)*real(c)

+ imag(c)*imag(c)) ;

}

и она вызывается с аргументом double, например
cout « "|7| = ” « complex_abs(7.0) « ’\n';

Литерал 7.0 представляет собой double, но перегрузки функции complex abs,
принимающей double, не существует. Однако есть перегрузка для аргумента
complex и конструктор complex, который принимает аргумент double. Так что
значение complex неявно создается из литерала double.
Неявное преобразование можно запретить, объявив конструктор как explicit:
class complex {
public:
explicit complex(double nr = 0.0, double i = 0.0):r(nr),i(i){}

};

В таком случае complex abs не будет вызываться с параметром double. Что­
бы вызвать эту функцию с аргументом double, нам придется написать перегруз­
ку для аргумента double или явно создать complex в вызове функции:
cout « " | 7 | = ’’ « complex_abs (complex{7.0}) « ’ \n ’;

Атрибут explicit действительно важен для некоторых классов, например для
класса vector. У него имеется конструктор, который принимает в качестве аргу­
мента размер вектора:
class vector
{
public:
vector (int n)

: my_size(n), data(new double[my_size])

{}

};

Пусть имеется функция, вычисляющая скалярное произведение и получающая
два вектора в качестве аргументов:
double dot(const vector

& v, const vector & w) { ... }

Эта же функция может быть вызвана с целочисленными аргументами:
double d = dot(8,8);

Что же происходит? С помощью неявного конструктора создаются два времен­
ных вектора размером 8, которые передаются функции dot. Этой бессмыслицы
легко избежать, объявив конструктор как explicit.
Какой конструктор должен быть объявлен как explicit, в конечном итоге яв­
ляется решением его создателя. В случае класса vector все очевидно — ни один
программист в здравом уме не захочет, чтобы компилятор автоматически преоб­
разовывал целые числа в векторы.

2.3. Установка значений. Конструкторы и присваивания

119

Должен ли конструктор класса complex быть объявлен как explicit, зависит
от ожидаемого применения этого класса. Поскольку комплексное число с нулевой
мнимой частью математически идентично действительному числу, неявное пре­
образование не создает семантических несогласованностей. Неявный конструк­
тор более удобен, поскольку при этом значение или литерал типа double может
использоваться везде, где ожидается значение complex. Функции, некритичные к
производительности, могут быть однократно реализованы для complex и исполь­
зоваться для double.
В С++03 атрибут explicit был важен только для конструкторов с одним ар­
гументом, но начиная с C++11 из-за введения унифицированной инициализации
(раздел 2.3.4) explicit становится важным и для конструкторов с несколькими
аргументами.

2.3.1.4. Делегирование

| С++11

В приведенных ранее примерах у нас были классы со многими конструктора­
ми. Обычно такие конструкторы различаются не очень сильно, и часть их кода
одинакова, так что зачастую имеется определенная избыточность. В С++03 при
наличии только примитивных переменных это обычно игнорировалось; в про­
тивном случае подходящие фрагменты кода выносились в отдельный метод, кото­
рый вызывался несколькими конструкторами.
C++11 предоставляет возможность делегирования конструкторов, когда одни
конструкторы могут вызывать другие. Наш класс complex мог бы использовать
эту возможность вместо аргументов по умолчанию:
class complex
{
public:
complex(double r, double i) : r{r}, i{i}
complex (double r) : cotnplexfr, 0.0} {}
complex () : cotnplexf 0.0} {}

{}

};

Очевидно, что для небольших примеров выгода не столь впечатляюща. Деле­
гирование конструкторов становится более полезным для классов с более сложной
инициализацией (более комплексной, чем для комплексных чисел).
2.3.1.5. Значения членов по умолчанию

| С++111

Еще одна новая возможность в C++11 — значения по умолчанию для перемен­
ных-членов. При их использовании нам нужно указать в конструкторе только те
значения, которые отличаются от значений по умолчанию:
class complex
{
public:
complex(double r, double i)

: r{r},

i{i}

{}

Классы

120
complex(double г)
complex () {}

private:
double г =
};

: г{г}

{}

0.0, i = 0.0;

И вновь — польза от этого нововведения, безусловно, более выражена для
больших классов.

2.3.2. Присваивание
В разделе 2.3.1.2 мы видели, что можно копировать объекты пользовательских
классов без применения функций получения и установки значений членов, по
крайней мере в процессе построения. Сейчас мы хотим получить возможность ко­
пирования в существующие объекты с помощью записи наподобие следующей:
х =
u =

у;
v = w = х;

Для этого класс должен обеспечивать оператор присваивания (или не мешать
компилятору сгенерировать таковой). Как обычно, сначала мы рассмотрим класс
complex. Присвоение значения complex переменной типа complex требует опе­
ратора
complexfi operator = (const cotnplex& src)
{
r = src.r; i = src.i;
return *this;
}

Очевидно, что мы копируем члены г и i. Оператор возвращает ссылку на
объект, что позволяет использовать цепочки присваиваний. Указатель this явля­
ется указателем на текущий объект, а так как нам нужна ссылка, мы разыменовы­
ваем указатель this. Оператор, который присваивает значения с типом объекта,
называется копирующим присваиванием и может быть сгенерирован компилято­
ром. В нашем конкретном примере сгенерированный код будет идентичен наше­
му, так что здесь мы могли бы опустить нашу реализацию присваивания.
Что же произойдет, если мы присвоим значение типа double переменной типа
complex?
с =

7.5;

Он будет компилироваться без определения оператора присваивания для типа
double. Мы вновь сталкиваемся с неявным преобразованием: неявный конструк­
тор создает объект типа complex “на лету” и присваивает его. Если это становит­
ся проблемой с точки зрения производительности, мы можем добавить присваи­
вание для типа double:

2.3. Установка значений. Конструкторы и присваивания
сошр1ех& operator =
{
г = nr; i = 0;
return *this;
}

121

(double nr)

Как и ранее, сгенерированный оператор для класса vector является неудов­
летворительным, потому что он копирует адрес данных, а не сами данные. Реа­
лизация оператора присваивания для этого класса очень похожа на реализацию
копирующего конструктора:
1
2
3
4
5
6
7
8
9

vectors operator = (const vectors src)
{
if (this == Ssrc)
return *this;
assert(my_size == src.my_size) ;
for(int i = 0; i < my_size; ++i)
data [i]= src.data[i];
return *this;
}

Рекомендуется [45, советы 52, 55], чтобы копирующее присваивание и копирую­
щий конструктор были согласованными во избежание путаницы у пользователей.
Присваивание объекта самому себе (когда исходный и целевой объекты имеют
один и тот же адрес) может быть опущено (строки 3 и 4). В строке 5 мы проверя­
ем, является ли присваивание корректной операцией, проверяя равенство разме­
ров векторов. В качестве альтернативы, если размеры различаются, присваивание
может изменить размер целевого вектора. Это технически законный вариант, но с
научной точки зрения довольно сомнительный. Просто подумайте о математичес­
ком или физическом контексте, в котором векторное пространство вдруг меняет
свою размерность.

2.3.3. Список инициализаторов

|с++п|

C++11 вводит новую возможность — списки инициализаторов (не путайте со
“списком инициализации членов” (раздел 2.3.1). Для его использования мы долж­
ны включить заголовочный файл . Хотя эта функция ор­
тогональна к концепции класса, конструктор и оператор присваивания вектора
являются отличными примерами их применения, что делает данное место книги
вполне подходящим для рассмотрения списков инициализаторов. Они позволяют
нам установить все элементы вектора одновременно (в разумных пределах).
Обычные массивы С можно полностью инициализировать непосредственно
при их определении:
float v[] =

(1.0, 2.0, 3.0};

122

Классы

Эта возможность обобщена в C++11, так что любые классы могут быть ини­
циализированы с помощью списка значений (подходящего типа). При наличии
соответствующего конструктора мы могли бы написать
vector v = {1.0, 2.0, 3.0};

или
vector v{1.0, 2.0, 3.0};

Можно также установить все значения элементов вектора при присваивании:
v = {1.0, 2.0, 3.0};

Функции, принимающие аргументы типа vector, могут быть вызваны с объек­
том vector, созданным “на лету”:
vector х = lu_solve (A, vector{1.0,

2.0, 3.0});

Предыдущая инструкция решает систему линейных уравнений для вектора
(1,2,3)т с помощью LU-разложения А.
Чтобы использовать эту возможность в нашем классе vector, нам нужно, чтобы
конструктор и оператор присваивания могли получать initializer_list
в качестве аргумента. Ленивые люди могут реализовать только конструктор и ис­
пользовать его в копирующем присваивании. Для демонстрации и повышения
производительности мы реализуем их оба. Это также позволит нам проверить сов­
падение размеров векторов при присваивании:
#include
#include

class vector
{

// ...

vector (std::initializer__list values)
: my_size(values.size ()), data(new double[my_size])

{

std::copy(std::begin(values),std::end(values),
std::begin(data));
}

vector& operator =(std::initializer_list values)
{

assert(my_size == values.size ());
std::copy(std::begin(values), std::end(values),
std::begin(data));
return *this;
}

};

Для копирования значений из списка наших данных мы используем функ­
цию std: :сору из стандартной библиотеки. Эта функция принимает в качестве

123

2.3. Установка значений. Конструкторы и присваивания

аргументов три итератора3. Эти три аргумента являются началом и концом вход­
ных данных и начальным местоположением для записи выходных данных. Сво­
бодные функции begin и end были введены в С++11. В С++03 мы должны ис­
пользовать соответствующие функции-члены, например values .begin ().

2.3.4. Унифицированная инициализация

|с++11|

Фигурные скобки {} используются в C++И как универсальная запись для всех
видов инициализации переменных
• конструкторами со списками инициализаторов,
• другими конструкторами или
• непосредственной установкой членов.
Последние доступны только для массивов и классов, если все (нестатические)
переменные являются открытыми и класс не имеет пользовательских конструк­
торов4. Такие типы называются агрегатами, а установка их значений с помощью
списков в фигурных скобках — соответственно агрегатной инициализацией.
В предположении, что мы могли бы определить некоторый неряшливый класс
complex без конструкторов, инициализировать его можно было бы следующим
образом:
struct sloppy_complex
{

double г, i;

};
sloppy_complex

zl{3.66, 2.33},
z2 « {0, 1};

Незачем говорить, что агрегатной инициализации мы предпочитаем использо­
вание конструкторов. Однако она может оказаться удобной, когда мы имеем дело
с устаревшим кодом.
Класс complex из данного раздела, который содержит конструкторы, может
быть инициализирован с помощью такой же записи:
complex с{7.0, 8}, с2 = {0, 1}, сЗ = {9.3}, с4 = {с};
const complex сс = {сЗ};

Запись с символом = не разрешена, если соответствующий конструктор объяв­
лен как explicit.
Остаются списки инициализаторов, с которыми вы познакомились в предыду­
щем разделе. Использование списков инициализаторов в качестве аргумента уни­
фицированной инициализации на самом деле требует двойных фигурных скобок:
3 Которые представляют собой разновидность обобщенных указателей (см. раздел 4.1.2).
4 Дополнительными условиями являются отсутствие у класса базовых классов и отсутствие вир­
туальных функций (раздел 6.1).

Классы

124
vector vl = {{1.0, 2.0, 3.0}},
v2 {{3, 4, 5}};

Для упрощения нашей жизни C++И обеспечивает пропуск фигурных скобок в
унифицированном инициализаторе, т.е. фигурные скобки могут быть опущены,
и список записей передается в заданном порядке в аргументы конструктора или
данные-члены. Так можно сократить приведенные объявления до
vector vl = {1.0, 2.0, 3.0},
v2 {3, 4, 5};

Пропуск фигурных скобок является и благословением, и проклятием. Пред­
положим, что мы интегрировали наш класс complex в vector для реализации
vector complex, который можно удобно инициализировать:
vectorcomplex v

= {{1.5, -2}, {3.4}, {2.6, 5.13}};

Однако код
vector_complex vid = {{2}};
vector_complex v2d = {{2, 3}};
vector_complex v3d = {{2, 3, 4}};
std::cout « "vid = " « vid « std::endl;

...

может дать кажущиеся удивительными результаты:
vid = [(2,0)]
v2d = [(2,3)]
v3d = [ (2,0) ,

(3,0) ,

(4,0) ]

В первой строке у нас имеется один аргумент, так что вектор содержит одно
комплексное число, которое инициализируется конструктором с одним аргумен­
том (мнимая часть равна 0). Следующая инструкция создает вектор с одним эле­
ментом, для которого вызывается конструктор с двумя аргументами. Очевидно,
что эта схема не может продолжаться и дальше: у класса complex нет конструкто­
ра с тремя аргументами. Поэтому здесь мы переходим к множественным элемен­
там вектора, которые создаются конструкторами с одним аргументом. Некоторые
иные эксперименты заинтересованный читатель найдет в разделе А.4.2.
Еще одним применением фигурных скобок является инициализация перемен­
ных-членов:
class vector
{
public:
vector (int n)
: my_size{n), data {new double [my__size]}

private:
unsigned my_size;
double
*data;

{}

125

2.3. Установка значений. Конструкторы и присваивания

Это защищает нас от случайных небрежностей. В приведенном выше примере
мы инициализируем член типа unsigned аргументом типа int. Такое сужение
должно не понравиться компилятору, и нам придется заменить тип:
vector(unsigned n)

: my_size(n}, data{new double [my_size] }

{}

Мы уже демонстрировали, что списки инициализаторов позволяют создавать
непримитивные аргументы функций “на лету”, например
double d = dot(vector{3,

4, 5}, vector{7, 8, 9});

Когда тип аргумента очевиден — когда, например, доступна только одна пере­
грузка, — список может быть передан в функцию без указания типа:
double d = dot({3, 4, 5},

{7, 8, 9});

Соответственно, результаты функции также можно задать с помощью унифи­
цированной записи:
complex subtract(const complex& cl, const complex& c2)
{

return

{cl.r - c2.r, cl.i - c2.i};

}

Тип return этой функции — complex, и мы инициализируем его с помощью
списка из двух аргументов в фигурных скобках.
В этом разделе мы продемонстрировали возможности унифицированной ини­
циализации и некоторые ее риски. Мы убеждены в том, что это очень полезная
возможность, но из тех, которые следует использовать с некоторой осторожнос­
тью, особенно в сложных случаях.

2.3.5. Семантика перемещения

|с++п|

Копирование больших объемов данных — операция дорогостоящая, и про­
граммисты используют разные трюки, лишь бы избежать ненужных копий. Неко­
торые программные пакеты используют поверхностные копии. В нашем примере
для vector это будет означать, что мы копируем только адрес данных, но не сами
данные. В результате после присваивания
v = w;

эти две переменные содержат указатели на одни и те же данные в памяти. Если
мы изменим v [ 7 ], то это приведет к изменению w [ 7 ] и наоборот. Поэтому про­
граммное обеспечение с поверхностным копированием обычно предоставляет
функции для явного глубокого копирования:
copy(v, w);

Эта функция должна использоваться вместо присваивания всякий раз при
присваивании переменных. Для временных значений — например, вектора, ко­
торый возвращается как результат функции — поверхностная копия не является

Классы

126

критической, так как никакого иного доступа ко временным данным нет, так что
нет и никаких побочных эффектов от такого совместного использования одного
адреса. Цена, которую программист вынужден платить за такое отсутствие копи­
рования — особое внимание к тому, чтобы память не оказалась освобожденной
дважды, т.е. необходимо применение метода подсчета ссылок.
С другой стороны, глубокие копии слишком дороги, когда функция возвраща­
ет в качестве результата большие объекты. Позже мы рассмотрим весьма эффек­
тивный способ избежать копий (см. раздел 5.3), а сейчас рассмотрим еще одну
возможность, появившуюся для этого в C++11, — семантику перемещения. Идея
заключается в том, что для переменных (другими словами, для всех именованных
объектов) выполняется глубокое копирование, а для временных значений (объек­
тов, которые не могут быть переданы по имени) выполняется перемещение их
данных.
При этом встает логичный вопрос — как указать разницу между временными
и постоянными данными? Но есть хорошая новость: компилятор сам делает это
для нас. На жаргоне C++ временные значения называются rvalue, потому что они
могут появляться только в правой части инструкции присваивания. C++11 вводит
ссылки на rvalue, которые обозначаются с помощью двух амперсандов, &&. Значе­
ния с именами, так называемые lvalue, не могут быть переданы таким ссылкам на
rvalue.
2.3.5.1. Перемещающий конструктор

С++11

Предоставляя перемещающий конструктор и перемещающее присваивание,
мы можем обеспечить дешевое копирование rvalue:
class vector
{
// ...

vector(vector && v)
: my_size(v.my_size) , data(v.data)
{

v.data = 0;
v.my_size = 0;
}

};

Перемещающий конструктор уводит данные из источника и оставляет его пустым.
Объект, который передается функции как rvalue, рассматривается как закон­
чивший свое существование после возвращения из функции. Это означает, что
все его данные могут быть совершенно случайными. Единственное требование к
этому объекту — его уничтожение (раздел 2.4) не должно привести к сбою. Осо­
бое внимание должно (как обычно) уделяться обычным указателям. Они не долж­
ны указывать на случайные области памяти, чтобы, таким образом, исключить
сбой или случайное освобождение некоторых данных другого пользователя. Если
бы мы оставили указатель v.data неизменным, память была бы освобождена

23. Установка значений. Конструкторы и присваивания

127

при выходе v из области видимости, и данные целевого вектора стали бы недейс­
твительными. Обычно исходный указатель после операции перемещения должен
быть равен nullptr (0 в С++03).
Обратите внимание, что ссылка на rvalue, такая как vector&&v, сама по себе
rvalue не является, а представляет собой lvalue, поскольку обладает именем. Если
мы хотим передать нашу ссылку v в другой метод, который помогает перемеща­
ющему конструктору с переносом данных, ее следует вновь превратить в rvalue с
помощью стандартной функции std: :move (см. раздел 2.3.5.4).
2.3.5.2. Перемещающее присваивание

С++11

Перемещающее присваивание можно легко реализовать — просто обменивая
указатели на данные:
class vector
{

// ...
vectors operator =(vector && src)
{

assert(my_size == 0 |I my_size == src.my_size);
std::swap(data,src.data);
return *this;

}
};

Этот метод не требует от нас освобождения наших собственных существую­
щих данных, так как это будет сделано при уничтожении исходного объекта.

Рис. 2.2. Перемещение данных

Классы

128

Скажем, пусть у нас есть пустой вектор vl и временно созданный в пределах
функции f () вектор v2, как показано в верхней части рис. 2.2. Когда мы присваи­
ваем результат функции f () переменной vl
vl = f(); // f возвращает v2

перемещающее присваивание обменивает (с помощью стандартной функции
swap) указатели data так, что после этой операции vl содержит бывшие значе­
ния v2, a v2 становится пустым, как показано в нижней части рис. 2.2.

2.3.5.3. Устранение копирования
Если мы добавим в эти две функции запись в журнал, то увидим, что переме­
щающий конструктор вызывается не так часто, как мы думали. Дело в том, что
современные компиляторы предоставляют еще лучшую оптимизацию, чем пере­
нос данных. Эта оптимизация называется пропуском копирования^ когда компиля­
тор просто опускает копирование данных, создавая их таким образом, чтобы они
сразу же сохранялись по целевому адресу операции копирования.
Наиболее важным примером применения такой оптимизации является опти­
мизация возвращаемого значения (Return Value Optimization — RVO), в особен­
ности когда новая переменная инициализируется результатом вызова функции
наподобие следующего примера:
inline vector ones(int n)
{

vector v(n);
for(unsigned i = 0; i < n; ++i)
v[i]= 1.0;
return v;
}

vector w(ones(7));

Вместо создания переменной v и ее копирования (или перемещения) в w в кон­
це вызова функции компилятор может немедленно создать переменную w и вы­
полнять все операции прямо с ней. Копирующий (или перемещающий) конструк­
тор при этом не вызывается. Это легко проверить с помощью журнальной записи
или отладчика.
Пропуск копирования был доступен во многих компиляторах еще до появле­
ния семантики перемещения. Однако это не должно означать, что перемещающие
конструкторы бесполезны. Правила перемещения данных в стандарте являются
обязательными, в то время как оптимизация RVO не гарантируется. Часто ей мо­
гут препятствовать такие мелкие детали, как, например, наличие в функции не­
скольких операторов return.

2.4. Деструкторы

129

2.3.5.4. Где необходима семантика перемещения

С++11
Одна из ситуаций, в которых, определенно, используется перемещающий кон­
структор, — при применении функции std: :move. На самом деле эта функция
ничего не перемещает, а только приводит lvalue к rvalue. Другими словами, она
делает вид, что переменная является временной, т.е. делает ее перемещаемой.
В результате последующие конструкторы или присваивания будут вызывать пе­
регрузку для ссылки на rvalue, как показано в фрагменте кода
vector х (std: :move (w));
v = std: :move(u);

В первой строке x забирает данные w и оставляет этот вектор пустым. Вторая
инструкция обменивает v и и5.
Наши перемещающие конструктор и присваивание не вполне согласуются с
std: :move. До тех пор, пока мы имеем дело только с истинными временными
значениями, мы не увидим разницу. Однако для большей согласованности мы мо­
жем оставлять исходный объект в пустом состоянии:
class vector
{
// ...
vectors operator =(vector&& src)
{
assert(my_size == src.my_size);
delete[] data;
data = src.data;
src.data = nullptr;
src.my_size = 0;
return *this;
}
};

Следует также учесть, что после std: :move объекты считаются устаревшими
(expired). Иначе говоря, они все еще не мертвы, но уже вышли в отставку, и не
гарантируется, что они имеют какое-то конкретное значение; важно лишь, что их
состояние должно быть корректным в том смысле, что деструктор такого объекта
не должен привести к аварии.
Красивое применение семантики перемещения можно найти в реализации по
умолчанию std:: swap в С++11 и выше (см. раздел 3.2.3).

2.4. Деструкторы
Деструктор представляет собой функцию, которая вызывается каждый раз,
когда объект уничтожается, например
5 В нашей конкретной реализации перемещающего присваивания с обменом, не в общем слу­
чае. — Примеч. ред.

Классы

130
~coniplex()
{

std::cout « "Спасибо за службу! Прощай!\п";
}

Поскольку деструктор является дополнительной по отношению к конструкто­
ру операцией, он использует символ дополнения (~). В отличие от конструкторов
имеется только одна-единственная перегрузка деструктора, и никакие аргументы
в нем не разрешены.

2.4.1. Правила реализации
Есть два очень важных правила.

1. Никогда не генерируйте исключения в деструкторе! Вполне вероятно, что
ваша программа аварийно завершится и исключение перехвачено не будет.
В C++11 или выше исключение в деструкторе всегда обрабатывается как
ошибка времени выполнения, которая прерывает выполнение программы
(деструкторы неявно объявляются как noexcept; раздел 1.6.2.4). Что проис­
ходит в С++03, зависит от реализации компилятора, но наиболее вероятной
реакцией является аварийное завершение программы.
2. Если класс содержит виртуальную функцию, деструктор тоже должен быть
виртуальным. Мы вернемся к этому в разделе 6.1.3.

2.4.2. Корректная работа с ресурсами
Что мы делаем в деструкторе — это наш свободный выбор; у нас нет никаких
ограничений на эти действия, накладываемые языком. Практически главная за­
дача деструктора — освобождение ресурсов объекта (память, дескрипторы фай­
лов, сокеты, блокировки...) и уборка за объектом, который больше не нужен в
программе. Поскольку деструктор не должен генерировать исключения, многие
программисты убеждены в том, что единственной функцией деструкторов долж­
но быть только освобождение ресурсов.
*=> c++03/vector_test.срр

В нашем примере нам нечего делать при уничтожении комплексного числа, так
что мы можем опустить деструктор. Деструктор необходим, когда объект получа­
ет ресурсы, например память. В таких случаях память или иные ресурсы должны
быть освобождены в деструкторе:
class vector
{
public:
// ...
-vector ()
{

delete [] data;

2.4. Деструкторы

131

}
// ...
private:
unsigned my_size;
double
*data;
};

Обратите внимание, что оператор delete сам проверяет, не равен ли указа­
тель nullptr (0 в С++03).
Файлы, которые открываются с помощью старых функций С, возвращающих
дескриптор, требуют по окончании работы явного закрытия (и это главная при­
чина их не использовать).
2.4.2.1. Захват ресурса есть инициализация

Захват ресурса есть инициализация (Resource Acquisition Is Initialization —
RAII) — это парадигма, разработанная в основном Бьярне Страуструпом (Bjarne
Stroustrup) и Эндрю Кёнигом (Andrew Koenig). Идея заключается в связывании ре­
сурсов с объектами и применении механизмов создания и уничтожения объекта
для автоматической обработки ресурсов в программах. Каждый раз, желая полу­
чить ресурс, мы делаем это с помощью создания объекта, который им владеет. Вся­
кий раз, когда объект покидает область видимости, ресурс (память, файл, сокет...)
автоматически освобождается, как в приведенном выше примере с вектором.
Представьте себе программу, которая выделяет 37 186 блоков памяти в 986 мес­
тах программы. Вы можете быть уверены, что все блоки памяти будут освобож­
дены? И сколько времени мы затратим, чтобы получить такую уверенность (или
по крайней мере приемлемый уровень доверия)? Даже с таким инструментарием,
как valgrind (раздел В.З) мы сможем только проверить отсутствие утечек памя­
ти для конкретного запуска программы, но не сможем в общем случае гаранти­
ровать, что память всегда будет освобождена. С другой стороны, если все блоки
памяти выделяются в конструкторах и освобождаются в деструкторах, мы можем
быть уверены, что утечки нет.
2.4.2.2. Исключения

Освобождение всех ресурсов оказывается еще более сложной задачей при нали­
чии исключений. Всякий раз, обнаруживая проблему, мы должны освободить все
ресурсы, захваченные к этому моменту, прежде чем сгенерировать исключение.
К сожалению, сказанное не ограничивается ресурсами в текущей области види­
мости, но распространяется и на охватывающие области видимости — в зависи­
мости от того, где перехватывается данное исключение. Это означает, что изме­
нение обработки ошибок требует утомительного изменения ручного управления
ресурсами.

Классы

132

2.4.2.3. Управление ресурсами
Все ранее упомянутые проблемы можно решить путем внедрения классов, ко­
торые управляют ресурсами. C++ уже предлагает такие менеджеры в стандартной
библиотеке. Файловые потоки управляют файловыми дескрипторами из языка
программирования С. unique_ptr и shared_ptr управляют памятью безопасно
с точки зрения исключений и без риска утечек памяти6. В нашем примере с векто­
ром мы также можем воспользоваться unique_ptr (при этом нам не потребова­
лось бы реализовывать деструктор).

2.4.2.4. Управление собой
Интеллектуальные указатели показывают, что могут существовать различные
подходы к управлению ресурсами. Когда ни один из существующих классов не
обрабатывает ресурс так, как мы хотим, это повод развлечься написанием диспет­
чера ресурсов с учетом наших потребностей.
“Развлекаясь” таким образом, мы не должны управлять в классе более чем од­
ним ресурсом. Это базовое правило основано на том, что в конструкторах могут
вызываться исключения, и очень утомительно писать конструктор так, чтобы га­
рантировать освобождение всех захваченных к этому моменту ресурсов.
Таким образом, всякий раз, когда мы пишем класс, который работает с двумя
ресурсами (даже одного и того же типа), мы должны представлять класс, который
управляет одним из ресурсов. Еще лучше — написать диспетчеры для обоих ре­
сурсов и полностью отделить работу с ресурсами от научной части программы.
Даже в случае исключения в средине конструктора у нас не возникнет проблем с
утечкой ресурсов, так как автоматически вызванные деструкторы их диспетчеров
позаботятся об освобождении ресурсов.
Термин “RAII” лингвистически переносит больший вес на инициализацию, од­
нако деструкция технически имеет еще более важное значение. Не обязательно,
чтобы ресурс захватывался именно в конструкторе. Это может произойти и позже
во время жизни объекта. Главное — что за ресурс отвечает единственный объект,
который и освобождает его в конце своей жизни. Йон Кальб (Jon Kalb) называет
этот подход применением принципа единой ответственности (Single Responsibi­
lity Principle — SRP); имеет смысл посмотреть его лекцию, которая доступна в Ин­
тернете.

2.4.2.5. Спасение ресурса

С++11

В этом разделе мы представим методику автоматического освобождения ресур­
сов, даже когда мы используем пакет программного обеспечения с явной обра­
боткой ресурсов. Мы продемонстрируем с помощью интерфейса Oracle C++ Call
Interface (OCCI) [33] для доступа к базам данных Oracle из программ на C++. Этот
пример позволяет нам показать реалистичное приложение, тем более что многим
ученым и инженерам приходится время от времени иметь дело с базами данных.
6 Отдельного рассмотрения требуют только циклические ссылки.

2.4. Деструкторы

133

Хотя база данных Oracle и является коммерческим продуктом, наш пример можно
протестировать с помощью бесплатного выпуска Oracle Database Express Edition.
OCCI представляет собой С++-расширение библиотеки OCI для языка С и до­
бавляет лишь тонкий слой с некоторыми возможностями C++ поверх нее, при
сохранении всей архитектуры в стиле языка программирования С. К сожалению,
это относится к большинству межъязычных интерфейсов библиотек С. Посколь­
ку язык С не поддерживает деструкторы, он не может воспользоваться идиомой
RAII, так что ресурсы должны освобождаться явно.
В OCCI необходимо сначала создать объект типа Environment, который мож­
но будет использовать для установления соединения Connection с базой данных.
Это, в свою очередь, позволит нам написать объект Statement, который возвра­
щает ResultSet. Все эти ресурсы представлены обычными указателями и долж­
ны быть освобождены в обратном порядке.
В качестве примера рассмотрим табл. 2.1, в которой наш старый знакомый Гер­
берт отслеживает свои решения (предположительно) нерешенных математичес­
ких проблем. Во втором столбце показано, заслуживает ли он награды за свою
работу. В целях экономии бумаги мы не будем приводить здесь полный список
его достижений.

Таблица 2.1. Достижения Герберта
Проблема

Достойна награды

Круг Гаусса
Конгруэнтные числа
Дружественные числа

V
?
V

=> c++03/occi_old_style.срр

Время от времени Герберт ищет свои достойные награды открытия с помощью
следующей программы на C++:
tfinclude
tfinclude
flinclude cocci.h>

using namespace std;
// Импорт имен
using namespace oracle::occi;
int main ()
{
string dbConn
user
password
Environment *env
Connection *conn

=
=
=
=
=

(раздел 3.2.1)

”172.17.42.1”,
’’Herbert”,
" NSA_go_away";
Environment::createEnvironment ();
env->createConnection(user,password,dbConn);

Классы

134
string query = ’’select problem from my_solutions"
” where award_worthy != 0”;
Statement *stmt = conn->createStatement(query);
ResultSet *rs
= stmt->executeQuery();

while(rs->next())
cout « rs->getString(1) « endl;

stmt->closeResultSet(rs);
conn->terminateStatement(stmt);
env->terminateConnection (conn);
Environment:: terminateEnvironment (env) ;
}

В этот раз мы не можем винить Герберта за его старый стиль программирова ­
ния — его вынуждает к этому библиотека. Давайте посмотрим на исходный текст
Герберта. Даже для программистов, не знакомых с OCCI, происходящее очевидно.
Прежде всего мы захватываем ресурсы, затем выполняем итерации по гениаль­
ным достижениям Герберта и наконец освобождаем ресурсы в обратном порядке.
Мы выделили операции освобождения ресурсов полужирным шрифтом, так как
им нужно будет уделить более пристальное внимание.
Техника освобождения работает достаточно адекватно, когда наша (или Гербер­
та) программа представляет собой монолитный блок, такой как показан выше. Си­
туация полностью меняется, когда мы пытаемся создавать функции с запросами:
ResultSet *rs = makes_me_famous ();
while(rs->next())
cout « rs->getString(1) « endl;

ResultSet *rs2 = needs_more_work();
while(rs2->next ())
cout « rs2->getString(1) « endl;

Теперь у нас есть результирующие наборы без соответствующих закрывающих
их инструкций; они были объявлены в функциях запросов и теперь находятся вне
области видимости. Таким образом, для каждого объекта необходимо дополни­
тельно поддерживать другой объект, который использовался для его создания.
Рано или поздно эти зависимости превращаются в кошмар с огромным потенци­
алом для ошибок.
■=> c++ll/occi_resource_rescue.срр

Главный вопрос звучит так: как можно управлять ресурсами, которые зави­
сят от других ресурсов? Решение заключается в использовании удалителей из
unique ptr или shared_ptr. Они вызываются всякий раз, когда освобождается
управляемая память. Интересным аспектом удалителей является то, что они не
обязаны фактически освобождать память. Мы воспользуемся этой свободой для
управления нашими ресурсами. Проще всего обрабатывается Environment, пото­
му что этот ресурс не зависит от других ресурсов:

135

2.4. Деструкторы
struct environment-deleter {
void operator()(Environment *env)
{ Environment::terminateEnvironment(env);

}

};
shared_jptr environment (
Environment::createEnvironment(),environment-deleter {});

Теперь мы можем создать столько копий среды, сколько необходимо, и при
этом получить гарантию, что удалитель вызовет terminateEnvironment (env),
когда за пределы области видимости выйдет последняя из копий.
Для создания и завершения Connection требуется Environment. Поэтому мы
храним его копию в удалителе connection deleter:
struct connection-deleter
{
connection_deleter (shared_jotr env)
: env(env) {}
void operator()(Connection* conn)
{ env->terminateConnection(conn) ; }
shared_j?tr env;
};

shared_jotr connection(environment->createConnection (...),

connection—dele ter {environment});

Теперь у нас есть гарантия того, что соединение Connection будет завершено, ког­
да перестанет быть нужным. Наличие копии Environment в connection_deleter
гарантирует, что этот объект не будет уничтожен до тех пор, пока существует
объект Connection.
Работать с базами данных будет удобнее, если создать для них класс-менеджер:
class db_manager
{
public:
using ResultSetSharedPtr = std::shared_ptr;

db_manager(string const& dbConnection, string const& dbUser,
string const& dbPw)
: environment(Environment ::createEnvironment()r
environment-deleter{}) ,
connection(environment->createConnection(dbUser, dbPw,
dbConnection)r

connection—dele ter { environment} )
{}
// Некоторые функции получения значении...
private:
shared_j3tr environment;
shared_ptr connection;

Классы

136

Обратите внимание, что класс не имеет деструктора, поскольку члены пред­
ставляют собой управляемые ресурсы.
В этот класс можно добавить метод query, который возвращает управляемый
результирующий набор ResultSet:
struct result_set_deleter
{

result_set_deleter(shared_ptr conn,
Statement* stmt )
: conn(conn)f stmt(stmt) {}
void operator ()( ResultSet *rs )
{

// Вызов оператора, как в 3.8

stmt->closeResultSet(rs);
conn-> terminates tatement (stmt);

}

shared_jotr conn;
Statement*
stmt;
};

class db_manager
{
public:
// ...
ResultSetSharedPtr query(const std::string& q) const
{
Statement* stmt = connection->createStatement(q) ;
ResultSet*rs = stmt->executeQuery();
auto deleter = result_set_deleter{connection,stmt};
return ResultSetSharedPtr{rs,deleter};
}
};

Благодаря этому новому методу и нашим удалителям само приложение стано­
вится очень простым:
int main

()

{

dbjnanager db("172.17.42.1", "herbert", "NSA_go_away");
auto rs = db.query("select problem from my_solutions "
" where award_worthy != 0");
while(rs->next())
cout « rs->getString(1) « endl;
}

Чем больше у нас запросов, тем больше окупаются наши усилия. Ничего стыд­
ного в том, чтобы постоянно повторять самому себе: все ресурсы освобождаются
неявно.

2.5. Резюме генерации методов

137

Внимательный читатель должен сообразить, что мы нарушили принцип еди­
ной ответственности. Чтобы выразить нашу признательность за это открытие, мы
приглашаем вас улучшить наш дизайн в упражнении 2.8.4.

2.5. Резюме генерации методов
C++ имеет шесть методов (четыре в С++03) с поведением по умолчанию:
• конструктор по умолчанию;

• копирующий конструктор;
• перемещающий конструктор (С++11 и старше);


копирующее присваивание;

• перемещающее присваивание (С++11 и старше);
• деструктор.
Код для них может быть сгенерирован компилятором, что спасает нас от скуч­
ной монотонной работы и, таким образом, предотвращает оплошности.
Правила, определяющие, какой метод генерируется компилятором, содержат
достаточное количество деталей, которые более подробно рассматриваются в при­
ложении А, “Скучные детали”, раздел А.5. Здесь же мы только хотим окончатель­
но подытожить результаты для C++11 и более поздних стандартов.
Правило шести
Реализуйте как можно меньше из шести перечисленных выше операций, но объяв­
ляйте их как можно больше. Любая не реализованная операция в идеале должна
быть объявлена как default или delete.

2.6. Доступ к переменным-членам
C++ предоставляет несколько способов доступа к членам наших классов. В этом
разделе мы рассмотрим различные варианты и обсудим их преимущества и не­
достатки. Надеюсь, вы почувствуете, как конструировать ваши классы в будущем
таким образом, чтобы они лучше всего подходили для вашей предметной области.

2.6.1. Функции доступа
В разделе 2.2.5 мы вводили функции получения и установки значений для доступа к переменным класса complex. Их использование становится громоздким,
если мы хотим, например, увеличить действительную часть:
c.set_r (c.get_r () + 5.);

Классы

138

Эта запись выглядит совершенно не как числовая операция и не очень удобо­
читаема. Лучший способ реализовать эту операцию — написать функцию-член,
которая возвращает ссылку:
class complex {
public:

doubles real() { return r; }
};

С помощью такой функции можно записать
с. real () += 5.;

Это уже выглядит намного лучше, но все равно немного странно. Почему бы
не использовать запись наподобие приведенной ниже?
real(с) += 5.;

Для этого нам надо написать свободную функцию (функцию, не являющуюся
членом):
inline doubles real(complexS c) { return c.r; }

Увы, данная функция должна обращаться к закрытому члену г. Мы можем из­
менить свободную функцию так, чтобы она вызывала функцию-член:
inline doubles real(complexS с)

{ return c.real();

}

В качестве альтернативы можно объявить свободную функцию в качестве дру­
га класса complex для доступа к закрытым данным:
class complex {
friend doubles real(complexS c);
};

Доступ к действительной части должен работать и тогда, когда комплексное
число является константой. Таким образом, нам также нужна константная версия
этой функции:
inline

const doubles real(const complexS c) { return c.r; }

Эта функция также требует объявления как friend.
В двух последних функциях мы возвращаем ссылки, но они гарантированно не
окажутся устаревшими. Очевидно, что функции — как свободная, так и функциячлен — могут быть вызваны, только когда объект уже создан. Ссылка на часть
real, которую мы используем в инструкции
real(с) += 5.;

существует только до конца инструкции, в отличие от переменной с, на которую
ссылается эта функция. Эта переменная существует до конца области видимости,
в которой она определена. Мы можем создать ссылочную переменную
double Srr = real(с);

2.6.

Доступ к переменным-членам

139

которая существует до конца текущей области видимости. Даже в случае, когда
с объявлена в той же области, обратный порядок уничтожения объектов в C++
гарантирует, что с существует дольше, чем гг.
Ссылки на члены временных объектов можно безопасно использовать в том
же выражении, например
double r2

= real(complex(3,7))*2.0; // OK!

Временное число типа complex существует только в самой инструкции, но по
крайней мере дольше, чем ссылка на ее часть real, так что эта инструкция явля­
ется корректной. Однако, если мы сохраним эту ссылку на действительную часть,
она будет устаревшей:
const double &rr = real(complex(3,7)); // Плохо!!!
cout « ’’Действительная часть равна ’’ « rr « ’ \n';

Комплексная переменная создана временно и существует только до конца пер­
вой инструкции. Ссылка на действительную часть живет до конца охватывающей
области видимости.

Правило
Не храните ссылки на временные выражения!
Они становятся некорректными еще до первого применения.

2.6.2. Оператор индекса
Для перебора элементов вектора мы могли бы написать функцию наподобие
следующей:
class vector
{
public:

double at(int i)
{

assert (i >= 0 && i < my_size);
return data[i];
}

};

Суммирование элементов вектора в таком случае имеет вид
double sum= 0.0;
for (int i= 0; i < v.size(); ++i)
sum += v. at(i);

Языки программирования C++ и С для доступа к элементам массивов (фикси­
рованного размера) используют оператор индекса. Поэтому вполне естественно
сделать то же самое и для векторов (с динамическим размером). Тогда мы могли
бы переписать предыдущий пример как

140

Классы
double sum= 0.0;
for (int i= 0; i < v.sizeQ; ++i)
sum += v[i];

Эта запись более краткая и более ясно показывает, что мы делаем.
Перегрузка оператора имеет тот же синтаксис, что и оператор присваивания, и
ту же реализацию, что и функция at:
class vector
{
public:

doubles operator!](int i)
{
assert(i >= 0 SS i < my_size);
return data[i];
}

};

С помощью этого оператора мы можем получить доступ к элементам вектора
с помощью квадратных скобок, но (при таком виде оператора) только если вектор
является изменяемым.

2.6.3. Константные функции-члены
Это приводит к более общему вопросу. Как можно написать операторы и фун­
кции-члены, которые применяются к константным объектам? На самом деле опе­
раторы являются просто особой формой функций-членов и могут быть вызваны,
как функции-члены:
v[i]; // Синтаксическое сокращение для:
v.operator!](i);

Конечно, длинная запись почти никогда не используется, но она иллюстрирует,
что операторы являются обычными методами, для которых имеется дополнитель­
ный синтаксис вызова.
Свободные функции позволяют указывать константность каждого аргумента.
Функции-члены в своей сигнатуре никак не упоминают об объекте, для которого
вызываются. Как же мы можем указать, что текущий объект должен быть конс­
тантным? Для этого имеется специальное обозначение — добавление квалифика­
тора после заголовка функции:
class vector
{
public:
const doubles operator[](int i) const
{
assert (i >= 0 SS i < my_size);
return data[i];
}
};

2.6. Доступ к переменным-членам

141

Атрибут const — не просто случайное указание на то, что программист не
возражает против вызова этой функции-члена с константным объектом. Ком­
пилятор C++ относится к константности очень серьезно и будет проверять, что
данная функция никак не изменяет объект (т.е. некоторые из его членов) и что
этот объект передается другим функциям только как константный аргумент. Та­
ким образом, когда вызываются другие методы, они также должны бытьконс­
тантными.
Эта гарантия константности не позволяет также вернуть такой функции некон­
стантный указатель или ссылку на члены данных. Она может возвращать толь­
ко константные указатели или ссылки, а также объекты. Возвращаемое значение
не обязано быть константным (но может быть таковым), так как это всего лишь
копия текущего объекта, одного из его переменных-членов (или констант), или
временная переменная. Ни одна из этих копий не несет риск изменения текущего
объекта.
Константная функция-член может вызываться для неконстантного объекта
(так как C++ при необходимости неявно преобразует неконстантные ссылки в
константные). Таким образом, часто достаточно предоставить только констант­
ную функцию-член. Например, ниже показана функция, которая возвращает раз­
мер вектора:
class vector
{
public:
int size() const { return my_size; }
// int size() { return my_size; } // Бессмысленно

};

Неконстантная функция size делает то же, что и константная, а потому бес­
смысленна.
Для нашего оператора индекса нам нужны обе версии — как константная, так
и неконстантная. Если бы у нас имелась только константная функция-член, то мы
могли бы использовать ее для чтения элементов как константных, так и изменяе­
мых векторов, но при этом мы не могли бы вносить изменения в последние.
Данные-члены могут быть объявлены как mutable. В таком случае они могут
быть изменены даже в константных методах. Эта возможность предназначена для
внутренних состояний — наподобие кешей, — которые не влияют на наблюдае­
мое поведение. Мы не используем эту возможность в данной книге и рекомендуем
вам прибегать к ней только тогда, когда это действительно необходимо, поскольку
она подрывает защиту данных языком.

2.6.4. Ссылочная квалификация членов

|с++п|

В дополнение к константности объекта (т.е. *this), в С++11 мы можем так­
же потребовать, чтобы объект был ссылкой на lvalue или rvalue. Предположим, у
нас есть векторное сложение (см. раздел 2.7.3). Его результатом будет временный

Классы

142

объект, который не является константным. Таким образом, можно присвоить зна­
чение его элементу:
(v + w)[i]= 7.3; // Бессмысленно

Правда, это довольно искусственный пример, но он иллюстрирует, что остают­
ся возможности для совершенствования.
В левой части присваиваний должны быть только изменяемые lvalue (что всег­
да справедливо для встроенных типов). Но рассмотренный пример поднимает но­
вый вопрос — почему (v+w) [i] представляет собой изменяемое lvalue-значение?
Оператор индекса у типа vector имеет две перегрузки — для изменяемых и конс­
тантных объектов, v+w константой не является, поэтому более предпочтительной
является перегрузка для изменяемых векторов. Таким образом, в этом выражении
мы обращаемся к изменяемой ссылке на член изменяемого объекта, что вполне
разрешено и законно.
Проблема в том, что (v+w) [i] является lvalue, в то время как v+w — нет. Нам
не хватает требования, чтобы оператор индекса мог применяться только к lvalue:
class vector
{
public:
doubles
operator[](int i)
const doubles operator[](int i)

...
...

S {
const S {

}
}

// #1
// #2

};

Квалифицируя одну из перегрузок с помощью ссылки, мы должны так же ква­
лифицировать и другие перегрузки. С такой реализацией перегрузка #1 не может
использоваться для временных векторов, а перегрузка #2 возвращает ссылку на
константу, которой нельзя присваивать значение. В результате бессмысленное
присваивание, показанное выше, приведет к ошибке времени компиляции:
vector_features .срр:167:15:error: read-only variable is not assignable7

(v + w) [i] = 3;

Мы можем аналогично квалифицировать и операторы присваивания векторов,
чтобы запретить присваивание временным объектам:
v+w=u; // Бессмысленно, следует запретить

Как и следовало ожидать, два амперсанда позволяют ограничить функциичлены объектами rvalue, т.е. этот метод может вызываться только для временных
объектов данного класса:
class my_class
{
something_good donate_my_data()

SS {

...

}

};

7 Ошибка: нельзя присваивать переменной, предназначенной только для чтения.

2.7. Проектирование перегрузки операторов

143

Примерами применения могут служить преобразования, в которых следует из­
бегать больших копий (например, матриц).
Доступ ко многомерным структурам данных, таким как матрицы, может осу­
ществляться различными способами. Можно использовать оператор приложения
(§3.8), который позволяет нам передать одновременно несколько индексов в ка­
честве аргументов. Оператор индекса, к сожалению, принимает только один ар­
гумент, и мы рассмотрим несколько способов справиться с этим ограничением в
приложении А.4.3 (ни один из которых не является полностью удовлетворитель­
ным). В разделе 6.6.2 будет представлен “продвинутый” подход к вызову операто­
ра приложения из сцепленных операторов индекса.

2.7. Проектирование перегрузки операторов
За малым исключением (раздел 1.3.10), в C++ могут быть перегружены боль­
шинство операторов. Однако перегрузка некоторых операторов имеет смысл толь­
ко для конкретных целей; например, выбор члена с разыменованием р->ш полезен
для реализации новых интеллектуальных указателей. Гораздо менее очевидно, как
интуитивно использовать этот оператор в научном или инженерном контексте.
Точно так же и изменение смысла оператора получения адреса &о требует весомо­
го обоснования.

2.7.1. Будьте последовательны
Как упоминалось ранее, язык дает нам высокую степень свободы в проекти­
ровании и реализации операторов для наших классов. Мы можем свободно вы­
бирать семантику каждого оператора. Однако чем ближе наше настраиваемое
поведение к таковому у стандартных типов, тем проще другим (коллегам, пользо­
вателям открытого исходного кода...) понять, что мы делаем, и доверять нашему
программному обеспечению.
Перегрузки, конечно, могут использоваться для лаконичного представления
операций в приложениях определенной предметной области, т.е. для создания,
по сути, встроенного языка для предметной области (Domain-Specific Embedded
Language — DSEL). В этом случае отход от типичного смысла операторов может
оказаться продуктивным решением. Тем не менее DSEL должны быть самосогла­
сованными. Например, если операторы =, + и += переопределяются пользовате­
лем, то выражения а = а + Ьиа+=Ь должны приводить к одному и тому же
результату.

Согласованная перегрузка

Определяйте свои операторы согласованно один с другим и по возможности обеспе­
чивайте семантику, аналогичную семантике этих операторов у стандартных типов.

Классы

144

Также нам никто не мешает выбирать произвольный тип возвращаемого зна­
чения каждого оператора; например, сравнение х == у может возвращать строку
или файл. И вновь, чем ближе результаты наших операторов будут к типичным
возвращаемым типам в C++, тем проще всем (и вам в том числе) будет работать
с вашими пользовательскими операторами.
Единственным предопределенным аспектом операторов являются их арность
(количество аргументов) и относительный приоритет операторов. В большинстве
случаев они наследуются от представимых ими операций; так, умножение всегда
получает два аргумента. Для некоторых операторов можно представить и иную
арность. Так, было бы неплохо, если бы оператор индекса мог получать два аргу­
мента; тогда мы могли бы получать доступ к элементу матрицы с помощью записи
A [i, j ]. Но единственный оператор, который может иметь произвольную арность
(включая реализации с переменным количеством аргументов; см. раздел 3.10), —
это оператор приложения operator ().
Другая свобода, предоставляемая нам языком, — свобода выбора типов аргу­
ментов. Мы можем, например, реализовать оператор индексации для аргументов
unsigned (возврат одного элемента), диапазона (возврат подвектора) и множес­
тва (возврат множества элементов вектора). Все это на самом деле реализовано в
библиотеке MTL4. По сравнению с MATLAB, C++ предлагает меньше операторов,
но у нас есть неограниченная возможность их перегрузки для создания любой не­
обходимой нам функциональности.

2.7.2. Вопросы приоритетов
При переопределении операторов следует удостовериться, что приоритет опе­
рации соответствует приоритету оператора. Например, мы можем захотеть ис­
пользовать запись ИГрХ для возведения матриц в степень:
А = ВА2;

А представляет собой В в квадрате. Все вроде бы хорошо. Исходное значение
оператора А — побитовое исключающее ИЛИ — не должно нас беспокоить: мы
не планируем реализовывать такую операцию для матриц.
Теперь прибавим С к В2:
А = ВА2 + С;

Выглядит красиво. Но не работает (или делает какую-то чушь). Почему? По­
тому что приоритет + выше приоритета А. Таким образом, компилятор понимает
это выражение как
А = ВА (2 + С) ;

Грустно... Хотя такая запись оператора обеспечивает краткий и интуитивно
понятный интерфейс, нетривиальные выражения могут не соответствовать на­
шим ожиданиям. Поэтому не забывайте о приоритетах.

2.7,

145

Проектирование перегрузки операторов

Уважайте приоритеты
Убедитесь, что семантический приоритет ваших перегруженных операторов соот­
ветствует приоритетам операторов C++.

2.7.3. Члены или свободные функции
Большинство операторов могут быть определены и как члены, и как свобод­
ные функции. Следующие операторы — присваивания любого рода, operator [ ],
operator-> и operator () — должны быть нестатическими методами для гаран­
тии, что их первый аргумент является lvalue. Примеры operator [ ] и operator ()
мы показали в разделе 2.6. В противоположность этому бинарные операторы со
встроенными типами в качестве первого аргумента могут определяться только как
свободные функции.
Последствия выбора того или иного варианта могут быть показаны с помощью
оператора сложения нашего класса complex:
class complex
{
public:
explicit complex(double rn = 0.0, double in = 0.0):r(rn),i (in){}

complex operator +(const complex& c2) const
{
return complex(r+c2.r, i+c2.i);
}

private:
double r,
};

i;

int main ()
{
complex cc(7.0, 8.0), c4(cc);
std::cout « "cc + c4 = " « cc
}

+ c4 « std::endl;

Можно ли при этом сложить также complex и double?
std:;cout « "cc + 4.2 = " «

cc + 4.2 « std::endl;

Нет, не с такой реализацией. Мы можем добавить перегрузку для оператора,
принимающую double в качестве второго аргумента:
class complex
{

complex operator +(double r2) const
{

return complex(r + r2, i);
}
};

Классы

146

Альтернативное решение — убрать из объявления конструктора explicit.
Тогда значение double может быть неявно преобразовано в complex, и мы скла­
дываем два комплексных значения.
Оба подхода имеют свои плюсы и минусы: перегрузка требует только одной до­
полнительной операции, а неявный конструктор является более гибким в целом,
позволяя передавать значения double вместо аргументов функций типа complex.
Если мы делаем и неявное преобразование, и перегрузку, то получаем и гибкость,
и эффективность.
А теперь давайте поменяем аргументы местами:
std::cout « ”4.2 + с4 = "

« 4.2 + с4 « std::endl;

Этот код не компилируется. Фактически выражение 4.2+с4 можно рассматри­
вать как сокращенную запись для
4.2.operator*(с4)

Другими словами, мы ищем оператор в типе double, который даже не являет­
ся классом.
Чтобы предоставить оператор с примитивным типом в качестве первого аргу­
мента, мы должны написать свободную функцию:
inline complex operator + (double d, const conplexfi c2)
{

return complex(d + real(c2),

imag(c2));

}

Таким же образом желательно реализовать и сложение двух комплексных зна­
чений — как свободную функцию:
inline complex operator +(const complexfi cl, const complex& c2)
{

return complex(real(cl) + real(c2),

imag(cl)

+ imag(c2));

}

Чтобы избежать неоднозначности, мы должны удалить соответствующую функ­
цию-член с комплексным аргументом.
Все это говорит о том, что основное различие между членом и свободной функ­
цией заключается в том, что член допускает неявное преобразование только для
второго аргумента (здесь — слагаемого), а свободная функция — для обоих аргу­
ментов. Если краткость исходного текста программы более важна, чем производи­
тельность, мы могли бы опустить все перегрузки с аргументом double и полагать­
ся на неявное преобразование.
Даже если мы сохраним все три перегрузки, более симметричным решением
будет их реализация как свободных функций. Второе слагаемое в любом случае
подлежит неявному преобразованию, так что лучше иметь одно и то же поведе­
ние для обоих аргументов. Словом, вот какой вывод из всего этого следует.

Упражнения

2.8.

147

Бинарные операторы
Реализуйте бинарные операторы как свободные функции.

То же различие справедливо и для унарных операторов, например
complex operator - (const complex& cl)
{ return complex(-real(cl), -imag(cl));

}

Эта свободная функция допускает неявное преобразование, в отличие от функ­
ции-члена
class complex
{
public:

complex operator -() const { return complex(-r, -i); }
};

Является ли это поведение желательным, зависит от контекста. Скорее всего,
пользовательский оператор разыменования * не должен включать преобразование.
Теперь реализуем оператор вывода для потоков. Этот оператор принимает
изменяемую ссылку на std: :ostream и обычно константную ссылку на объект
пользовательского типа. Для простоты давайте продолжим работать с нашим
классом complex:
std: :ostream & operator«(std: :ostream& os, const complexS c)
{

return os « ’(’ « real(c) «

« imag(c)

«

}

Поскольку первым аргументом является ostream&, мы не можем написать этот
оператор как функцию-член complex, а добавление члена в класс std: :ostream
решением не является. Приведенная реализация предоставляет возможность
вывода во все стандартные выходные потоки, т.е. в классы, производные от
std: :ostream.

2.8. Упражнения
2.8.1. Полиномы
Напишите класс для полиномов, который должен содержать по крайней мере
следующее:
• конструктор, которому передается степень полинома;
• динамический массив/вектор/список элементов типа double для хранения
коэффициентов;

148

Классы

• деструктор;

• функцию вывода в ostream.

Прочие члены, такие как арифметические операции, являются необязательными.

2.8.2. Перемещающее присваивание
Напишите перемещающий оператор присваивания для полинома из уп­
ражнения 2.8.1. Определите копирующий конструктор как default. Что­
бы проверить, как используется ваше присваивание, напишите функцию
polynomial f (double с2, double cl, double cO), которая принимает три
коэффициента и возвращает полином. Выведите сообщение в своем операторе
присваивания или используйте отладчик, чтобы убедиться, что ваше присваива­
ние используется в написанной вами функции.

2.8.3. Список инициализаторов
Расширьте программу из упражнения 2.8.1, добавив конструктор и оператор
присваивания для списка инициализаторов. Степень полинома должна быть на
единицу меньше длины списка инициализаторов.

2.8.4. Спасение ресурса
Выполните рефакторинг реализации из раздела 2.4.2.5. Реализуйте удалитель
для Statement и используйте управляемые инструкции в управляемом результи­
рующем наборе ResultSet.

Глава 3

Обобщенное
программирование
Шаблоны — это возможность языка программирования C++, обеспечивающая
создание функций и классов, которые работают с параметрическими (обобщен­
ными, универсальными) типами. В результате класс или функция может работать
со многими различными типами данных без переписывания вручную для каждо­
го из них.
Обобщенное программирование иногда считается синонимом шаблонного про­
граммирования. Но это не верно. Обобщенное программирование — парадигма
программирования, обеспечивающая максимальную применимость кода при со­
хранении корректности. Его основными инструментами являются шаблоны. Ма­
тематически оно основано на анализе формальных концепций [15]. В обобщенном
программировании программы шаблонов завершаются документированием доста­
точных условий для их корректного использования. Можно сказать, что обобщенное
программирование является ответственным стилем программирования шаблонов.

3.1. Шаблоны функций
Шаблон функции — именуемый также обобщенной функцией — это чертеж
для создания потенциально бесконечного количества перегрузок функций. В по­
вседневной речи чаще используется термин шаблонная функция (template function),
чем шаблон функции (function template), при этом последний термин является
корректным термином из стандарта. В этой книге мы используем оба термина, и
здесь они имеют один и тот же смысл.
Предположим, что мы хотим написать функцию шах (х, у), где х и у — пе­
ременные или выражения некоторого типа. Используя перегрузку функций, это
можно легко сделать следующим образом:
int max(int a, int b)
{

if

(a > b)
return a;
else
return b;

}

double max (double a.r double b)
{
if (a > b)
return a;
else
return b;
}

Обобщенное программирование

150

Обратите внимание, что тело функции совершенно одинаково для типов int и
double. С использованием механизма шаблонов мы можем записать только одну
обобщенную реализацию:
template
Т max (Т a, Т Ь)
{

if (а > Ь)
return а;
else
return b;
}

Этот шаблон функции заменяет нешаблонные перегрузки и сохраняет имя
max. Он может быть использован так же, как и перегруженные функции:
std::cout «
std::cout «
std::cout «

"Максимум 3 и 5 = "
« max (3, 5)
«
"Максимум 31 и 51 = "
« max (31, 51)
«
"Максимум 3.0 и 5.0 = " « max (3.0, 5.0) «

'\п’;
’\п’;
'\п';

В первом случае 3 и 5 являются литералами типа int, и функция шах инстан­
цируется в
int max (int, int);

Аналогично инстанцирования второго и третьего вызовов max имеют вид
long
max (long, long);
double max(double, double);

поскольку соответствующие литералы интерпретируются как long и double.
Точно так же шаблонная функция может быть вызвана с переменными и выра­
жениями:
unsigned ul = 2, u2 = 8;
std:: cout « "Максимум ul и u2 = " « max(ul, u2) « '\n’;
std::cout « "Максимум ul*u2 и ul+u2 = " « max(ul*u2,ul+u2) « ’\n’;

Здесь функция инстанцируется для типа unsigned.
Вместо ключевого слова typename в этом контексте можно также использо­
вать слово class, но мы не рекомендуем так поступать, поскольку выражение
typename лучше выражает предназначение обобщенных функций.

3.1.1. Инстанцирование
Так что же означает термин инстанцирование! Для необобщенной функции
компилятор читает ее определение, проверяет его на наличие ошибок и генери­
рует выполнимый код. Обрабатывая определение обобщенной функции, компи­
лятор может обнаружить только те ошибки, которые не зависят от параметров
шаблона, наподобие ошибок синтаксического анализа. Например, код

151

3.1. Шаблоны функций
template ctypename Т>
inline Т max (Т а, Т Ь)
{

if а > Ь // Ошибка!
return а;
else
return b;

}

не будет компилироваться, поскольку инструкция if без скобок не является кор­
ректным выражением грамматики C++.
Однако большинство ошибок зависят от подставляемых типов. Например, сле­
дующая реализация может компилироваться:
template ctypename Т>
inline Т max(T х, Т у)
{
return х < у ? у.value ;
}

х.value;

Но мы не можем вызвать ее ни с каким встроенным типом наподобие int или
double; шаблонная функция может и не предназначаться для встроенных типов
и вполне корректно работать с конкретными аргументами типов, для которых она
и была написана.
Сама по себе компиляция шаблона функции не генерирует никакого машинно­
го кода. Это происходит только тогда, когда мы вызываем такую функцию, — при
этом происходит инстанцирование шаблона функции. Только тогда компилятор
выполняет полную проверку корректности обобщенной функции для указанных
аргументов типа. В наших предыдущих примерах мы видели, что функция max
может быть инстанцирована с int и double.
До сих пор мы видели неявное инстанцирование: шаблон инстанцируется, ког­
да существует вызов и параметр типа выводится из переданных аргументов. Од­
нако можно и явно объявить тип, который подставляется вместо параметра шаб­
лона, например
std::cout « max(8.1, 9.3) « ’\n’;

Здесь шаблон инстанцируется явно с указанным типом. В наиболее явной фор­
ме мы выполняем инстанцирование без вызова функции:
template short max (short,

short);

Это может быть полезно, когда мы генерируем объектные файлы (раз­
дел 7.2.1.3) и хотим гарантировать присутствие в них определенных экземпляров,
независимо от наличия вызовов функций в компилируемом модуле.
Определение 3.1. Для краткости мы будем называть инстанцирование с выво­
дом типа неявным инстанцированием, а инстанцирование с явным объявлением
типа — явным инстанцированием.

Обобщенное программирование

152

По нашему опыту неявное инстанцирование в большинстве случаев работа­
ет так, как от него ожидается. Явное указание типа инстанцирования в основном
необходимо для того, чтобы избегать неоднозначностей и для специального ис­
пользования наподобие std: : forward (раздел 3.1.2.4). Для более глубокого по­
нимания шаблонов очень полезно знать, как компилятор выполняет подстановку
типов.

3.1.2. Вывод типа параметров
■=> c++ll/template_type_deduction. срр

В этом разделе мы подробнее рассмотрим выполнение подстановки шаблон­
ных параметров в зависимости от того, как передаются аргументы: по значению
или как ссылки на lvalue или rvalue. Это знание еще более важно, когда перемен­
ные объявляются с указанием автоматического типа с помощью ключевого слова
auto, как показано в разделе 3.4.1. Однако правила подстановки в случае пара­
метров функций более интуитивно понятны по сравнению с переменными auto,
а потому мы рассмотрим их здесь.
3.1.2.1. Параметры, передаваемые по значению

В предыдущем примере мы использовали параметр типа Т непосредственно
как параметр функции в max:
template ctypename Т>
Т max (Т а, Т Ь) ;

Как и параметр любой другой функции, параметр функции шаблонов может
иметь квалификаторы константности и ссылки:
template
Т max(const Т& a, const Т& Ь);

Давайте (без потери общности) запишем унарную void-функцию f:
template
void f(FPara p);

Здесь FPara содержит TPara. Когда мы вызываем f (arg), компилятор должен
вывести тип TPara, такой, чтобы параметр р мог быть инициализирован значе­
нием arg. В двух словах это вся история. Но чтобы лучше прочувствовать этот
момент, давайте рассмотрим несколько случаев. Простейший с синтаксической
точки зрения случай — равенство TPara и FPara:
template
void fl(TPara p);

Это означает, что параметр функции является локальной копией аргумента. Мы
вызываем fl с литералом int, переменной типа int и изменяемой и константной
ссылками на int:

3.1. Шаблоны функций

153

template
void fl(TPara p) {}
int main ()
{
int i= 0;
int& j= i;
const int& k= i;

fl(3);
fl (i) ;
fl (j) ;
fl (k) ;
}

Во всех четырех инстанцированиях вместо TPara подставляется int, так что
типом параметра р функции также является int. Если бы TPara был заменен int&
или const int&, аргументы также могли бы быть переданы. Но тогда не было бы
семантики передачи аргумента по значению, поскольку модификация р влияла
бы на аргумент функции (например, j). Таким образом, когда параметр функции
является параметром типа без квалификации, TPara становится типом аргумен­
та, в котором удалены все квалификаторы. Эта шаблонная функция принимает
все аргументы, лишь бы их типы были копируемыми. Например, у unique_ptr
копирующий конструктор удален, так что этот тип может быть передан в данную
функцию только как rvalue:
unique_ptr up;
// fl(up);
// Ошибка: нет копирующего конструктора

fl(move(up));

// OK: используется перемещающий конструктор

3.1.2.2. Передача ссылки на lvalue

Чтобы функция действительно принимала любой аргумент, можно использо­
вать в качестве параметра ссылку на константу:
template
void f2(const TPara& p) {}

TPara вновь представляет собой тип аргумента с отброшенными квалификато­
рами. Таким образом, р представляет собой константную ссылку на неквалифици­
рованный тип аргумента, так что мы не можем изменять р.
Более интересным случаем является передача в качестве параметра изменяе­
мой ссылки:
template
void f3(TPara & p) {}

Обобщенное программирование

154

Эта функция отвергает все литералы и временные значения, поскольку на них
невозможно ссылаться1. Это же можно перефразировать с точки зрения подста­
новки типов: временные значения отвергаются, поскольку для ТРага не сущест­
вует такого типа, что ТРага& становится int&& (мы вернемся к этому вопросу в
разделе 3.1.2.3).
Когда мы переходим к обычным переменным типа int наподобие i, ТРага
заменяется int, чтобы р имел тип int& и ссылался на i. Та же подстановка может
наблюдаться и при передаче изменяемой ссылочной переменной, такой как j. Что
же произойдет, когда мы передадим const int или const int& наподобие k?
Можно ли сопоставить этот тип с ТРагаб? Да, можно, если ТРага замещается на
const int. Соответственно, тип р в этом случае является const int&. Таким
образом, тип шаблона ТРага& не ограничивает аргументы для изменяемых ссы­
лок. Шаблон может соответствовать константной ссылке, однако, если функция
изменяет р, позже инстанцирование окажется неудачным.
3.1.2.3 Передаваемые ссылки

| С-ьч-111

В разделе 2.3.5.1 мы ввели ссылки на rvalue, которые принимают в качестве
аргументов только rvalue. Ссылки на rvalue с параметром типа вида Т&& прини­
мают также и lvalue. По этой причине Скотт Мейерс придумал для них термин
универсальные ссылки. Мы же будем придерживаться стандартного термина пере­
даваемые ссылки (forward reference). Мы покажем, почему они могут принимать
как rvalue, так и lvalue. С этой целью рассмотрим подстановку типа в следующей
унарной функции:
template
void f4(ТРага && p) {}

Когда мы передаем в эту функцию rvalue, например
f4 (3);
f4 (move(i));
f 4 (move (up)) ;

вместо ТРага подставляется неквалифицированный тип аргумента (здесь — int
и unique_ptr), и типом р является соответствующая ссылка на rvalue.
Когда мы вызываем f4 с передачей ей lvalue, такого как i или j, компилятор
принимает эти аргументы как параметры шаблона, являющиеся ссылками на
rvalue. Вместо параметра типа ТРага подставляется int&, и он же является ти­
пом р. Как такое возможно? Объяснение содержится в табл. 3.1, в которой показа­
но, как будут свернуты ссылки на ссылки.

1 Формально ни литералы, ни временные значения по той же причине неприемлемы как пара­
метры, представляющие собой константные ссылки, но язык делает исключение для удобства про­
граммистов.

155

3.1, Шаблоны функций

Таблица 3.1. Свертывание ссылок
&

т&
Т&&

т&
т&

т&
Т&&

Подытожив информацию в табл. 3.1, можно сказать, что ссылки сворачиваются
в ссылку на lvalue, если хотя бы одна из них является ссылкой на lvalue (т.е. при­
мерно (очень примерно!) можно сказать, что после свертывания остается мини­
мальное количество амперсандов). Это объясняет обработку значений lvalue в f 4.
Вместо TPara подставляется int&, и ссылка на rvalue для него тоже оказывается
int&.
Причиной, по которой нешаблонные ссылки на rvalue не принимают значения
lvalue, является отсутствие подстановки типа. Единственной причиной, по которой
параметр функции может быть lvalue, является то, что ссылка на lvalue вводится с
помощью подстановки. Без этой подстановки ссылка на lvalue не принималась бы,
и ссылки бы не сворачивались.
Более подробный и драматичный дедуктивный рассказ представлен в [32, с. 2347 и 165-219 русского издания].

3.1.2.4. Прямая передача

|С++11

Мы уже видели, что lvalue-значения могут быть превращены в rvalue с помо­
щью move (раздел 2.3.5.4). Сейчас мы хотим преобразовывать их условно. Пара­
метр прямой ссылки принимает как rvalue-, так и lvalue-аргументы, которые пере­
даются с помощью lvalue- и rvalue-ссылок соответственно. Передавая ссылочный
параметр в другую функцию, мы хотим, чтобы ссылка на lvalue передавалась как
lvalue, а ссылка на rvalue — как rvalue. Однако ссылки сами по себе в обоих слу­
чаях являются lvalue (так как они имеют имена). Мы могли бы выполнить приве­
дение к rvalue с помощью move, но при этом оно будет применяться и к ссылкам
на lvalue.
Поэтому нам нужно условное приведение. Оно достигается путем применения
std: : forward. Эта функция преобразует ссылку на rvalue в rvalue, но lvalue ос­
тавляет нетронутым, forward следует инстанцировать с (неквалифицированным)
параметром типа, например
template ctypename TPara>
void f5(TPara && p)
{
f4(forward(p));
}

Аргумент функции f5 передается функции f4 с той же категорией значения.
Все, что было передано функции f 5 как lvalue, передается как lvalue функции f 4;
аналогично передаются и ссылки на rvalue. Подобно move, forward представ­
ляет собой чистое приведение и не генерирует ни одной машинной команды.

Обобщенное программирование

156

Программисты формулируют это как парадокс: move ничего не перемещает, а
forward ничего не передает. Они просто приводят свои аргументы к такому виду,
чтобы те можно было перемещать или передавать.

3.1.3. Работа с ошибками в шаблонах
Вернемся к нашему примеру с max, который работает для всех числовых ти­
пов. Но что будет с типами, для которых нет оператора operator>, например с
std: :complex? Давайте попробуем скомпилировать следующий фрагмент2:
std::complex z(3,2), с (4,8);
std::cout « "Максимум от с и z = " « ::max(c,z) « ’\n’;

Попытка компиляции заканчивается сообщением об ошибке наподобие следу­
ющего:
Error: no match for "operator>" in "a > b"3

Что происходит, когда наша шаблонная функция вызывает другую шаблон­
ную функцию, которая, в свою очередь, вызывает еще одну... и так далее? Эти
функции точно так же только анализируются, а полная проверка откладывается
до инстанцирования. Давайте взглянем на следующую программу:
int main

()

{

vector v;
sort (v.begin (), v.endO);
}

Если не вдаваться в подробности, проблема при этом остается той же, что и
раньше. Мы не можем сравнивать комплексные числа и, таким образом, мы не в
состоянии сортировать массивы из этих чисел. В этот раз отсутствие сравнения
обнаруживается в косвенно вызываемой функции, и компилятор предоставляет
нам всю информацию о вызове, включающую стек выполнения, так, чтобы мы
могли отследить ошибку. Пожалуйста, попробуйте скомпилировать этот пример
разными компиляторами и посмотрите, сможете ли вы извлечь из сообщения об
ошибке какую-то осмысленную информацию.
Если вы получите такое длинное сообщение об ошибке4, не паникуйте! Сначала
посмотрите на сообщение об ошибке и выберите из него то, что полезно для вас,
например, отсутствие оператора > или что что-то нельзя присваивать, или что в
наличии есть const, которого не должно бы быть. Затем найдите в стеке вызовов
наиболее глубоко вложенный код, который все еще является частью вашей про­
граммы, т.е. место, где вы вызываете шаблонную функцию из стандартной или
2 Два двоеточия перед именем max позволяют избежать неоднозначности с функцией max из
стандартной библиотеки, которую некоторые компиляторы включают неявно (например, д++).

3 Не найден соответствующий оператор > в выражении а>Ь.
4 Самое длинное сообщение об ошибке, о котором доводилось слышать автору, было длиной
около 18 Мбайт, что соответствует примерно 9000 страниц текста.

157

3.1. Шаблоны функций

сторонней библиотеки. Обратите особое внимание на этот код и несколько пред­
шествующих ему строк, потому что, скорее всего, место, где произошла ошибка,
находится именно там (так говорит нам наш опыт). После этого спросите себя, не
отсутствует ли у типа аргумента шаблонной функции необходимый оператор или
функция-член, о которой говорится в сообщении об ошибке?
Не позвольте запугать себя до клятвы больше никогда не использовать шабло­
ны. В большинстве случаев проблема гораздо проще, чем можно решить, глядя на
бесконечное сообщение об ошибке. Исходя из нашего опыта, большинство оши­
бок в шаблонных функциях можно найти быстрее, чем ошибки времени выпол­
нения, — при небольшой тренировке.

3.1.4. Смешение типов
Следующий вопрос, на который мы пока что не ответили, — что произойдет
с нашей функцией шах, если мы используем два различных типа в качестве ее
аргументов?
unsigned ul = 2;
int i = 3;
std::cout « ”max(ul,i) = ” « max(ul,i)

« ’\n’;

Компилятор сообщит (на удивление кратко) что-то вроде
Error: no match for function call max(unsigned int&, int)5

Действительно, при написании мы предположили, что оба типа одинаковы.
Но подождите, разве С-1-4- не преобразует неявно аргументы, когда не существует
точного совпадения? Да, он это делает, но не для аргументов шаблона. Механизм
шаблонов должен обеспечивать достаточную гибкость на уровне типов. Кроме
того, сочетание инстанцирования шаблона с неявным преобразованием обладает
высокими возможностями для неоднозначности.
Пока что ничего хорошего. Может, мы напишем шаблон функции с двумя
параметрами шаблона? Конечно, мы можем это сделать. Но это создаст новые
проблемы. Каким должен быть тип возвращаемого значения этого шаблона? Есть
и другие варианты. Мы могли бы добавить в качестве перегрузок нешаблонные
функции наподобие
int inline max(int a, int b)

{ return a > b ? a : b;

}

Такая функция может быть вызвана со смешанными типами, и аргумент типа
unsigned будет неявно преобразован в int. Но что произойдет, если мы также
добавим еще одну перегрузку функции для unsigned?
int max( unsigned a, unsigned b)

{ return a > b ? a : b;

}

Будет ли int преобразован в unsigned или наоборот? Компилятор не будет
знать, как поступить, и будет жаловаться на неоднозначность ситуации.
5 Не найдено соответствие для вызова функции max (unsigned int&, int).

Обобщенное программирование

158

В любом случае добавление нешаблонных перегрузок к реализации шаблона
далеко от элегантности или продуктивности. Так что мы убираем все не шаблон­
ные перегрузки и смотрим, что же мы можем сделать в вызове функции. Можно,
например, явно преобразовать тип одного аргумента в тип другого аргумента:
unsigned ul= 2;
int i= 3;
std::cout « ”max(ul,i)

= " « max(int(ul),i) « '\n’;

Теперь max вызывается с двумя значениями типа int. Еще один вариант —
явно указать тип шаблона в вызове функции:
std: :cout « ”max(ul,i) = ’’ « max(ul, i) « ’\n’;

Тогда оба параметра являются int, и экземпляр шаблона функции может быть
вызван, когда оба аргумента либо имеют тип int, либо неявно преобразуемы в int.
После всех этих не слишком приятных подробностей, связанных с шаблонами,
пришло время сообщить и кое-что хорошее: шаблонные функции выполняются
так же эффективно, как и их нешаблонные коллеги! Дело в том, что C++ генери­
рует новый код для каждого типа или сочетания типов, с которыми вызывается
функция. В отличие от этого подхода Java компилирует шаблоны только один раз
и выполняет их для различных типов, преобразуя последние в соответствующие
типы. Это приводит к более быстрой компиляции и более коротким выполнимым
файлам, но и к большему времени выполнения.
Еще одной ценой, которую придется заплатить за скорость выполнения шаб­
лонов, является то, что у нас будут выполнимые файлы большего размера из-за
множественных инстанцирований для каждого типа (или комбинации типов).
В экстремальных (и редких) случаях большие бинарные файлы могут привести к
более медленному выполнению, когда более быстрая память6 заполняется команда­
ми, а данные должны загружаться из более медленной памяти и сохраняться в ней.
Однако на практике количество экземпляров функции таким большим не бы­
вает, так что имеет значение только то, что большие функции не встраиваются.
Для встроенных функций бинарный код в любом случае вставляется непосред­
ственно в место вызова функции в выполнимом файле, поэтому влияние шаблон­
ных и нешаблонных функций на длину выполнимого файла оказывается одина­
ковым.

3.1.5. Унифицированная инициализация

С++11

Унифицированная инициализация (из раздела 2.3.4) работает и с шаблонами.
Однако в очень редких случаях пропуск фигурных скобок может привести к не­
которому удивительному поведению. Если вы любознательный человек (или если
уже столкнулись с сюрпризами), обратитесь к приложению А, “Скучные детали”,
раздел А.6.1.

6 Кеши L2 и L3 обычно разделяются между данными и командами.

3.2. Пространства имен и поиск функций

3.1.6. Автоматический возвращаемый тип

159

| С++14

В C++11 в язык введены лямбда-выражения с автоматическим возвращае­
мым типом, в то время как для функций его применение остается запрещенным.
В C++14 мы можем позволить компилятору вывести возвращаемый тип:
template
inline auto max(T a, U b)
{
return a > b ? a : b;
}

Тип возвращаемого значения выводится из выражения в операторе return
таким же образом, как параметры шаблонов функций выводятся из аргументов.
Если функция содержит несколько операторов return, их выводимые типы долж­
ны быть одинаковы. Иногда в шаблонных библиотеках простые функции имеют
довольно длительные объявления возвращаемых типов (которые могут быть даже
длиннее, чем тело функции), и то, что их записи можно избежать, — это большое
облегчение для программиста.

3.2. Пространства имен и поиск функций
Пространства имен не являются подтемой обобщенного программирования
(на самом деле это независимые темы). Однако их роль становится более важной
при наличии шаблонов функций, так что это место в книге вполне подходит для
того, чтобы о них поговорить.

3.2.1. Пространства имен
Основной причиной появления пространств имен стало то, что распростра­
ненные имена, такие как min, max или abs, могут быть определены в различных
контекстах и потому являются неоднозначными. Даже имена, которые в настоя­
щий момент, при реализации класса или функции, являются уникальными, мо­
гут привести к коллизии позже, когда будет подключено больше библиотек, или
при развитии подключенной библиотеки. Например, в реализации графического
пользовательского интерфейса обычно имеется класс с именем window, и такой
же класс может встретиться в библиотеке статистики. Их можно различить с по­
мощью пространств имен:
namespace GUI {
class window;
}

namespace statistics {
class window;

}

Обобщенное программирование

160

Одна из возможностей разрешения конфликтов имен состоит в применении
иных имен, таких как my_abs или library_name_abs. Так в действительности
и поступают программисты на С. Основные библиотеки обычно используют ко­
роткие имена, пользовательские библиотеки — имена подлиннее, а внутренние
реализации, связанные с операционной системой, обычно начинаются с симво­
ла подчеркивания, _. Это уменьшает вероятность конфликтов, но недостаточно.
Пространства имен являются очень важными, когда мы пишем собственные клас­
сы, и еще важнее, когда они используются в шаблонах функций. Они позволяют
нам иерархически структурировать имена в нашем программном обеспечении.
Пространства имен предотвращают конфликты имен и обеспечивают управление
доступом к именам функций и классов.
Пространства имен подобны областям видимости, т.е. мы можем видеть имена
в охватывающих пространствах имен:
struct global {};
namespace cl {
struct clc {};
namespace c2 {
struct c2c
struct cc {

{};

global x;
clc
y;
c2c
z;
};
} // namespace c2
} // namespace cl

Имена, которые переопределяются во внутреннем пространстве имен, скрыва­
ют имена из внешних пространств имен. В отличие от сокрытия в блоках, мы все
равно можем обращаться к ним, используя квалификацию пространства имен:
struct same {};
namespace cl {
struct same {};
namespace c2 {
struct same {};
struct csame {
: : same x;
cl::same y;
same
z;
};
} // namespace c2
} // namespace cl

Как вы уже догадались, : : same ссылается на тип из глобального пространства
имен, a cl: : same — на имя в cl. Переменная-член z имеет тип cl: : с2 : : same,
поскольку внутреннее имя скрывает наружные. Пространства имен просматрива­
ются изнутри наружу. Если мы добавим пространство имен cl в с2, оно будет
скрывать внешнее пространство имен с тем же именем, и тип у станет неверен:

3.2. Пространства имен и поиск функций

161

struct same {};
namespace cl {
struct same {};
namespace c2 {
struct same {};
namespace cl {} 11 Скрывает ::cl
struct csame {
::same x;
cl::same у; // Ошибка: тип cl::c2::cl::same не определен
same
z;
};
} // namespace c2
} // namespace cl

Здесь cl: :same существует в глобальном пространстве имен, но поскольку
пространство имен cl скрыто пространством имен cl: : с2: : cl, мы не можем к
нему обращаться. Мы могли бы обнаружить аналогичное сокрытие, если бы оп­
ределили класс с именем cl в пространстве имен с2. Мы можем избежать сокры­
тия и более явно указать тип у, добавив двойное двоеточие перед пространством
имен:
struct csame {
::cl::same у;

// Такое имя уникально

};

Так становится ясно, что мы имеем в виду имя cl в глобальном пространстве
имен, а не какое-то иное имя cl. Имена часто необходимых функций или классов
можно импортировать с использованием объявления using:
void fun (...)
{

using cl::c2::cc;
cc x;
cc y;
}

Эта объявление работает в функциях и пространствах имен, но не в классах
(где оно может конфликтовать с другим объявлением using). Импорт имен в за­
головочных файлах существенно увеличивает опасность конфликтов имен, пото­
му что после этого имя становится видимым во всех последующих файлах еди­
ницы компиляции. Применение using в функции (даже в заголовочных файлах)
менее критично, поскольку импортированное имя является видимым только до
конца функции.
Аналогично можно импортировать пространство имен полностью:
void fun (...)
{

using namespace cl::c2;
cc x;

Обобщенное программирование

162

сс у;
}

Как и ранее, это можно делать внутри функции или другого пространства
имен, но не в области видимости класса. Инструкция
using namespace std;

часто является первой в функции main или даже первой после директив включе­
ния заголовочных файлов. Импорт std в глобальное пространство имен чревато
потенциальным конфликтом имен, например, когда мы также определяем класс с
именем vector (в глобальном пространстве имен). Но действительно проблема­
тичным является использование директивы using в заголовочных файлах.
Если пространство имен слишком длинное, в особенности при наличии вло­
женных пространств имен, его можно переименовать с помощью псевдонима про­
странства имен:
namespace Iname = long_namespace_name;
namespace nested = long_namespace_name: :yet_another_name: : nested;

Как и ранее, это следует делать в подходящей области видимости.

3.2.2. Поиск, зависящий от аргумента
Поиск, зависящий от аргумента (Argument-Dependent Lookup — ADL), расши­
ряет поиск имен функций в пространства имен их аргументов — но не в их ро­
дительские пространства имен. Это избавляет нас от необходимости подробной
квалификации пространств имен для функций. Скажем, мы пишем научную биб­
лиотеку в скромном пространстве имен rocketscience:
namespace rocketscience {
struct matrix {};
void initialize(matrix& A) {/*...*/}
matrix operator + (const matrix& A, const matrix& B)
{
matrix C;
initialize(С); // He квалифицировано, то же пространство имен
add (А, В, С) ;
return С;
}
}

Каждый раз при использовании функции initialize мы можемопустить
квалификацию всех классов в пространстве имен rocketscience:
int main

()

{

rocketscience::matrix А, В, С, D;
rocketscience::initialize(В); // Квалифицированное имя
initialize(С);

// Полагаемся на ADL

163

3.2. Пространства имен и поиск функций

chez_herber t::matrix Е, F, G;
rocketscience::initialize(Е); // Необходима квалификация
// Ошибка: initialize не найдена

initialize(G);
}

Операторы также являются субъектом ADL:
А= В + С + D;

Представим себе предыдущее выражение без ADL:
А= rocketscience: :operator +(rocketscience::operator +(B,C), D);

He менее уродливыми и даже более громоздкими оказались бы инструкции
потокового ввода-вывода, если бы они должны были квалифицироваться имена­
ми пространств имен. Поскольку пользовательский код не должен находиться в
пространстве имен std:operator« для класса предпочтительнее определять в
пространстве имен этого класса. Это позволит ADL найти правильную перегрузку
для каждого типа, например
std: :cout « А « Е « В « F « std: :endl;

Без ADL нам пришлось бы квалифицировать пространство имен каждого опе­
ратора с использованием длинной записи с ключевым словом operator. Это при­
вело бы к следующей записи предыдущего выражения:
std: :operator« (chez_herbert: :operator« (
rocketscience::operator
double one_norm(const Vectors x)

{

...

}

}

}

}

Механизм ADL выполняет поиск функций только в пространствах имен объяв­
лений типов аргументов, но не в их родительских пространствах имен:
namespace rocketscience {

namespace vec {
struct sparse_vector {};
struct dense_vector {};
struct upper_vector {};

}
template ctypename Vector>
double one_norm(const Vectors x)

{

...

}

}
int main()
{
rocketscience::vec::upper_vector x;
double norm_x = one_nonn(x); // Ошибка: ADL не находит
}

При импорте имени в другое пространство имен функция в этом пространстве
имен не подлежит ADL:
namespace rocketscience {

using vec::upper_yector;
template ctypename Vector>
double one_norm(const Vectors x)

{

...

}

}

int main()
{
rocketscience::upper_vector x;
double norm_x = one_norm(x); // Ошибка: ADL не находит
}

Выбор правильной перегрузки, основанный только на применении ADL, имеет
свои ограничения. Используя сторонние библиотеки, мы можем найти функции

165

3.2. Пространства имен и поиск функций

и операторы, которые мы также реализовали в нашем пространстве имен. Такая
неоднозначность может быть уменьшена (но не полностью устранена) с использо­
ванием только одних функций вместо целых пространств шмен.
Вероятность неоднозначности растет с применением функций с несколькими
аргументами, в особенности если параметры берутся из разных пространств имен,
например
namespace rocketscience {
namespace mat {

template
Matrix operator *(const Scalars a, const Matrixs A)

{

...

}

{

...

}

{

...

}

}
namespace vec {

template
Vector operator *(const Scalars a, const Vectors x)
template
Vector operator *(const Matrixs A, const Vectors x)

}
}

int main

(int argc, char * argv [])

{
rocketscience::mat::upper_matrix A;
rocketscience::vec::upper_vector x,y;
y= A * x;
// Какая перегрузка должна быть выбрана?
}

Намерение здесь очевидно. По крайней мере для читающего исход­
ный текст человека. Для компилятора — куда менее. Тип А определяется в
rocketscience: :mat, ах — в rocketscience: :vec, так что operator* рас­
сматривается в обоих пространствах имен. Таким образом, доступны все три шаб­
лона, и ни один из них не соответствует вызову лучше, чем другие (хотя, вероят­
но, корректно скомпилируется только один).
К сожалению, явное инстанцирование шаблона не работает с ADL. Когда ар­
гументы шаблона объявляются в вызове функции явно, поиск имени функции в
пространствах имен аргументов не выполняется7.
Какая из перегрузок функции вызывается, зависит от рассмотренных к насто­
ящему моменту правил
• вложения и квалификации пространств имен;
• сокрытия имен;
7 Проблема в том, что ADL при компиляции осуществляется слишком поздно, и открывающая
угловая скобка уже неверно трактуется как знак "меньше”. Для преодоления этой проблемы функция
должна быть сделана видимой с помощью квалификации пространства имен или импорта с помо­
щью using (подробнее — в разделе 14.8.1.8 стандарта).

166

Обобщенное программирование

• ADL;

• разрешения перегрузки.
Следует хорошо понимать это нетривиальное взаимодействие для часто пере­
гружаемых функций, чтобы быть уверенным, что в написанном исходном тексте
нет никакой неоднозначности и выбирается именно та перегрузка, которая вам
нужна. Поэтому мы приводим некоторые примеры в разделе А.6.2 приложения А,
“Скучные детали”. Не стесняйтесь отложить рассмотрение этого вопроса до тех
пор, пока не столкнетесь с неожиданными разрешениями перегрузки или неод­
нозначностями при работе с большими базами кода.

3.2.3. Квалификация пространств имен или ADL
Многие программисты не хотят разбираться в сложных правилах выбора ком­
пилятором перегрузки или устранения неоднозначностей. Они указывают про­
странство имен вызываемой функции и точно знают, какая перегрузка функции
выбрана (в предположении, что перегрузки в этом пространстве имен при раз­
решении перегрузки не являются неоднозначными). Их нельзя в этом винить —
о поиске имен можно сказать многое, но не то, что это тривиальная тема.
Если мы планируем написать хорошее обобщенное программное обеспечение,
содержащее шаблоны функций и классов, инстанцируемые со многими типами,
мы должны рассмотреть ADL. Мы покажем это с помощью очень популярной
ошибки производительности (особенно в С++03), с которой сталкивались мно­
гие программисты. Стандартная библиотека содержит шаблон функции swap. Эта
функция меняет местами содержимое двух объектов одного и того же типа. Ста­
рая реализация по умолчанию использует копирование и временный объект:
template
inline void swap(T& x, T& у)
{
Т tmp (х); х = у; у = trap;

}

Этот способ работает для всех типов с копирующими конструктором и при­
сваиванием. Пока что все в порядке. Скажем, у нас есть два вектора, каждый из
которых содержит 1 Гбайт данных. Тогда при обмене с помощью реализации по
умолчанию нам нужно скопировать 3 Гбайта. Или поступить умнее: обменять
указатели, которые указывают на данные, и информацию о размере:
template
class vector
{

friend inline void swap (vector & x, vector & y)
{ std::swap(x.my_size,у.my_size); std::swap(x.data,y.data) ;

}

3.2. Пространства имен и поиск функций

167

private:
unsigned my_size;
Value
Mata;
};

Обратите внимание, что этот пример содержит функцию, являющуюся inline
и friend. Этот код объявляет свободную функцию, которая является другом клас­
са, в котором она содержится. Это очевидно короче, чем отдельное объявление
функции как friend и отдельное определение этой функции.
Предположим, что мы должны обменять данные типа параметра в некоторой
обобщенной функции:
template
inline void some_function(T& x, T& y,
{

const U& z,

int i)

// Может быть дорогим

std::swap(x,у);
}

Это безопасное решение, в котором используется стандартная функция swap,
работающая со всеми копируемыми типами. Но так мы копируем 3 Гбайта дан­
ных. Было бы гораздо быстрее и эффективнее использовать нашу реализацию,
которая включает только указатели. Это может быть достигнуто с помощью не­
большого изменения в обобщенном коде:
template ctypename T, typename U>
inline void some_function(T& x, T& y, const U& z,
{

int i)

using std::swap;
swap(x, у);

// Включает ADL

}

При такой реализации обе перегрузки swap являются кандидатами при раз­
решении, но используется та из них, которая находится в нашем классе, — по­
скольку типы ее аргументов более конкретны, чем в стандартной реализации.
В общем случае любая реализация для пользовательских типов более конкретна,
чем std:: swap. Фактически std:: swap по указанной причине уже перегружена
для стандартных контейнеров. Это общее правило.

Используйте using

Не квалифицируйте пространства имен для шаблонов функций, для которых мо­
гут существовать пользовательские перегрузки. Вместо этого делайте имена ви­
димыми и вызывайте функции неквалифицированными.

168

Обобщенное программирование

C++11 В качестве добавления к реализации swap по умолчанию: начиная с
C++11 по умолчанию используется перемещение значений между двумя
аргументами и временной переменной:
template
inline void swap(T& x, T& у)
{
T tmp (move (x)) ;
x = move (y);
у = move (trap) ;
}

В результате типы без пользовательской реализации swap могут быть обмене­
ны более эффективно, если они обеспечивают быстрые перемещающие конструк­
тор и присваивание. Копирование выполняется только для типов без пользова­
тельской реализации swap и поддержки перемещения.

3.3. Шаблоны классов
В предыдущем разделе описано применение шаблонов для создания обобщен­
ных функций. Шаблоны могут также использоваться для создания обобщенных
классов. Аналогично обобщенным функциям “шаблон класса” является правиль­
ным названием по стандарту, в то время как в повседневной жизни чаще исполь­
зуется название “класс шаблона” (или “шаблонный класс”). В этих классах типы
данных-членов могут быть параметризованы.
Это особенно полезно для классов контейнеров общего назначения — таких
как векторы, матрицы или списки. Мы могли бы также расширить класс complex
с помощью параметра типа. Однако мы уже провели так много времени с этим
классом, что более интересным кажется взглянуть на что-то другое.

3.3.1. Пример контейнера
*=> c++ll/vector_template.срр

Давайте, например, напишем обобщенный класс вектора, — в смысле линей­
ной алгебры, а не вектора STL. Сначала мы реализуем класс только с основными
операторами.
Листинг 3.1. Шаблонный класс vector
template

{
public:
explicit vector(int size)
: my_size(size), data(new T[my_size])
{}

169

Шаблоны классов

3.3.

vector ( const vector & that )
: my_size(that.my_size), data(new T[my_size])
{

std::copy(&that.data [0],&that.data[that.my_size]r &data[0]);
}

int size()

const { return my_size;

const T& operator[](int i)

}

const

{

check_index(i) ;
return data[i];
}
// ...
private:
int
my_size;
std::unique_ptr data;

};

Шаблонный класс, по сути, не отличается от нешаблонного класса. Имеется
только дополнительный параметр Т, служащий заполнителем для типа его элемен­
тов. У нас есть такие переменные-члены, как my_size, и функции-члены, такие
как size (), в которых никак не затрагивается параметр шаблона. Другие функ­
ции, такие как оператор квадратных скобок (индексации) или копирующий кон­
структор, параметризуются, но по-прежнему очень похожи на нешаблонные фун­
кции: везде, где ранее был указан тип double, мы помещаем параметр типа Т, как
для возвращаемых типов, так и для выделения памяти. Аналогично переменнаячлен data просто параметризована типом Т.
Параметры шаблонов могут иметь значения по умолчанию. Предположим, что
наш класс vector параметризует не только тип значения, но и ориентацию и мес­
тоположение:
struct
struct
struct
struct

row_major {};
col_major {};
heap {};
stack {};

// Просто для маркировки
// То же самое

template
class vector;

Аргументы такого вектора могут быть указаны полностью:
vector v;

Последний аргумент, равный его значению по умолчанию, может быть опущен:
vector v;

Обобщенное программирование

170

Как и в случае функций с аргументами по умолчанию, опущенными могут
быть только последние аргументы. Например, если второй аргумент имеет значе­
ние по умолчанию, а третий нет, то мы должны записать их все:
vector w;

Когда все параметры шаблона установлены равными значениям по умолчанию,
можно опустить их все. Однако по грамматическим причинам, которые здесь не
рассматриваются, угловые скобки все равно следует писать:
vector
х; // Ошибка: рассматривается как нешаблонный класс
vectorO у; // Выглядит странновато, но корректно

В отличие от аргументов функций по умолчанию, шаблонные значения по
умолчанию могут ссылаться на предыдущие параметры:
template

Это класс для двух значений, которые могут иметь различные типы. Если они
одинаковы, мы можем объявить только один тип:
paircint, float> pl; // Объект со значениями типа int и float
pair
р2; // Объект с двумя значениями типа int

Значение по умолчанию может быть даже выражением с участием предыду­
щих параметров, как мы увидим позже, в главе 5, “Метапрограммирование ”.

3.3.2. Проектирование унифицированных
интерфейсов классов и функций
c++03/accumulate_example.cpp

Когда мы пишем обобщенные классы и функции, мы можем задать себе во­
прос о курице и яйце: что же было раньше? Мы можем сначала писать шаблоны
функций, а затем адаптировать для них свои классы, реализуя соответствующие
методы. Мы можем также сначала спроектировать интерфейс своих классов, а за­
тем реализовывать обобщенные функции для работы с ними.
Ситуация немного изменяется, когда наши обобщенные функции должны
быть в состоянии обрабатывать встроенные типы или классы из стандартной биб­
лиотеки. Эти классы не могут быть изменены, и необходимо адаптировать наши
функции к их интерфейсу. Есть и другие варианты, которые мы рассмотрим поз­
же: специализация и метапрограммирование, которые обеспечивают поведение,
зависящее от типа.
В качестве примера используем функцию accumulate из стандартной библио­
теки шаблонов (см. раздел 4.1). Она была разработана в то время, когда програм­
мисты использовали указатели и обычные массивы куда более часто, чем сегодня.
Таким образом, создатели STL Алекс Степанов (Alex Stepanov) и Дэвид Мюссер
(David Musser) создали чрезвычайно универсальный интерфейс, который рабо­
тает с указателями и массивами, а также со всеми контейнерами их библиотеки.

3.3. Шаблоны классов

171

3.3.2.1. Истинное суммирование массива

Для того чтобы просуммировать элементы массива обобщенно, первое, что
приходит на ум, — вероятно, функция, принимающая адрес и размер массива:
template
Т sum(const Т* array, int n)
{
T sum (0);
for (int i = 0; i < n; ++i)
sum += array[i];
return sum;
}

Эта функция может быть вызвана и дать корректные результаты, как и ожидается:
int
ai[]= {2, 4, 7};
double di[]= {2., 4.5, 7.};
cout « "Сумма ai равна " « sum(ai,3) « '\n';
cout « "Сумма ad равна " « sum (ad, 3) « ’\n';

Однако может возникнуть вопрос, почему мы должны передавать размер мас­
сива? Разве компилятор не может вывести его для нас? В конце концов, он извес­
тен во время компиляции. Чтобы использовать вывод компилятора, мы использу­
ем для размера параметр шаблона и передадим массив по ссылке:
template // О параметрах, не являющихся
Т sum(const Т (&array)[N])
// типами, читайте в разделе 3.7
{

Т sum(0);
for(int i = 0; i < N; ++i)
sum += array[i];
return sum;
}

Синтаксис выглядит немного странно: нам нужны круглые скобки, чтобы объя­
вить ссылку на массив в отличие от массива ссылок. Эта функция может вызы­
ваться с одним аргументом:
cout « "Сумма ai равна " « sum(ai) « '\п’;
cout « "Сумма ad равна " « sum(ad) « ’\n’;

Теперь выводятся и тип, и размер. Это, в свою очередь, означает, что если мы
суммируем два массива одного размера, но с разным количеством элементов, то
функция будет создаваться дважды. Тем не менее это не должно сильно сказаться
на размере выполнимого файла, поскольку такие малые функции обычно оказы­
ваются в любом случае встраиваемыми.

3.3.2.2. Суммирование элементов списка

Список является простой структурой данных, элементы которой содержат зна­
чение и ссылку на следующий элемент (а иногда и на предыдущий). В стандарт­
ной библиотеке C++ шаблон класса std: :list представляет собой двусвязный

Обобщенное программирование

172

список (раздел 4.1.3.3), а список без ссылок на предыдущий элемент был введен
в С++11 как шаблон класса std: : forward_list. Здесь мы будем рассматривать
только прямые ссылки:
template
struct list_entry
(

list_entry(const T& value)
T value;
list_entry* next;

: value(value), next(nullptr)

{}

};

template
struct list
{
list() : first(nullptr), last(nullptr)
~list()

{}

{

while(first) {
list_entry *tmp = first->next;
delete first;
first = tmp;

}
}
void append ( const T& x)
{
last = (first ? last->next : first) = new list_entry(x);
}
list_entry *first, *last;
};

Эта реализация list на самом деле слишком минималистична и лаконична.
Имея этот интерфейс, мы можем создать небольшой список:
list 1;
1.append(2.Of); 1.append(4.Of); 1.append(7.Of);

Пожалуйста, не стесняйтесь обогатить приведенный код полезными методами,
такими как конструктор с initializer_list.
Функция суммирования для этого класса list выглядит незатейливо.
Листинг 3.2. Суммирование элементов списка
template
Т sum(const list& 1)

{

Т sum = 0;
for(auto entry = 1.first; entry ?= nullptr; entry = entry->next)
sum += entry->value;
return sum;
}

3.3. Шаблоны классов

173

Эта функция может быть вызвана обычным путем. Мы выделили детали, кото­
рые отличаются от деталей реализации соответствующей функции для массива.
3.3.2.3. Общности

Для создания общего интерфейса сначала необходимо спросить себя, насколь­
ко похожи эти две реализации sum? На первый взгляд, не очень похож:
• доступ к значениям выполняется по-разному;

• обход элементов организован по-разному;
• критерии завершения разные.
Однако на более абстрактном уровне обе функции выполняют одни и те же
задачи:

• обращение к данным;
• переход к следующему элементу;
• проверка окончания контейнера.
Разница между этими двумя реализациями в том, как эти задачи реализуются
с помощью данных интерфейсов типов. Таким образом, чтобы обеспечить одну
обобщенную функцию для обоих типов, необходимо создать общий интерфейс.
3.3.2.4. Альтернативное суммирование массива

В разделе 3.3.2.1 мы обращались к массиву в индексно-ориентированном сти­
ле, который не может быть применен к спискам, элементы которых произвольно
рассеяны в памяти — по крайней мере, не столь эффективно. Таким образом, мы
реализуем суммирование массива заново, в более последовательном стиле с по­
шаговым обходом. Мы сможем добиться этого путем приращения указателя до
тех пор, пока не достигнем конца массива. Первым адресом за пределами массива
является &а [п], или, более кратко с применением арифметики указателей, а+п.
На рис. 3.1 показано, что мы начинаем обход с адреса а и завершаем его по дости­
жении а+n. Таким образом, мы указываем диапазон записей как полуоткрытый
справа интервал адресов.
а •=> «=> с=> t=> с=> а+п

= czj

;

Рис, 3.1. Массив длиной п с началь­
ным и конечным указателями
Если программное обеспечение пишется для максимальной применимости, по­
луоткрытый справа интервал оказывается более универсальным, чем закрытый,

Обобщенное программирование

174

особенно для таких типов, как списки, в которых позиции элементов представле­
ны произвольно выделенными адресами в памяти. Суммирование по такому по­
луоткрытому интервалу может быть реализовано так, как показано в листинге 3.3.
Листинг 3.3. Суммирование элементов массива
template
inline Т accumulate_array(Т*

a, Т* a_end)

{

Т sum(0);
for(; а ’= a_end;
sum += *а;
return sum;

++а)

}

Используется эта функция следующим образом:
int main

(int argc, char * argv [])

{
int
ai[] = {2, 4, 7};
double ad[] = {2., 4.5, 7.};

cout « "sum(ai) = " « accumulate_array(ai, &ai[3]) « ’\n’;
cout « ’’sum(ad) = ’’ « accumulate_array (ad, ad+3)
« '\n';

Пара указателей, представляющих полуоткрытый справа интервал, является
диапазоном — очень важной концепцией в C++. Многие алгоритмы в стандар­
тной библиотеке реализованы для диапазонов объектов, подобных указателям,
в стиле, подобном accumulate array. Чтобы использовать такие функции для
новых контейнеров, нужно только предоставить этот указателеподобный интер­
фейс. В качестве примера мы покажем теперь, как можно адаптировать интерфейс
нашего списка.

3.3.2.5. Обобщенное суммирование
Две функции суммирования в листингах 3.2 и 3.3 выглядят совершенно разны­
ми, потому что они написаны для разных интерфейсов. Функционально же они
не столь уж и различаются.
В разделе 3.3.2.3 мы уже говорили, что реализации sum из разделов 3.3.2.1 и
3.3.2.2:
• обе проходят по последовательности от одного элемента до другого;

• обе обращаются к значению текущего элемента и добавляют его к sum;


обе выполняют проверку, не достигнут ли конец последовательности.

То же самое справедливо и для нашей пересмотренной реализации для массива
в разделе 3.3.2.4. Однако последняя использует интерфейс с более абстрактным
понятием инкрементного обхода последовательности. Как следствие это позволяет

175

Шаблоны классов

3.3.

применять его для другой последовательности, такой как список, если она предо­
ставляет этот последовательный интерфейс.
Гениальная идея Алекса Степанова (Alex Stepanov) и Дэвида Мюссера (David
Musser), реализованная в STL, заключается во введении общего интерфейса для
всех типов контейнеров и традиционных массивов. Этот интерфейс состоит из
обобщенных указателей, именуемых итераторами. Затем все алгоритмы реали­
зуются для этих итераторов. Мы обсудим этот вопрос более подробно в разде­
ле 4.1.2, а пока лишь бегло взглянем на это гениальное решение, чтобы иметь о
нем представление.
■=> c++03/accumulate_example.cpp

Сейчас нам нужен итератор для нашего списка, который обеспечивает необхо­
димую функциональность в стиле синтаксиса указателя, а именно:



обход последовательности с помощью +-hit;

• доступ к значениям с помощью *it;

• сравнение итераторов с помощью == или ! =.
Реализация такого итератора незамысловата:
template
struct list_iterator
{

using value_type = T;
list_iterator(list—entry* entry)

: entry(entry)

T& operator*() { return entry->value; }
const T& operator*() const { return entry->value;

list_iterator operator++()
{ entry = entry->next; return *this;

{}

}

}

bool operator !=(const list_iterator& other)
{ return entry != other.entry; }

const

list—entry* entry;
};

Для удобства добавим методы begin и end в наш list:
template ctypename T>
struct list
{
list—iterator begin()
list_iterator end()
}

{ return list_iterator(first); }
{ return list_iterator (0);

}

176

Обобщенное программирование

Класс list—iterator позволяет объединить листинги 3.2 и 3.3 в одну функ­
цию accumulate.
Листинг 3.4. Обобщенное суммирование
template
inline T accumulate(Iter it, Iter end, T init)
{
for (; it != end; ++it)
init += *it;
return init;
}

Эта обобщенная функция sum может использоваться как для массивов, так и
для списков в следующем виде:
cout « ’’Массив = ” « sum (а, а+10, 0.0) « ’ \п' ;
cout « ’’Список = ’’ « sum (1 .begin() ,1 .end() , 0) « ’ \n' ;

Как прежде, ключ к успеху состоит в том, чтобы найти правильную абстрак­
цию — итератор.
Реализация list—iterator является также хорошей возможностью наконец от­
ветить на вопрос, почему они должны обладать возможностью преинкремента, а не
постинкремента. Мы уже видели, что префиксный инкремент обновляет член entry
и возвращает ссылку на итератор. Постфиксный инкремент должен возвращать ста­
рое значение и увеличивать свое внутреннее состояние таким образом, чтобы при
очередном использовании итератора он указывал на следующую запись списка.
К сожалению, это может быть достигнуто, только если операция постинкремен­
та перед изменением данных-членов копирует весь итератор и возвращает копию:
template
struct list_iterator
(
list_iterator operator ++(int)

{
list_iterator tn^(*this);
p = p->next;
return tmp;
}

};

Чаще всего операцию инкремента мы вызываем только для того, чтобы перей­
ти к следующей записи, и не заботимся о значении, возвращаемом операцией.
В таком случае создание копии итератора, которая никогда не используется, —
пустая трата ресурсов. Хороший компилятор может оптимизировать подобные
избыточные операции, но нет никакого смысла полагаться на это. Заметим, что
забавной деталью определения постинкремента является фиктивный параметр
int, который присутствует только для того, чтобы отличать определение пост­
фиксного инкремента от префиксного.

177

3.4. Вывод и определение типа

3.4. Вывод и определение типа
Компиляторы C++ автоматически выводили типы для аргументов шаблонов
функций еще в С++03. Пусть f — шаблонная функция, и мы вызываем ее:
f

(g(x,y,z)+3*x)

В таком случае компилятор в состоянии вывести тип аргумента f.

3.4.1. Автоматический тип переменных

|с++и |

Для присваивания переменной результата выражения, подобного приведен­
ному выше, в С++03 нам нужно знать тип данного выражения. С другой сторо­
ны, если мы выполняем присваивание типу, к которому результат не приводим,
компилятор сообщает о несовместимости типов. Это показывает, что компилятор
знает тип выражения, и в C++11 он делится этим знанием с программистом.
Простейшее средство использования информации о типе в предыдущем при­
мере — это переменная автоматического (даже auto-матического) типа:
auto а = f(д(х,у,z)+3*х) ;

Это никак не меняет того факта, что C++ является строго типизированным
языком программирования. Тип auto отличается от динамических типов на дру­
гих языках, таких как Python. В Python присваивание переменной а может изме­
нить ее тип, меняя его на тип присваиваемого выражения. В C++11 переменная
получает тип, равный типу результата выражения, и этот тип никогда позже не
будет изменен. Таким образом, тип auto не является типом, который автомати­
чески приспосабливается ко всему, что присваивается переменной, а определяется
только один раз и до конца существования этой переменной.
Мы можем объявить несколько переменных auto в одной и той же инструк­
ции, если все они инициализируются выражениями одного и того же типа:
auto i = 2*7.5, j= std::sqrt(3.7); // ОК; обе имеюттип double
auto i = 2*4,
j= std::sqrt(3.7); // Ошибка:i — int, j —
double
autoi=2*4,
j; // Ошибка: j не инициализирована
auto v = g(x,y,z); // Результат result of g

Можно квалифицировать auto с помощью ключевого слова const и ссылок:
auto&

ri = i;

const

auto& cri = i;

auto&&

ur = g(x,y,z);

// Ссылка на i
// Константная ссылка на i
// Передаваемая ссылка на результат g

Вывод типа работает с переменными auto в точности так же, как вывод пара­
метров функции, описанный в разделе 3.1.2. Это означает, например, что перемен­
ная v не является ссылкой, даже если функция g возвращает ссылку. Аналогично
универсальная ссылка иг является ссылкой на rvalue или lvalue — в зависимости
от того, является ли результат функции g rvalue или lvalue (ссылкой).

178

Обобщенное программирование

3.4.2. Тип выражения

|с++ц

Еще одной новой возможностью в C++11 является decitype. Она похожа на
функцию, которая возвращает тип выражения. Если f в первом примере с auto
возвращает некоторое значение, мы могли бы также выразить его тип с помощью
decltype:
decltype(f(g(x,y,z)+3*x)) a = f(g(x,y,z)+3*x);

Очевидно, что такое объявление слишком громоздкое и не слишком полезное
в данном контексте.
Данная возможность очень важна там, где требуется указание явного типа —
прежде всего времени компиляции параметра шаблона для шаблонов классов. Мы
можем, например, объявить вектор, элементы которого могут хранить сумму эле­
ментов двух других векторов, например типа vl [ 0 ] +v2 [ 0 ]. Это позволяет ука­
зать тип return для суммы двух векторов различных типов:
template
auto operator +(const Vectorl& vl, const Vector2& v2)
-> vector;

Этот фрагмент кода демонстрирует еще одну новую возможность — заверша­
ющий тип возвращаемого значения. В C++11 мы по-прежнему обязаны объявить
тип возвращаемого с помощью инструкции return значения функции. Наличие
decltype позволяет удобно выразить его с использованием типов аргументов
функции. Таким образом можно переместить объявление типа return и размес­
тить его после аргументов.
Эти два вектора могут иметь различные типы, а результирующий вектор —
еще один, третий тип. С помощью выражения decltype (vl [0] +v2 [0]) мы вы­
водим, какой тип получается при сложении элементов обоих векторов. Этот тип и
будет типом элемента нашего результирующего вектора.
Интересным аспектом decltype является то, что он работает только на уровне
типа и не вычисляет выражение, переданное ему в качестве аргумента. Таким об­
разом, выражение из предыдущего примера не вызовет ошибку для пустых векто­
ров, поскольку значение vl [ 0 ] не вычисляется; определяется только его тип.
Две возможности — auto и decltype — различаются не только применением;
различаются также и выводы типов. В то время как auto следует правилам пара­
метров шаблонной функции и часто опускает ссылки и квалификаторы const,
decltype принимает тип выражения таким, какой он есть. Например, если функ­
ция f в нашем вступительном примере возвращает ссылку, то переменная а будет
ссылкой. Соответствующая переменная auto будет значением.
До тех пор, пока мы главным образом занимаемся встроенными типами,
мы вполне можем обойтись без автоматического вывода типа. Но используя
современное обобщенное программирование и метапрограммирование, можно
получить существенные преимущества от этих чрезвычайно мощных возмож­
ностей.

3.4. Вывод и определение типа

179

3.4.3. dedtype(auto)

|с++14|

Эта новая возможность закрывает промежуток между auto и decltype. При
использовании decltype (auto) можно объявлять переменные auto, которые
имеют тот же тип, который выводится decltype. Два следующих объявления
идентичны:
decltype(ехрг) v = ехрг; // Избыточно и многословно для длинных ехрг

decltype(auto) v = ехрг; // О, так гораздо лучше!

Первая инструкция слишком многословна, ведь мы дважды записываем вы­
ражение ехрг. И при любом его изменении мы должны не забывать обеспечить
идентичность этих двух выражений.
•=> c++14/value_range_vector .срр

Сохранение квалификации имеет важное значение для автоматических воз­
вращаемых типов. В качестве примера мы рассмотрим представление вектора, ко­
торое проверяет, находятся ли его значения в заданном диапазоне. Обращение к
элементу вектора выполняется с использованием оператора operator [ ], который
возвращает этот элемент после проверки на соответствие диапазону в точности с
теми же квалификаторами. Очевидно, что это работа для decltype (auto). Наш
пример реализации этого представления содержит только конструктор и оператор
доступа к элементу:
template ctypename Vector>
class value_range_vector
{

using value_type = typename Vector::value_type;
using size_type = typename Vector::size_type;
public:
value_range_vector(Vectors vref, value_type minv, value_type maxv)
: vref(vref), minv(minv), maxv(maxv)
{}

decltype(auto) operator!](size_type i)
{

decltype(auto) value = vref[i];
if (value < minv)
if (value > maxv)
return value;

throw too_small{};
throw too_large{};

}
private;
Vectors vref;
value_type minv, maxv;

};

Наш оператор доступа кеширует элемент из vref для проверки диапазона,
прежде чем возвратить его. Как тип временной переменной, так и тип возвраща­
емого значения выводятся с помощью decltype (auto). Чтобы проверить, что

Обобщенное программирование

180

элемент вектора возвращается с правильным типом, сохраним его в переменной
decltype (auto) и проверим его тип:
int main ()
{

using Vec = mtl::vector;
Vec v = {2.3, 8.1, 9.2};
value_range_vector w(v,
decltype(auto) val= w[1];

1.0,

10.0);

}

Типом val, как и требовалось, является doubles. В этом примере
decltype (auto) используется три раза: дважды — в реализации представления
и один раз — при тестировании. Если заменить его auto, тип val будет просто
double.

3.4.4. Определение типов

|С++п

Существует два варианта определения типов: typedef и using. Первый был
введен в С и существовал в C++ с самого начала. Это единственное его преиму­
щество — обратная совместимость8. При написании нового программного обес­
печения без необходимости компиляции с помощью компиляторов, не поддержи­
вающих С++11, мы настоятельно рекомендуем вам прислушаться к следующему
совету.
Совет
Используйте using вместо typedef.

Это более удобочитаемая и более мощная запись. В случае простых определе­
ний типов это просто вопрос порядка размещения слов:
typedef double value_type;

против
using value_type = double;

В объявлении using новое имя располагается слева, в то время как в typedef
оно находится справа. При объявлении массива имя нового типа является не край­
ней справа частью typedef, а тип делится на две части:
typedef

double dal[10];

При использовании же объявления using тип остается единым целым:
using da2 = double[10];

8

И это единственная причина, по которой в примерах в этой книге иногда используется

typedef.

3.4. Вывод и определение типа

181

Разница становится еще более выраженной для типов функций (указате­
лей), которые, мы надеемся, никогда не понадобятся вам в определениях типа,
std: -.function из раздела 4.4.2 является куда более гибкой альтернативой.
Но вернемся к объявлениям типа. Например, объявление функции от float и
int, которая возвращает float, имеет вид
typedef

float floatfuni(float,int);

против
using float_fun2 =

float(float,int);

Во всех этих примерах объявление using четко отделяет имя нового типа от
определения.
Кроме того, объявление using позволяет определять псевдонимы шаблонов. Это
определения с параметрами типов. Предположим, у нас есть шаблонный класс для
тензоров произвольного порядка с параметризуемым типом значений:
template
class tensor { ... };

Теперь мы можем ввести имена типов vector и matrix для тензоров первого и
второго порядка соответственно. Это невозможно сделать с помощью typedef, но
легко получается при использовании псеводнимов шаблонов с помощью using:
template
using vector = tensor c++ll/find_test3.cpp

В том же стиле мы можем написать обобщенную функцию для вывода интер­
вала, которая работает со всеми контейнерами STL. Давайте сделаем еще один
шаг: мы хотим обеспечить поддержку классических массивов. К сожалению, мас­
сивы не имеют членов begin и end. (По правде говоря, они не имеют никаких
членов вовсе.) С++11 пожалел их (и все контейнеры STL) и предоставил свобод­
ные функции с именами begin и end, которые позволяют нам быть еще более
обобщенными.
Листинг 4.2. Обобщенная функция для вывода закрытого интервала
struct value_not_found{};
struct value_not_found_twice{};
template
void print_interval(const Ranges r, const Values v,
std::ostreamS os = std::cout)
(
using std::begin; using std::end;
auto it = std::find(begin(r),end(r),v), it2 = it;
if (it == end(r))
throw value_not_found() ;
++it2;
auto past = std::find(it2, end(r), v);
if (past == end(r))
throw value_not_found_twice ();
++past;
for(; it != past; ++it)
os « *it « ' ’;
os « ’\n’;
}

4.1. Стандартная библиотека шаблонов
int main

235

()

{

std::list seq = {3, 4, 7, 9, 2, 5, 7,
print_interval(seq, 7);
int arrayf] = {3, 4, 7, 9, 2, 5, 7, 8};
std::stringstream ss;
print_interval(array, 7, ss);
std::cout « ss.strQ;

8};

}

Мы также параметризуем выходной поток, чтобы не ограничиваться
std: : cout. Пожалуйста, обратите внимание на гармоничную комбинацию стати­
ческого и динамического полиморфизма в аргументах функции: типы диапазона
г и значения v инстанцируются во время компиляции, в то время как оператор
вывода « для os выбирается во время выполнения в зависимости от типа, на
который в действительности ссылается os.
Здесь мы хотим также обратить ваше внимание на способ работы с простран­
ствами имен. Когда мы используем множество стандартных контейнеров и алго­
ритмов, можно просто объявить
using namespace std;

непосредственно после включения заголовочных файлов и больше не писать
std::. Это прекрасно работает для небольших программ. В крупных проектах мы
рано или поздно столкнемся с конфликтом имен. Исправление ситуации может
оказаться трудоемким и раздражающим делом (всегда лучше предотвратить не­
приятности, чем потом их устранять). Таким образом, следует импортировать как
можно меньше имен — в особенности в заголовочных файлах. Наша реализация
print_interval не полагается на предшествующий импорт имен и может быть
безопасно размещена в заголовочном файле. Даже внутри функции мы не импор­
тируем все пространство имен std, а ограничиваемся теми функциями, которые
мы действительно используем.
Обратите внимание, что мы не квалифицировали пространство имен в неко­
торых вызовах функций, например в std::begin (г). Это будет работать в при­
веденном выше примере, но приведет к неприятностям при работе с пользова­
тельскими типами, определяющими функцию begin в пространстве имен класса.
Комбинация using std: :begin и begin (г) гарантирует, что будет найдена
функция std: :begin. С другой стороны, пользовательская функция begin будет
найдена с помощью ADL (раздел 3.2.2) и будет соответствовать вызову лучше, чем
std::begin. То же самое справедливо и для end. Что касается функции find, то
здесь мы, напротив, не хотим вызывать возможную пользовательскую перегрузку
и хотим быть уверены, что будет вызвана функция из пространства имен std.
find_if обобщает find для поиска первого элемента, который удовлетворяет
общему критерию. Вместо сравнения с единственным значением этот алгоритм
вычисляет предикат — функцию, возвращающую значение типа bool. Пусть, на­
пример, мы ищем первую запись в list, которая больше 4 и меньше 7.

Библиотеки

236
bool check(int i) { return i>4&&i: ’.value « endl;

Весь С++-код является условно компилируемым: макрос__ cplusplus являет­
ся предопределенным во всех компиляторах C++. Его значение показывает, какой
стандарт поддерживает компилятор (в текущей трансляции). Показанный выше
класс может использоваться со списком инициализаторов:
simple jpoint pl= {3.0,

7.0};

Тем не менее он может быть скомпилирован и с помощью компилятора С (если
только какой-нибудь клоун не определит в коде на С макрос__ cplusplus). Биб­
лиотека CUDA реализует некоторые классы в таком стиле. Однако такие гибрид­
ные конструкции должны использоваться только тогда, когда это действительно
необходимо, чтобы избежать избыточных усилий на поддержку и сопровождение,
и избежать рисков несогласованности.
=> c++ll/memcpy_test.cpp

Старые обычные данные располагаются в памяти непрерывно и могут быть
скопированы как ^отформатированные данные, без вызова копирующего кон­
структора. Это делается с помощью традиционных функций memcpy и memmove.
Однако как ответственные программисты мы должны проверить с помощью
свойства is_trivially_copyable, можем ли мы и в самом деле вызывать эти
низкоуровневые функции копирования:
simplejpoint pl{3.0,7.1}, р2;
static assert (std: : is__trivially_copyableoint>: : value,
”simple_point не настолько прост, как вы думаете,
”и не может быть скопирован с помощью memcpy!”);
std::memcpy(&р2,&pl,sizeof(pl) );

"

К сожалению, это свойство типа было реализовано только в нескольких по­
следних компиляторах. Например, д++ 4.9 и clang 3.4 его не предоставляют (оно
появилось лишь в д++ 5.1 и clang 3.5).
Эта функция копирования в старом стиле должна использоваться только при
работе с С. В рамках программы на чистом C++ лучше полагаться на функцию
сору из STL:
сору(&х,

&х+1,

&у);

Этот способ работает независимо от размещения нашего класса в памяти, а
реализация сору использует memmove внутренне, когда типы позволяют это.

Библиотеки

260

Сч—1-141 C++14 добавляет несколько псевдонимов шаблонов наподобие
conditional_t

в качестве сокращения для
typename conditionaKB, Т, F>:: type

Аналогично enable_if_t является сокращением для типа у enable_if.

4.4. Утилиты

|с++п|

C++11 добавляет новые библиотеки, которые делают современный стиль про­
граммирования проще и элегантнее. Например, мы можем намного проще воз­
вращать несколько результатов, более гибко обращаться к функциям и функто­
рам и создавать контейнеры ссылок.

4.4.1. tuple

|с++и|

Когда функция вычисляет несколько результатов, их обычно возвращают через
изменяемые ссылочные аргументы. Предположим, что мы реализуем LU-разложение с выбором опорного элемента, принимающее матрицу А и возвращающее LUразложение и вектор перестановок р:
void lu(const matrix& A, matrixfi LU,

vectors p) { ... }

Мы могли бы также вернуть LU или р как результат функции и передать дру­
гой объект по ссылке. Такой смешанный подход еще более запутывающий.
=> c++ll/tuple_move_test.срр

Чтобы вернуть несколько результатов без реализации нового класса, мы можем
объединить их в кортеж. Кортеж tuple (из заголовочного файла ) отли­
чается от контейнера, допуская наличие элементов различных типов. В отличие
от большинства контейнеров, количество объектов в кортеже должно быть извес­
тно во время компиляции. С помощью кортежа мы можем вернуть оба результата
LU-разложения одновременно:
tuple lu (const matrix& A)
{

matrix LU(A);
vector p(n);

// ... некоторые вычисления
return tuple(LU,p);
}

Оператор return может быть упрощен благодаря вспомогательной функции
make_tuple с выводом типов параметров:

4.4.1.

261

tuple

tuple lu(const matrix& A)
{

return

make_tuple(LU,p);

}

make tuple особенно удобна в сочетании с переменными auto:
auto t= make_tuple (LU,p, 7.3,9,LU*p,2.0+9.0*i);

Функция, вызывающая нашу функцию lu, вероятно, извлечет матрицу и век­
тор из кортежа с помощью функции get:
tupleCmatrix,vector> t = lu (A) ;
matrix LU = get(t);
vector p = get(t);

Здесь также могут быть выведены все типы:
auto t = lu (А);
auto LU = get(t);
auto p = get(t);

Функция get принимает два аргумента: кортеж и позицию в нем. Последняя
является параметром времени компиляции — в противном случае тип результата
был бы неизвестен. Если используется слишком большой индекс, обнаруживается
ошибка времени компиляции:
auto t = lu(А) ;
auto am_i_stupid = get(t);

// Ошибка времени компиляции

| С++141 В C++14 обратиться к элементам кортежа можно также с использовани­
ем их типов (если это не вызывает неоднозначности):
auto t = lu(А);
auto LU = get(t);
auto p = get(t);

Теперь мы не обязаны больше запоминать внутренний порядок данных в кор­
теже. Кроме того, мы можем использовать функцию tie для разделения записей
в кортеже. Зачастую это оказывается более элегантным. В этом случае мы должны
объявить совместно используемые переменные заранее:
matrix LU;
vector р;

tie(LU,p) = lu(A);

На первый взгляд, tie выглядит довольно таинственно, но ее реализация
на удивление проста: она создает объект со ссылками на аргументы функции.
Присвоение кортежа этому объекту выполняет присваивания каждого члена кор­
тежа соответствующей ссылке.

Библиотеки

262

Реализация с применением tie имеет преимущество в производительнос­
ти над реализацией с помощью get. Когда мы передаем результат функции lu
непосредственно tie, он все еще является rvalue (не имеет имени), и мы можем
выполнить перемещение записей. При наличии промежуточной переменной он
становится lvalue (получает имя), и записи должны быть скопированы. Чтобы из­
бежать копирования, можно также перемещать элементы кортежа явно:
auto t = lu (А) ;
auto LU = get(move(t));
auto p = get (move (t));

Здесь мы становимся на довольно тонкий лед. В принципе объект считается
недействительным после применения к нему move. Он может находиться в любом
состоянии, лишь бы вызов деструктора не привел к аварийной ситуации. В нашем
примере мы читаем t заново после применения к нему move. В данной особой си­
туации это корректное действие, move превращает t в rvalue, и мы можем делать с
его данными все, что хотим. Но мы этого не делаем. Когда создается LU, мы заби­
раем только данные из нулевой записи кортежа и не трогаем запись с индексом 1.
И наоборот, мы забираем только данные из первой записи 1 кортежа t в перемен­
ную р и не обращаемся к просроченным данным в записи 0. Таким образом, две
указанные операции move относятся к полностью несвязанным данным. Тем не
менее несколько применений move к одному и тому же элементу данных очень
опасны и должны быть тщательно проанализированы (что мы и сделали).
После обсуждения эффективной обработки на стороне вызывающей функции
мы должны еще раз взглянуть на функцию lu.Получаемые в результате матрица
и вектор копируются в кортеж при возврате из функции. В операторе return мож­
но безопасно перенести все данные из функции6, так как они все равно подлежат
уничтожению при выходе из функции. Вот фрагмент исправленной функции:
tuple lu(const matrix& A)

{
return make_tuple (move (LU), move (p)) ;
}

Теперь, когда мы избежали копирования, реализация возвращает кортеж так
же эффективно, как и код с изменяемой ссылкой, по крайней мере когда результат
используется для инициализации переменных. Когда результат присваивается су­
ществующим переменным, мы все еще несем накладные расходы на выделение и
освобождение памяти.
Еще одним гетерогенным классом C++ является pair. Он уже был в С++03 и
продолжает находиться в стандарте языка, pair представляет собой эквивалент
tuple с двумя аргументами. Имеются преобразования одного к другому, так что
pair и двухаргументный tuple могут обмениваться данными один с другим и
6 Если только объект не появляется в кортеже дважды.

4.4.1. tuple

263

даже смешиваться в выражениях. Приведенные в этом разделе примеры могут
быть реализованы с использованием pair. Вместо get (t) мы могли бы напи­
сать t. first (и t. second — вместо get (t)).
=> c++ll/boost_fusion_example .срр

Дополнительные материалы. Библиотека Boost: ‘.Fusion предназначена
для объединения метапрограммирования с классическим (времени выполнения)
программированием. Используя эту библиотеку, мы можем написать код для об­
хода кортежей. Приведенная далее программа реализует обобщенный функтор
printer, который вызывается для каждого элемента кортежа t:
struct printer
{
template
void operator()(const T& x) const
{
std::cout « ’’Запись ” « x « std: :endl;
}
};
int main

()

{

auto t = std: :make__tuple (3, 7u, "Hallo ", std: : string ("Hi"),
std::complex (3,7));
boost::fusion::for_Mch(t,printer{});
}

Библиотека также предлагает более мощные функции для обхода и преобра­
зования гетерогенных композитов типов (boost: : fusion: :for_each, на наш
взгляд, — куда более полезная функция, чем std: :for_each). Когда функци­
ональность времени компиляции и времени выполнения взаимодействуют не­
тривиальным образом, библиотека Boost Fusion становится совершенно необхо­
димой.
Широчайшую функциональность в области метапрограммирования предлага­
ет библиотека Boost Meta-Programming Library (MPL) [22]. Библиотека реализует
большинство алгоритмов STL (раздел 4.1), а также предоставляет аналогичные
типы данных; например, vector и тар реализованы как контейнеры времени
компиляции. Особенно мощной комбинация MPL и Boost Fusion оказывается
тогда, когда функциональность времени компиляции и времени выполнения вза­
имодействуют нетривиальным образом. На момент написания этой книги име­
ется новая библиотека Напа [11], которая предназначена для вычислений време­
ни компиляции и времени выполнения с более функциональным подходом. Это
приводит к значительно более компактным программам с сильным акцентом на
возможности C++14.

Библиотеки

264

4.4.2. function

| C++11
=> c++ll/function_example.cpp

Шаблон класса function из заголовочного файла представляет
собой обобщенный указатель на функцию. Спецификация типа функции переда­
ется в качестве аргумента шаблона, как показано в следующем фрагменте кода:
double add(double х, double у)
{
return x + у;
}
int main ()
{
using bin_fun = function;
bin_fun f= &add;
cout « ”f(6,3) = " « f(6,3) « endl;

}

Функция-оболочка может хранить функциональные сущности разных видов
с одним и тем же возвращаемым типом и списком параметров7. Мы даже можем
построить контейнеры совместимых функциональных объектов:
vector functions;
functions.push_back(&add);

Когда функция передается в качестве аргумента, ее адрес получается автомати­
чески, так что оператор получения адреса & можно опустить:
functions.push_back(add);

Если функция объявлена как inline, ее код должен быть вставлен в контекст
вызова. Тем не менее каждая встраиваемая функция при необходимости также по­
лучает свой уникальный адрес, который может храниться как объект function:
inline double sub(double x, double y)
{
return x - y;
}
functions.push—back(sub);

Получение адреса вновь осуществляется неявно. В качестве объекта function
могут храниться и функторы:
struct mult {

double operator()(double x, double y) const { return x*y; }
};
functions.push—back(mult{});

Здесь мы создаем анонимный объект с помощью конструктора по умолчанию.
Шаблоны классов не являются типами, поэтому мы не можем создать их объекты:

7 Таким образом, они могут иметь разные сигнатуры, поскольку одинаковые имена функций не
требуются. Более того, одинаковость сигнатур не гарантирует одинаковых возвращаемых типов.

4.4.1.

265

tuple

template
struct power {
Value operator()(Value x, Value y) const { return pow(x,y);
};
functions.push_back (power()); // Ошибка

}

Мы можем создавать объекты только инстанцированных шаблонов:
functions.push_back(power{});

С другой стороны, можно создавать объекты классов, которые содержат шаб­
лоны функций:
struct greater_t {
template

Value operator()(Value x, Value y) const { return x > y; }
} greater_than;
functions.push—back(greater_than);

В этом контексте оператор вызова шаблона должен быть инстанцируем для
данного типа функции. В качестве противоположного примера приведенная далее
инструкция не компилируется, так как мы не можем выполнить инстанцирование
с различными типами аргументов:
function ff= greater_than;

// Ошибка

Наконец как объекты function могут храниться лямбда-выражения с соот­
ветствующим возвращаемым типом и типами аргументов:
functions.push_back ([](double x, double y){ return x/y;

});

Каждый элемент нашего контейнера может быть вызван как функция:
for(auto& f : functions)
cout « "f(6, 3) = " « f(6,3)

« endl;

дает вывод

f(6,
f(6,
f (6,
f(6,
f(6,
f(6,

3)
3)
3)
3)
3)
3)

=
=
=
=
=
=

9
3
18
216
1
2

Разумеется, эта обертка для функций предпочтительнее простых указателей на
функции с точки зрения гибкости и ясности (за которые мы несем некоторые на­
кладные расходы).

266

Библиотеки

4.4.3. Оболочка для ссылок

|с++п|
c++ll/ref_example.срр

Предположим, мы хотим создать список векторов или матриц — возможно,
весьма больших. Кроме того, предположим, что некоторые записи появляются в
нем несколько раз. Таким образом, мы не хотим хранить фактические векторы
или матрицы. Мы могли бы создать контейнер указателей, но мы хотим избежать
всех опасностей, связанных с ними (раздел 1.8.2).
К сожалению, мы не можем создать контейнер ссылок:
vector w; // Ошибка

C++11 предоставляет для этой цели тип, похожий на ссылку и именуемый
reference wrapper, который включен в заголовочный файл :
ve с t о г finished();
private:
Solver
my_solver;
mutable interruptible_iteration my_iter;
mutable std:: thread
my__thread;

}

};

После того как решатель запускается с помощью async executor, мы можем
работать над чем-то еще и время от времени проверять, не является ли решатель
в состоянии finished (). Если мы понимаем, что результат вычислений больше
не нужен, мы можем прервать выполнение с помощью interrupt (). Как при
полном решении, так и при прерывании выполнения мы должны дождаться с по­
мощью вызова wait (), пока поток thread не будет должным образом завершен
с помощью вызова j oin ().
Приведенный далее псевдокод иллюстрирует, как асинхронное выполнение
может быть использовано учеными:
while (!happy (science_foundation)) {
discretize_model ();
auto my_solver = itl: :make_cg_solver (A, PC) ;

itl::async_executor async_exec(my_solver);
async_exec.start_solve(x,b,iter);
play_with_model();
if (found_better_model)

async_exec.interrupt();
else

async_exec.wait();
}

4.7. Научные библиотеки за пределами стандарта

273

Мы могли бы также использовать асинхронные решатели для численно слож­
ных систем, для которых априори неизвестно, какие решения могут сходиться.
С этой целью мы могли бы запустить решатели параллельно и подождать до тех
пор, пока один из них не завершится, после чего прерывать выполнение других.
Для ясности желательно хранить исполнители в контейнере. Если исполнители не
являются ни копируемыми, ни перемещаемыми, можно воспользоваться контей­
нером из раздела 4.1.3.2.
Этот раздел — вовсе не всеобъемлющее введение в параллельное програм­
мирование C++. Это просто небольшая демонстрация, которая, надеемся, станет
источником вдохновения для вдумчивого изучения того, что можно сделать с но­
выми функциями. Прежде чем писать серьезные параллельные приложения, мы
настоятельно рекомендуем ознакомиться с литературой на эту тему, в которой
рассматриваются теоретические основы параллельности. В частности, мы реко­
мендуем книгу C++ Concurrency in Action [53] Энтони Уильямса (Antony Williams),
который был основным источником параллельных возможностей в C++И.

4.7. Научные библиотеки за пределами стандарта
Помимо стандартных библиотек, имеется множество сторонних библиотек для
научных приложений. В этом разделе мы кратко представим некоторые библи­
отеки с открытым исходным кодом. Это чисто субъективный выбор автора на
момент написания книги. Таким образом, не должно быть переоценено ни отсут­
ствие библиотеки в списке, ни тем более ее присутствие — особенно с учетом того,
что с момента ее написания прошло некоторое время. Учитывая, что программное
обеспечение с открытым исходным кодом меняется быстрее, чем основы базового
языка программирования, и что многие библиотеки очень быстро добавляют но­
вые возможности, мы воздержимся от детального представления этих библиотек
и рекомендуем вам обратиться к соответствующим руководствам.

4.7.1. Иная арифметика
Большинство вычислений выполняются с действительными, комплексными и
целыми числами. В школе на уроках математики мы также узнали о существова­
нии рациональных чисел. Хотя рациональные числа не поддерживаются стандар­
том C++8, для работы с ними имеются библиотеки с открытым исходным кодом,
в частности перечисленные ниже:
Boost::Rational — это библиотека шаблонов, предоставляющая обычные
арифметические операции с естественными обозначениями операторов. Раци­
ональные числа всегда нормализованы (знаменатель всегда положителен и вза­
имно прост с числителем). Использование библиотеки для работы с целыми
8 Хотя такие предложения делались неоднократно. Возможно, в будущем поддержка рациональ­
ных чисел войдет в стандарт C++.

Библиотеки

числами с неограниченной точностью преодолевает проблемы потери точности,
переполнения и потери значимости.
Библиотека GMP предлагает возможность работы с целыми числами с неограниченной/произвольной точностью. Она также обеспечивает работу с рациональ­
ными числами на основе собственных целых чисел и работу с числами с плаваю­
щей точкой произвольной точности. Интерфейс C++ вводит для этих операций
классы и обозначения операторов.
ARPREC — это еще одна библиотека для работы с произвольной точностью
(ARbitrary PRECision) с целыми, действительными и комплексными числами с на­
страиваемым числом десятичных знаков.

4.7.2. Арифметика интервалов
Идеей этой арифметики является то, что входные данные являются не точны­
ми значениями, а некоторыми приближениями. С учетом этой неточности дан­
ных каждый их элемент представлен интервалом, гарантированно содержащим
правильное значение. Арифметика реализована с соответствующими правилами
округления таким образом, чтобы результирующий интервал содержал точный
результат (т.е. значение, вычисленное с совершенно правильными входными
данными и вычислениями без погрешностей). Однако в случае больших интер­
валов для входных данных или для численно нестабильных алгоритмов (или при
выполнении обоих этих условий) результирующий интервал может быть очень
большим: в худшем случае — (-оо; оо). Этот совершенно неудовлетворительный
результат, тем не менее, по крайней мере очевидно указывает, что что-то пошло
не так, что качество вычисляемых значений с плавающей точкой совершенно не­
ясно и необходимо проведение дополнительного анализа.
Библиотека Boost: : Interval предоставляет шаблонный класс для представ­
ления интервалов, а также распространенные арифметические и тригонометри­
ческие операции с ними. Класс может быть инстанцирован с каждым типом, для
которого установлены необходимые стратегии, например с типами из предыду­
щих разделов.

4.7.3. Линейная алгебра
Это предметная область, в которой доступны многие пакеты — как с откры­
тым исходным кодом, так и коммерческое программное обеспечение. Здесь мы
представим только незначительную их часть.
Blitz++ является первой научной библиотекой, использующей шаблоны выра­
жений (раздел 5.3), созданные Тоддом Фельдхойзеном (Todd Veldhuizen), одним из
двух изобретателей этой технологии. Это позволяет определять векторы, матрицы
и тензоры высшего порядка с элементами настраиваемых скалярных типов.
uBLAS — это более поздняя библиотека шаблонов C++, изначально написан­
ная Йоргом Вальтером (Jorg Walter) и Матиасом Кохом (Mathias Koch). Она стала
частью коллекции Boost и поддерживается его сообществом.

4.7, Научные библиотеки за пределами стандарта

275

MTL4 — это библиотека шаблонов от автора книги для векторов и широко­
го спектра матриц. Базовая ее версия поставляется с открытым исходным кодом.
Поддержка GPU в библиотеке обеспечивается с помощью CUDA. Суперкомпью­
терное издание библиотеки может работать на тысячах процессоров. Разрабаты­
ваются и другие версии библиотеки.

4.7.4. Обычные дифференциальные уравнения
Библиотека odeint Карстена Ахнерта (Karsten Ahnert) и Марио Мулански
(Mario Mulansky) предназначена для численного решения обычных дифференци­
альных уравнений. Благодаря своему обобщенному дизайну библиотека работает
не только с целым рядом стандартных контейнеров, но может работать и с рядом
внешних библиотек. Таким образом, фундаментальные вычисления линейной ал­
гебры могут выполняться с библиотеками MKL, CUDA-библиотекой Thrust, MTL4,
VexCL и ViennaCL. Примененные в библиотеке передовые методы поясняются
Марио Мулански в разделе 7.1.

4.7.5. Дифференциальные уравнения в частных производных
Имеется огромное количество пакетов программного обеспечения для реше­
ния дифференциальных уравнений в частных производных. Здесь мы упомянем
только два из них, которые, по нашему мнению, очень широко применимы и эф­
фективно используют современные методы программирования.
FEniCS представляет собой набор программного обеспечения для решения
дифференциальных уравнений в частных производных методом конечных эле­
ментов. Эта библиотека предоставляет пользователю API для языков программи­
рования Python и C++, позволяющий описывать задачу в частных производных
в слабой форме, а затем FEniCS по этому описанию генерирует приложение C++,
которое и решает поставленную задачу.
FEEL++ является библиотекой метода конечных элементов от Кристофа Прудхома (Christophe Prud’homme), которая также позволяет записывать задачу в час­
тных производных в слабой форме. FEEL++ в отличие от FEniCS использует не
внешний генератор кода, а возможности компилятора C++ по преобразованию
кода.

4.7.6. Алгоритмы на графах
Boost Graph Library (BGL), написанная главным образом Джереми Сиком
(Jeremy Siek), предоставляет весьма обобщенный подход, так что библиотека мо­
жет применяться к разнообразным форматам данных [37]. Она содержит значи­
тельное количество алгоритмов для работы с графами. Параллельное расширение
этой библиотеки эффективно работает на сотнях процессоров.

Библиотеки

276

4.8. Упражнения
4.8.1. Сортировка по абсолютной величине
Создайте вектор чисел double и инициализируйте его значениями -9.3, -7.4,
-3.8, -0.4, 1.3, 3.9, 5.4, 8.2. Для этого можно использовать список инициализации.
Отсортируйте данные значения по абсолютной величине. Напишите для выпол­
нения сравнения чисел


функтор и

• лямбда-выражение.

Испытайте оба решения.

4.8.2. Контейнер STL
Создайте std: :map для номеров телефонов, т.е. отображение строк на
unsigned long. Заполните отображение как минимум четырьмя записями. Вы­
полните поиск существующего и несуществующего имен. Выполните также поиск
существующего и несуществующего номеров.

4.8.3. Комплексные числа
Реализуйте визуализацию множества Жюлиа (для квадратичного полинома),
подобное множеству Мандельброта. Простое различие между ними заключается
в том, что константа, добавляемая к квадратичной функции, не зависит от поло­
жения пикселя. По существу, вы должны ввести константу к и немного изменить
iterate.

Рис. 4.9. Множество Жюлиа для к = -0.6 + 0.6i дает комплексную канторову пыль

4.8. Упражнения

277

Начните с к = -0.6 + 0.6/ (рис. 4.9). Должна получиться комплексная канторова
пыль, известная также как пыль Фату.
Испробуйте другие значения для к наподобие 0.353 -I- 0.288/ (сравните с
http://warp.povusers.org/Mandelbrot). Вы можете изменить цветовую схе­
му, чтобы получить более красивую визуализацию.



Проблема при разработке программного обеспечения в том, чтобы написать
реализацию множеств Мандельброта и Жулиа с минимальной избыточнос­
тью кода. (Еще одна проблема — найти цвета, которые хорошо выглядят
для всех fc. Но она точно выходит за рамки данной книги.)

• Дополнительно: объедините оба фрактала в интерактивном режиме. Для
этого надо предоставить два окна. В первом из них, как и ранее, выводится
множество Мандельброта. Кроме того, можно отслеживать указатель мыши
так, чтобы комплексное значение под ним использовалось как значение к
для множества Жюлиа во втором окне.
• Для особо талантливых: если вычисление множества Жулиа слишком мед­
ленное, можно использовать параллельные потоки или даже графический
процессор с помощью CUDA или OpenGL.

Глава 5

Метапрограммирование
Метапрограммы представляют собой программы над программами. Чтение
программы в виде текстового файла и выполнение определенных его преобразо­
ваний, конечно, возможны в большинстве языков программирования. В C++ мы
можем даже писать программы, которые выполняют вычисления во время ком­
пиляции или трансформируют сами себя. Тодд Фельдхойзен (Todd Veldhuizen)
показал, что система типов шаблонов C++ является Тьюринг-полной [49]. Это
означает, что в C++ во время компиляции может быть вычислено все вообще вы­
числимое.
В этой главе мы детально обсудим эту интригующую возможность C++. В час­
тности, мы рассмотрим три основных ее приложения:
• вычисления времени компиляции (раздел 5.1);

• информация о типах и их преобразование (раздел 5.2);
• генерация кода (разделы 5.3 и 5.4).
Эти методы позволяют нам сделать примеры из предыдущих разделов более
надежными, более эффективными и более широко применимыми.

5.1. Пусть считает компилятор
Во всей своей полноте метапрограммирование было обнаружено, вероятно,
благодаря ошибке. Эрвин Унру (Erwin Unruh) написал в начале 1990-х годов про­
грамму, которая выводила простые числа в качестве сообщений об ошибках, и,
таким образом, продемонстрировал, что компиляторы C++ способны к вычис­
лениям. Эта программа, безусловно, — самый известный код C++, который не
компилируется. Заинтересовавшийся читатель может найти его в разделе А.9.1.
Примите его как свидетельство возможности экзотического поведения, а не как
пример для ваших будущих программ.
Вычисления времени компиляции могут осуществляться двумя способами:
обратно совместимо с помощью шаблонных метафункций и более легко с помо­
щью constexpr. Последняя возможность была введена в С++11 и расширена в
C++14.

280

Метапрограммирование

5.1.1. Функции времени компиляции

C++11
«=> c++ll/fibonacci.cpp

Даже в современном C++ выполнить проверку на простоту числа — это
не самая простая задача, так что мы начнем с более простых чисел Фибоначчи.
Их можно вычислять рекурсивно:
constexpr long fibonacci(long n)
{

return n
struct Magnitude {};
template о
struct Magnitude
{
using type = int;
};

template
struct Magnitude
{
using type = float;

Предоставление и использование информации о типах

5.2.

289

template о
struct Magnitude
{

using type =

double;

};

template
struct Magnitude
class transposed_view
{
public:
using value_type = typename Matrix::value_type;
using size_type = typename Matrix::size_type;
explicit transposed_view(Matrix&

A) : ref(A) {}

value_type& operator()(size_type r, size_type c)
{
return ref(c,r);
}
const value_type& operator()(size_type r, size_type c)
{
return ref(c,r);
}
private:
Matrix& ref;

const

};

Здесь мы предполагаем, что класс Matrix предоставляет operator (), прини­
мающий два аргумента, которые представляют собой индексы строки и столбца,
и возвращает ссылку на соответствующую запись
Далее мы предполагаем,
что для value_type и size_type определены свойства типов. Это все, что нам
нужно знать в этом мини-примере о матрице (в идеале мы могли бы определить
концепцию простой матрицы). Реальные библиотеки шаблонов, такие как MTL4,
конечно, предоставляют большие интерфейсы. Однако этот небольшой пример
достаточно хорошо демонстрирует использование метапрограммирования в опре­
деленных представлениях.
Объект класса transposed_view можно рассматривать как обычную матрицу;
например, он может передаваться во все шаблоны функций, в которых ожидает­
ся матрица. Транспонирование получается “на лету” путем вызова operator ()
объекта, на которое ссылается представление, с индексами, поменянными мес­
тами. Для каждого объекта матрицы мы можем определить транспонированное
представление, которое ведет себя, как матрица:
mtl::dense2D А = {{2, 3, 4},
{5, 6, 7},
{8, 9, 10}};

transposed_view неоднозначное наследование

};
int main ()
{
math_student bob("Robert Robson",
"Algebra",
"Большую теорему Ферма");
bob.all_info();
}

Класс math_student наследует all_info от класса student и от класса
mathematician, и никакого приоритета одного перед другим не существует.
Единственный способ устранения неоднозначности all_inf о в math student —
определить этот метод в классе math student соответствующим образом.
Эта неоднозначность позволяет проиллюстрировать одну тонкость C++, о ко­
торой вы должны быть осведомлены. Модификаторы public, protected и
private изменяют доступность, но не видимость. Это становится (мучительно)
ясно, если мы попытаемся добиться однозначного определения функции-члена
путем наследования одного или более суперклассов как private или protected:
class
class
class
:
{ ...

student { ... };
mathematician { ... };
math_student
public student, private mathematician
I;

Теперь методы student являются открытыми, а методы mathematician —
закрытыми. Вызывая math_student: :all_info, мы надеемся теперь увидеть
результат student: :all_info. Но вместо этого мы получаем два сообщения
об ошибке: вызов math_student: :all_info является неоднозначным, а метод
mathematician: :all_info является недоступным.

6.3.2. Общие прародители
Нередко возникает ситуация, когда несколько базовых классов имеют свои об­
щие базовые классы. В предыдущем разделе mathematician и student не имели
суперклассов. Но из рассмотренных ранее разделов было бы более естественным
их наследование от person. Изображение этой конфигурации наследования име­
ет вид ромба, как показано на рис. 6.2. Мы реализуем ее двумя различными спо­
собами.

370

Объектно-ориентированное программирование

Рис. 6.2. Ромбовидная иерархия классов

6.3.2.1. Избыточность и неоднозначность
■=> c++ll/oop_multil. срр

Сначала мы реализуем классы как обычно:
class person { . . . }
// Как и ранее
class student { ... } // Как и ранее

class mathematician
: public person
{
public:
mathematician(const strings name, const strings proved)
: person(name), proved(proved) {}
virtual void all__info() const override {
person::all_info() ;
cout « "
Я доказал: " « proved « endl;
}
private:
string proved;
};
class math_student
: public student, public mathematician
{
public:
math_student(const strings name, const strings passed,
const strings proved )
: student(name,passed), mathematician(name,proved )
virtual void all_info() const override {
student::all_info();

{}

371

Множественное наследование

6.3.

mathematician::all_infо();
}
};

int main()
{
math_student bob (’’Robert Robson", ’’Algebra",
"Большую теорему Ферма’’);
bob.all_info ();
}

Программа работает корректно, за исключением выдачи избыточной инфор­
мации об имени:

[student] мое имя - Robert Robson
Я прошел следующие курсы: Algebra
[person] Мое имя - Robert Robson
Я доказал: Большую теорему Ферма

Как читатель вы теперь имеете два варианта действий: принять этот неопти­
мальный метод и продолжить чтение или перейти к упражнению 6.7.1 и попы­
таться решить проблему самостоятельно.
Следствия двойного наследования person таковы.
Избыточность кода: name хранится в объекте дважды, как показано на рис. 6.3.

• Склонность к ошибкам: два значения name могут быть несогласованными.

• Неоднозначность: при обращении к person: : name в классе math student.

math_student

student

person
name

mathematician
person

name

Puc, 6,3, Схема памяти объекта math_student
■=> c++ll/oop_multi2.cpp

Объектно-ориентированное программирование

372

Для иллюстрации упомянутой неоднозначности вызовем person: :all_info
в math—Student:
class math_student :

...

{
virtual void all_info() const override {
person::all_info ();
}

};

Компилятор (в данном случае clang 3.4) тут же начнет жаловаться:
...: ошибка: неоднозначное преобразование в производном классе
’const math_student' к базовому классу 'person':
class math_student -> class student -> class person
class math_student -> class mathematician -> class person
person: :all_infoO;

Конечно же, мы столкнемся с этой проблемой при работе с каждой функциейчленом или членом-данными суперклассов, унаследованных несколькими путями.

6.3.2.2. Виртуальные базовые классы
«=> c++ll/oop_multi3.срр

Виртуальные базовые классы позволяют нам хранить члены общих суперклас­
сов в одном экземпляре и тем самым справляться с указанными проблемами. Од­
нако, чтобы применение этой технологии не привело к новым проблемам, требу­
ется базовое понимание ее внутренней реализации. В приведенном далее примере
мы просто указываем person как виртуальный базовый класс с помощью ключе­
вого слова virtual:
class person {

...

};

class student
: public virtual person
{ ... };

class mathematician
: public virtual person
{

...

};

class math_student
: public student, public mathematician
{
public:
ma th_student(const strings name, const strings passed,
const strings proved)
: student(name,passed), mathematician(name,proved)

};

{}

6.3. Множественное наследование

373

В результате мы получаем следующий вывод на экран, который может удивить
некоторых читателей:
[student] мое имя я прошел следующие курсы: Algebra
я доказал: Большую теорему Ферма

Мы потеряли значение name, несмотря на то что и класс student, и класс
mathematician вызывают конструктор класса person, который инициализиру­
ет name. Чтобы понять это поведение, мы должны знать, как C++ обрабатыва­
ет виртуальные базовые классы. Мы знаем, что за вызов конструктора базового
класса отвечает конструктор производного класса (в противном случае компиля­
тор вызовет конструктор по умолчанию). Однако у нас есть только одна копия
базового класса person: на рис. 6.4 показана новая схема памяти: mathematician
и student больше не содержат данные person, а только ссылаются на об­
щий объект, который является частью наиболее позднего производного класса
math_student.

Рис. 6.4. Классmath—Student с виртуальными
базовыми классами (vbase — ссылка на вир­
туальный базовый класс)

При создании объекта student его конструктор должен вызвать конструктор
person. Аналогично при создании объекта mathematician его конструктор тоже
должен вызвать конструктор person. Теперь мы создаем объект math student.
Конструктор math student должен вызвать конструкторы как mathematician,
так и student. Но мы знаем, что оба эти конструктора должны вызвать кон­
структор person, и таким образом, совместно используемая единственная часть
person окажется сконструированной дважды!

Объектно-ориентированное программирование

374

Для предотвращения такой неприятности было определено, что в случае
виртуальных базовых классов за вызов конструктора общего базового класса
(в нашем случае — класса person) отвечает наиболее поздний производный класс
(most derived class, которым в нашем случае является math_student). В свою
очередь, косвенные вызовы конструктора person из производных классов,
т.е. mathematician и student, отключены.
=> c++ll/oop_multi4.срр

С учетом этого изменим наши конструкторы соответствующим образом:
class student
: public virtual person
{
protected:

student(const strings passed) : passed(passed) {}
};

class mathematician
: public virtual person
{
protected:

mathematician(const strings proved) : proved(proved) {}
};
class math_student
: public student, public mathematician
{
public:
math_student(const strings name, const strings passed,
const strings proved)

: person (name), student (passed), mathematician (proved) {}
virtual void all_info() const override {
student::all_infо();

mathematician: :my_infos () ;
}
};

Теперь math student явно инициализирует person, передавая ему значение
name. Два промежуточных класса, student и mathematician, переделаны следу­
ющим образом.

• Инклюзивная обработка включает методы класса person: двухаргументный
конструктор и метод all_inf о. Эти методы объявлены как public и пред­
назначены, в первую очередь, для объектов student и mathematician.

• Эксклюзивная обработка имеет дело только с членами самого класса: од­
ноаргументным конструктором и методом my_inf os. Эти методы являются
защищенными, а потому доступными только в подклассах.

Динамический выбор с использованием подтипов

6.4.

375

Приведенный пример показывает необходимость всех трех модификаторов
доступа:

• private: для членов-данных, которые доступны только внутри класса;
• protected: для методов, необходимых подклассам, которые не используют­
ся собственными объектами;

• public: для методов, предназначенных для объектов класса.

После изучения азов объектно-ориентированного программирования мы зай­
мемся их применением в научном контексте.

6.4. Динамический выбор с использованием подтипов
=> c++ll/solver_selection_example .срр

Динамический выбор решателя может быть реализован с помощью конструк­
ции switch следующим образом:
#include
tfinclude
class matrix {};
class vector {};

void eg(const matrixs A, const vectors b, vectors x);
void bieg(const matrixs k, const vectors b, vectors x);
int main (int argc,
{
matrix A;
vector b, x;

char* argv [])

int solver_choice = argc >= 2 ? std::atoi(argv[1])
switch (solver_choice) {
case 0: eg(A, b, x);
break;
case 1: bieg (A, b, x); break;

: 0;

}
}

Этот способ работает, но он не масштабируем в смысле сложности исходного
кода. Вызывая решатель с другими векторами и матрицами где-нибудь в другом
месте, мы должны скопировать весь блок switch-case для каждой комбинации
аргументов. Этого можно избежать путем инкапсуляции блока в функцию и вы­
зова этой функции с разными аргументами.

Объектно-ориентированное программирование

376

Ситуация становится гораздо более сложной, когда динамически выбирается
несколько аргументов. Например, в случае решателя системы линейных уравне­
ний нам нужно выбирать решатель на основании различных вариантов матриц
(диагональной, треугольной и т.д.). При усложнении задачи количество парамет­
ров растет, так что у нас имеется уже не один, а несколько выборов разновид­
ностей матриц. При этом нам понадобятся вложенные конструкции switch, как
показано в разделе А.8. Таким образом, мы можем решить задачу динамического
выбора наших функциональных объектов, не прибегая к объектно-ориентиро­
ванному программированию, но мы должны признать наличие комбинаторного
взрыва в пространстве параметров: решателей, левых и правых предобусловливателей. Если мы добавим новый решатель или иной элемент, то нам придется
расширять этот чудовищный блок выбора в нескольких местах.
Элегантным решением этой проблемы выбора является использование аб­
страктных классов в качестве интерфейсов и производных классов с конкретными
решателями:
struct solver
{
virtual void operator() (...) =0;
virtual -solver() {}
};

// Потенциально использует шаблоны
struct cg_solver : solver

{
virtual void operator() (...) override { eg(A, b, x);

}

};

struct bicg_solver : solver

{
virtual void operator()(

...

)

override { bieg(A, b, x);

)

};

C++HI В нашем приложении мы можем определить (интеллектуальный) ука­
затель на тип интерфейса solver решения и присвоить ему требуемый
конкретный решатель:
unique_ptr my_solver;
switch (solver_choice) {
case 0: my_solver = unique_ptr(new cg_solver);
break;
case 1: my_solver = unique_ptr(new bicg_solver);
break;
}

6.4. Динамический выбор с использованием подтипов

377

Этот метод тщательно рассмотрен в книге, посвященной шаблонам проектиро­
вания [14], и представляет собой шаблон фабрики. В С-Ы-03 фабрика может быть
реализована и с помощью обычных указателей.
С++14

Создание unique_ptr все же оказывается несколько излишне гро­
моздким, так что в стандарт C++14 введена вспомогательная функция
make unique, которая облегчает разработку:
unique_ptr my_solver;
switch (solver_choice) {
case 0: my_solver = make__unique () ;
break;
case 1: my_solver = make_unique(); break;
}

Реализация собственной функции make unique — хорошее упражнение для
самостоятельной работы (см. упражнение 3.11.13).
После того как наш полиморфный указатель будет инициализирован, вызов
динамически выбранного решателя не представляет проблемы:
(*my_solver)(А, Ь, х);

Скобки в этом выражении означают, что мы разыменовываем указатель на
функцию и вызываем ее. Без скобок получалось бы, что мы (безуспешно) пыта­
емся применить вызов к указателю на функцию и разыменовать полученный ре­
зультат.
Полная мощность фабричного подхода становится очевидной, когда динами­
чески выбираются несколько функций. Так мы можем избежать упоминавшегося
ранее комбинаторного взрыва. Полиморфные указатели на функции позволяют
нам разделить выбор в соответствии с критериями и разложить задачу на после­
довательность фабрик и единственный вызов с указателями:
struct рс
{

virtual void operator()(...) = 0;
virtual '-pc ()

{}

};

struct solver {

...

};

// Фабрика решателя
// Фабрика левого предусловия
// Фабрика правого предусловия
(*my_solver)(Az b, х, *left, *right);

Теперь у нас есть код с линейной сложностью в фабриках и один оператор вы­
зова функции, в то время как ранее в огромном блоке выбора мы сталкивались с
кубической сложностью.
| С++111 В нашем примере мы реализовали общий суперкласс. Мы также мо­
жем работать с классами решателей и функций без общего базового

Объектно-ориентированное программирование

378

класса с помощью шаблона std: : function, который позволяет нам
реализовывать более общие фабрики. Тем не менее он основан на том
же подходе: виртуальные функции и указатели на полиморфные клас­
сы. Обратную совместимость в С++03 можно обеспечить, применив
boost::function.
C++ запрещает виртуальные шаблонные функции (они могут сделать реали­
зации компилятора чрезвычайно сложной задачей из-за наличия потенциально
бесконечных таблиц виртуальных функций). Однако шаблоны класса могут со­
держать виртуальные функции. Это позволяет применять обобщенное програм­
мирование с виртуальными функциями с использованием параметризации типа
всего класса вместо параметризации одного метода.

6.5. Преобразования
Преобразования связаны не только с темой объектно-ориентированного про­
граммирования, но мы не могли бы обсудить его всесторонне, не введя перед этим
понятия базовых и производных классов. И наоборот, рассмотрение приведений
между связанными классами помогает в понимании наследования.
C++ является строго типизированным языком. Тип каждого объекта (перемен­
ной или константы) определяется во время компиляции и не может быть изменен
во время выполнения5. Мы можем рассматривать объект как


биты в памяти;

• тип, который придает этим битам смысл.

Для некоторых приведений компилятор просто по-разному смотрит на биты
в памяти: с иной их интерпретацией или другими правами доступа (например,
константные и неконстантные значения). Другие приведения фактически создают
новые объекты.
В C++ имеется четыре различных оператора приведения.


static_cast



dynamic_cast



const_cast



reinterpret_cast

Язык С в качестве лингвистического корня C++ знает только один оператор
приведения: (type) ехрг. Этот единственный оператор трудно понять, потому что
он может вызвать целый каскад преобразований для создания объекта целевого
5 В противоположность этому переменные языка Python не имеют фиксированного типа и явля­
ются на самом деле просто именами, которые ссылаются на некоторый объект. В случае присваива­
ния переменная просто ссылается на другой объект и принимает тип нового объекта.

6.5. Преобразования

379

типа, например преобразование константного указателя на int в неконстантный
указатель на char.
В отличие от приведения в С приведение в C++ изменяет только один аспект
типа за раз. Еще одним недостатком приведений в стиле С является то, что их
трудно найти в коде (см. также [45, раздел 95]), тогда как приведения C++ легко
обнаружить: нужно просто искать _cast. C++ разрешает применение приведе­
ний в старом стиле С, но все эксперты сходятся в том, что следует запретить его
использование.
Приведения в стиле С

Не используйте приведения в стиле языка С!

В этом разделе мы рассмотрим различные операторы приведения и обсудим
“за” и “против” различных приведений в разных контекстах.

6.5.1. Преобразование между базовыми
и производными классами
C++ предоставляет статическое и динамическое преобразования между класса­
ми иерархии.

6.5.1.1. Приведение к базовому классу
=> c++03/up_down_cast_example .срр

Восходящее приведение производного класса к базовому возможно всегда, если
при этом не возникает неоднозначностей. Оно даже может быть выполнено неяв­
но, как мы это делали в функции spy on:
void spy_on(const
spy_on(tom);

persons p);

// Приведение student -> person

spy_on принимает все подклассы person без необходимости явного преобразо­
вания. Таким образом, мы свободно можем передать в качестве аргумента объект
tom типа student. Для обсуждения преобразований между классами в ромбовид­
ной иерархии введем для краткости некоторые однобуквенные имена:
struct А
{
virtual void f () {}
virtual ~A(){}
int ma;
};
struct В : A { float mb; int fb(){ return 3;
struct C : A {};
struct D : В, C {};

}

};

380

Объектно-ориентированное программирование

Добавим следующие унарные функции:
void f (А а)
void g(A& а)
void h(A* а)

{/*...*/}// Не полиморфна => срезка!
{/*...*/ }
{/*...*/}

Объект типа В может быть передан во все три функции:
В Ь;
f (Ь);
g(b);
h(&b) ;

// Срезка!

Во всех трех случаях объект Ь неявно преобразуется в объект типа А. Однако
функция f не является полиморфной, так как срезает объект b (как описано в раз­
деле 6.1.3). Приведение к базовому классу не работает только тогда, когда базовый
класс является неоднозначным. В следующем примере мы не можем выполнить
приведение D к А:
D d;

A ad(d); // Ошибка: неоднозначность

Дело в том, что компилятор не знает, означает ли базовый класс А, получен­
ный через В или через С. Мы можем прояснить ситуацию с помощью явного про­
межуточного приведения:
A ad(B(d) ) ;

Можно также прибегнуть к виртуальному наследованию А классами В и С:
struct В: virtual А { ...
struct С: virtual А {};

};

В этом случае члены А имеются в D только в одном экземпляре. В большинстве
случаев это является лучшим решением для множественного наследования, пото­
му что этим мы экономим память и устраняем риск несогласованности копий А.

6.5.1.2. Приведение к производному классу
Нисходящее приведение представляет собой преобразование указателя/ссылки
к указателю/ссылке на подтип. Если указываемый объект при этом не является
объектом требуемого типа, мы получаем неопределенное поведение. Таким обра­
зом, данное приведение должно использоваться с особой осторожностью и только
тогда, когда это абсолютно необходимо.
Напомним, что мы передаем объект типа В как ссылку типа А& или указатель
типа А*:
void g (А& а)
void h(А* а)
В Ь;
g (Ь) ;
h(fib);

{
{

...
...

}
}

6.5. Преобразования

381

В функциях g и h мы не можем обращаться к членам В (т.е. mb и fb О), не­
смотря на тот факт, что объект Ь, ссылка на который используется, на самом деле
является объектом типа В. При абсолютной уверенности в том, что параметр фун­
кции а ссылается на объект типа В, мы можем выполнить нисходящее приведение
а к В& (или к В*) и получить доступ к mb и fb ().
Прежде чем использовать нисходящее приведение в нашей программе, мы
должны задать сами себе следующие вопросы.
• Как мы можем убедиться, что переданный в функцию аргумент на самом
деле является объектом производного класса? Например, с помощью допол­
нительных аргументов или проверок времени выполнения?

• Что мы можем сделать, если объект не может быть подвергнут нисходяще­
му приведению?



Не должны ли мы вместо этого написать функцию для производного класса?

• Почему мы не можем перегрузить функцию для базового и производного
типов? Это, определенно, более красивый и всегда осуществимый дизайн.
• И последнее (не по важности): не можем ли мы изменить свои классы таким
образом, чтобы наша задача выполнялась с помощью позднего связывания
виртуальных функций?
Если, ответив на все эти вопросы, мы по-прежнему искренне полагаем, что нам
нужно нисходящее приведение, то тогда мы должны решить, какое именно нисхо­
дящее приведение мы применяем. Имеются два варианта:


static cast, более быстрое и небезопасное;



dynamic_cast, безопасное, но имеющее определенные накладные расходы
и доступное только для полиморфных типов.

Как подсказывает его название, приведение static_cast выполняет провер­
ку сведений только во время компиляции. В контексте нисходящего приведения
это означает проверку, является ли целевой тип производным от исходного. Мы
можем, например, привести а, аргумент функции д, к типу В&, а затем вызвать
метод класса В:
void g(А& а)
{
В& bref = static_cast (а) ;
std::cout « "fb возвращает " «
}

bref.fbO « "\n";

Компилятор проверяет, является ли В подклассом А, и принимает нашу реали­
зацию. Когда аргумент а ссылается на объект, тип которого отличается от типа В
(или его подтипа), поведение программы становится неопределенным (наиболее
вероятным результатом будет ее аварийное завершение).

Объектно-ориентированное программирование

382

В нашем ромбическом примере мы также можем выполнить нисходящее при­
ведение указателя на В к указателю на D. В этой связи мы объявляем указатели
типа В*, которые могут указывать на объекты подкласса D:
В *bbp= new В,

*bdp= new D;

Компилятор принимает нисходящее приведение к D* для обоих указателей:
dbp = static_cast (bbp);
ddp = static_cast (bdp);

// Ошибочное приведение
// Корректное приведение (не проверяемое)

Поскольку проверки времени выполнения не производятся, мы как програм­
мисты отвечаем за то, чтобы указатели указывали на объекты правильного типа,
bbp указывает на объект типа В, так что, разыменовывая указатель, мы рискуем
повредить данные и аварийно завершить программу. В этом небольшом примере
достаточно интеллектуальный компилятор может путем статического анализа об­
наружить некорректное нисходящее приведение и выдать предупреждение. Но в
общем случае не всегда можно отследить фактический тип, на который ссылается
указатель, особенно если он может быть выбран во времявыполнения:
В *bxp= (

argc >1) ? new В : new D;

В разделе 6.6 мы увидим интересное применение статического нисходящего
приведения, которое является безопасным, поскольку сведения о типе предостав­
ляются в виде аргумента шаблона.
dynamic cast выполняет тест времени выполнения, проверяя, действительно
ли приводимый объект имеет нужный тип или подтип. Он может быть применен
только к полиморфным типам (классам, которые определяют или наследуют одну
или несколько виртуальных функций; раздел 6.1):
D* dbp = dynamic_cast(bbp);
D* ddp = dynamic_cast(bdfc>);

// Ошибка: приведение к D невозможно
// OK: bdp указывает на объект D

Если выполнить приведение невозможно, возвращается нулевой указатель, так
что программист может в конечном итоге отреагировать на сбой нисходящего
приведения. Сбой нисходящего приведения для ссылок приводит к генерации ис­
ключения типа std: :bad_cast и может быть обработан в блоке try-catch. Про­
верки осуществляются с помощью информации о типе времени выполнения (run­
time type information — RTTI) и отнимают некоторое дополнительное время.

Дополнительные сведения. За кулисами dynamic cast реализуется как
виртуальная функция. Таким образом, она доступна только тогда, когда
пользователь сделал класс полиморфным путем определения по меньшей
мере одной виртуальной функции (в противном случае все классы вынуж­
дены были бы нести расходы на таблицы виртуальных функций). Поли­
морфные функции имеют эти таблицы так или иначе, так что стоимость
dynamic_cast сводится к одному дополнительному указателю в таблице
виртуальных функций.

6.5. Преобразования

383

6.5.1.3. Перекрестное приведение
Интересной возможностью dynamic_cast является приведение В к С, когда
тип указываемого объекта является производным классом для них обоих:
С* cdp = dynamic__cast

(bdp); //

ОК: В -> С через объект D

Аналогично можно выполнить приведение student к mathematician.
Статическое перекрестное приведение В к С
cdp = static_cast (bdp);

//

Ошибка: не подкласс и не суперкласс

невозможно, поскольку С не является ни базовым, ни производным по отноше­
нию к В. Приведение может быть выполнено косвенно, через D:
cdp = static_cast(static_cast (bdp));

//

В -> D -> С

Здесь вновь вся ответственность за выяснение, можно ли выполнять приведе­
ние таким образом, возлагается на программиста.

6.5.1.4. Сравнение статического и динамического приведений
Динамическое приведение безопаснее, но медленнее статического из-за време­
ни на выполнение проверки типа объекта. Статическое приведение быстрее, но
возлагает на программиста ответственность за корректную обработку приводи­
мых объектов.
В табл. 6.1 подытожены различия между этими двумя разновидностями при­
ведений.

Таблица 6.1. Сравнение динамического и статического приведений

Какие классы
Перекрестное приведение
Проверки времени выполнения
Накладные расходы

static_cast

dynamic_cast

Все
Нет
Нет
Нет

Полиморфные
Да
Да
Проверка RTTI

6.5.2. const_cast
const_cast добавляет или удаляет атрибуты const и/или volatile. Ключе­
вое слово volatile информирует компилятор о том, что значение переменной
может быть изменено некоторым “неязыковым” образом. Например, некоторые
значения в памяти записываются аппаратным обеспечением, и мы должны быть
осведомлены об этом, когда пишем драйверы для такого устройства. Эти записи
в памяти нельзя кешировать или сохранять в регистрах, и их значения следует
всякий раз получать из основной памяти. В научном и высокоуровневом инже­
нерном программном обеспечении такие изменяемые извне переменные — гости
нечастые, так что мы воздержимся от обсуждения volatile далее в этой книге.
Как const, так и volatile могут быть добавлены неявно. Удаление атри­
бута volatile у объекта, который действительно является volatile, ведет к

Объектно-ориентированное программирование

384

неопределенному поведению, так как в кеше и регистрах могут находиться несо­
гласованные значения. И наоборот, атрибут volatile может быть удален только
из указателей и ссылок, квалифицированных этим ключевым словом, когда они
ссылаются на объект, не являющийся volatile.
Удаление атрибута const делает недействительными все соответствующие ква­
лификаторы const во всем стеке вызовов и тем самым существенно увеличивает
усилия, необходимые для отладки при случайной перезаписи данных. К сожале­
нию, при работе с библиотеками в старом стиле, в которых часто отсутствуют
соответствующие квалификаторы const, это удаление иногда необходимо.

6.5.3. reinterpret_cast
Это наиболее агрессивная форма приведения, и в данной книге она не исполь­
зуется. Она получает объект в памяти и интерпретирует его биты, как если бы
этот объект имел другой тип. Это позволяет нам, например, изменить один бит
в числе с плавающей точкой путем его приведения к последовательности битов.
Приведение reinterpret_cast является более важным для программирования,
например, драйверов оборудования, чем, скажем, для программ решения задач
линейной алгебры. Разумеется, применение reinterpret_cast — это один из
наиболее эффективных способов подорвать переносимость наших приложений.
Если вы действительно не в состоянии без него обойтись, включите по крайней
мере условную компиляцию, зависимую от платформы, и очень тщательно про­
тестируйте получающийся код.

6.5.4. Преобразования в стиле функций
Для преобразования значений могут использоваться конструкторы: если тип
Т имеет конструктор для аргументов типа U, мы можем создать объект типа Т из
объекта типа U:
U и;
Т t (и) ;

Или еще лучше:
U и;
Т t(u}; // С ++11

Таким образом, имеет смысл применять запись с конструктором для преобра­
зования значений. Давайте воспользуемся нашим примером с матрицами различ­
ных типов. Предположим, у нас есть функция для плотной матрицы, и мы хотим
применить ее к сжатой матрице:
struct dense_matrix
{

...

In­

struct compressed_matrix
{ ... };

385

Преобразования

6.5.

void f

(const dsnse_matrixfi) {}

int main ()
{
compressed_matrix A;
f (dense—matrix (A));
}

Здесь мы получаем объект А типа compressed_matrix и создаем из него
объект dense_matrix. Для этого требуется

• либо конструктор dense_matrix, который принимает compressedmatrix;
• либо оператор преобразования compressed—matrix в dense—matrix.

Эти методы выглядят следующим образом:
struct compressed_matrix;

// Предварительное объявление.
// Необходимо для конструктора.

struct dense_matrix
{

dense_matrix () = default;
dense_matrix (const compressed—matrixfi

A) { ... }

};

struct compressed__matrix
{

operator dense—matrix() { dense_matrix A; ... return A; }
};

Если имеются оба метода, предпочтительным оказывается конструктор. При
такой реализации класса мы можем также вызвать f с неявным преобразованием:
int main ()
{
compressed_matrix А;

f (А) ;

04-4-111 В этом случае оператор преобразования имеет приоритет над конструк­
тором. Обратите внимание, что неявное преобразование не работает с
явным конструктором (объявленным как explicit) или таким же опе­
ратором преобразования. Явные (explicit) операторы преобразования
были введены в С++11.
Опасность этой записи в том, что она ведет себя как преобразование в стиле С
со встроенными целевыми типами, т.е.
long(x);
(long)x;

// Соответствует приведению

Объектно-ориентированное программирование

386

Это позволяет нам написать код в духе следующего:
double d = 3.0;
double const * const dp = &d;
long 1 = long(dp);
// Опасно!!

Здесь мы преобразуем константный указатель на const double в long! Хотя
это выглядит как просьба создать новое значение, выполняются преобразования
const cast и reinterpret cast. Незачем упоминать, что значение 1 довольно
бессмысленное, как и все значения, зависящие от него.
Обратите внимание, что инициализация
long l(dp);

// Ошибка: нельзя инициализировать long указателем

не компилируется, как и инициализация с помощью фигурных скобок:
long l{dp);

// Та же ошибка (С++11)

Это приводит нас к еще одной записи:
1= long{dp};

// Ошибка: неверная инициализация

(С++11)

Использование фигурных скобок всегда инициализирует новое значение с за­
претом сужения, static cast допускает сужение, но также отказывается выпол­
нить преобразование указателя в число:
1 = static_cast(dp); // Ошибка: указатель -> long

По этим причинам Бьярне Страуструп (Bjarne Stroustrup) советует использо­
вать Т{и} и Т (и) для конструкторов с очевидным поведением и именованные
приведения static cast для остальных преобразований.

6.5.5. Неявные преобразования
Правила неявного преобразования нетривиальны. Хорошая новость заключа­
ется в том, что большую часть времени работы достаточно знать наиболее важные
правила, и обычно можно совершенно не знать их приоритеты. Полный список
можно найти, например, в [7]; здесь же, в табл. 6.2, дан обзор только самых важ­
ных преобразований.

Таблица 6.2. Неявные преобразования

Из

В

т
т
т

Супертип

Т [N]

т

т*
и, согласно разделу 6.5.4

Функция

Указатель на функцию

nullptr_t

Т*

Целые числа
Числовой тип

Большие целые числа
Другой числовой тип

т

const Т
volatile Т

6.6. CRTP

387

Числовые типы могут быть преобразованы различными способами. Целочис­
ленные типы могут быть повышены, т.е. расширены нулевыми или знаковыми
битами6. Кроме того, каждый встроенный числовой тип может быть преобразован
в каждый другой числовой тип, когда это необходимо для соответствия типов ар­
гументов функции. Для новых методов инициализации в C++11 разрешены толь­
ко те преобразования, которые не приводят к потере точности (т.е. не являются
сужающими). Без ограничения сужения возможно даже преобразование числа с
плавающей точкой в значение bool с помощью промежуточного преобразова­
ния в тип int. Все преобразования между пользовательскими типами, которые
могут быть выражены в стиле функции (раздел 6.5.4), также выполняются неяв­
но, если подходящий конструктор или оператор преобразования не объявлен как
explicit. Разумеется, не следует переусердствовать в использовании неявных
преобразований. Какие преобразования должны быть выражены явно, а где мы
можем рассчитывать на правила неявного преобразования, — является важным
проектным решением, для выбора которого не существует общего правила.

6.6. CRTP
В этом разделе описан шаблон проектирования Странно повторяющийся шаб­
лон (curiously recurring template pattern — CRTP). Он очень эффективно комбини­
рует шаблонное программирование с наследованием. Это название иногда путают
с Трюком Бартона-Накмана, основанным на CRTP и разработанным Джоном Бар­
тоном (John Barton) и Ли Накманом (Lee Nackman) [4].

6.6.1. Простой пример
c++03/crtp_simple_example .срр

Эту новую методику мы поясним на простом примере. Предположим, у нас
есть класс point, содержащий оператор равенства:
class point
{
public:
point(int xr int y) : x(x), y(y) {}
bool operator = (const points that) const
{
return x == that.x SS у == that.у;
}
private:
int xf y;

};

Можно запрограммировать неравенство исходя из здравого смысла или приме­
няя закон де Моргана:

6 В строгом смысле законов языка повышение преобразованием не является.

388

Объектно-ориентированное программирование

bool operator ?= (const point& that) const
{
return x != that.x || у != that.у;
}

Можно упростить нашу жизнь и просто обратить результат равенства:
bool operator != (const points that)
{
return !(*this = that);
}

const

Наши современные компиляторы настолько интеллектуальны, что они, безу­
словно, могут идеально обрабатывать закон де Моргана после встраивания. Отри­
цание оператора равенства таким способом является корректной реализацией опе­
ратора неравенства для каждого типа (вместе с оператором равенства). Мы могли
бы каждый раз копировать и вставлять этот фрагмент кода и просто заменять тип
аргумента.
В качестве альтернативного решения можно написать класс наподобие
template
struct inequality
{
bool operator != (const T& that) const
{
return !(static_cast(*this)
};

= that );

}

и выполнить его наследование:
class point : public inequality {

...

};

Это определение класса устанавливает взаимозависимость:
• point является производным от inequality, а


inequality параметризован классом point.

Эти классы вполне компилируются, несмотря на их взаимную зависимость,
потому что функции-члены шаблонных классов (наподобие inequality) не ком­
пилируются до тех пор, пока не вызываются. Мы можем проверить работоспособ­
ность operator !=:
point pl(3,4), р2(3,5);
cout « "pl != p2 дает " « boolalpha «

(pl ?= p2) « ’\n’;

Но что реально происходит при вызове pl ! = р2?

1. Компилятор выполняет поиск operator! = в классе point — безуспешно.

2. Компилятор выполняет поиск
inequality — успешно.

operator !=

в

базовом

классе

3. Указатель this указывает на объект типа inequality, являющий­
ся частью объекта point.
4. Оба типа полностью известны, и мы можем статически привести указатель
this к типу point*.

389

6.6. CRTP

5. Поскольку мы знаем, что указатель this класса inequality пред­
ставляет собой указатель this, приведенный к point*, его можно безопас­
но привести к исходному типу.
6. Вызывается и инстанцируется оператор равенства класса point (если это не
было сделано раньше).

Каждый класс U с оператором равенства точно так же может быть унаследован
от класса inequality. Набор таких шаблонов CRTP для операторов предо­
ставляется в библиотеке Boost.Operators, созданной Джереми Сиком (Jeremy Siek)
и Дэвидом Абрамсом (David Abrahams).

6.6.2. Повторно используемый оператор доступа
=> c++ll/matrix_crtp_example .срр

Идиома CRTP позволяет нам решать упомянутые ранее (раздел 2.6.4) пробле­
мы: доступ к многомерным структурам данных с помощью оператора [ ] с воз­
можностью повторного использования реализации. В то время мы еще не знали
всех необходимых языковых возможностей, в особенности шаблонов и наследо­
вания. Теперь мы их знаем и можем применить эти знания для реализации двух
операторов индексации, эквивалентных одному бинарному оператору вызова,
т.е. позволяющих вычислять А [ i ] [ j ] как А (i, j).
Пусть у нас есть тип матрицы с элегантным именем some_matrix, оператор
operator () которой обеспечивает доступ к элементу aijt Для согласованности
с векторной записью мы предпочитаем применять operator []; но он прини­
мает только один аргумент, и потому нам нужен прокси-класс, предоставляю­
щий доступ к строке матрицы. Этот прокси, в свою очередь, предоставляет свой
operator [ ] для доступа к столбцу в соответствующей строке, т.е. дает элемент
матрицы:
class some_matrix;

// Предварительное объявление

class simple_bracket_proxy

{
public:
simple_bracket_proxy(matrixs A, size_t r) : A(A), r(r) {}
doubles operator[](size_t c){ return A(r,c); }
// Ошибка
private:
matrixs A;
size_t
r;
};
class some_matrix
{

// ...
doubles

operator()(size_t

r,

size_t c)

{

...

}

Объектно-ориентированное программирование

390

simple_bracket_proxy operator[](size_t г)

{
return simple_bracket_proxy(*this,r) ;

}

};

Идея заключается в том, что А [ i ] возвращает прокси р, ссылающийся на А и
содержащий i. Вызов A[i] [ j ] соответствует р [ j ], который, в свою очередь, дол­
жен вызывать А (i, j). К сожалению, этот код не компилируется. Когда мы вызы­
ваем some_matrix::operator() в simple_bracket_proxy ::operator[], тип
some matrix только объявлен, но не полностью определен. Обмен местами этих
двух определений классов просто даст обратную зависимость и приведет к еще
более некомпилируемому коду. Проблема в этой реализации прокси заключается
в том, что нам нужны два полных типа, которые зависят друг от друга.
Это очень интересный аспект шаблонов: они позволяют нам разбить взаимоза­
висимость благодаря их отложенной генерации кода. Добавление параметра шаб­
лона в прокси устраняет зависимость:
template
class bracket_proxy
{
public:
bracket_proxy(MatrixS A, size_t r) : A(A), r(r)
Results operator!](size_t c){ return A(r,c); }
private:
MatrixS A;
size_t
r;
};
class some_matrix

{}

{
// ...

bracket_proxy operator!](sizet r)
{
return bracket_proxy(*this,r);

}
};

Наконец мы можем написать А [ i ] [ j ], и этот код будет внутренне выполнен
через двухаргументный оператор operator (). Теперь мы можем написать мно­
го классов матриц с совершенно разными реализациями operator (), но все они
смогут воспользоваться bracket_proxy таким же образом.

|С++11

Реализовав несколько классов матриц, мы понимаем, что operator [ ]
выглядит во всех классах матриц, по сути, одинаково: просто возвращая
прокси с аргументами, которые представляют собой ссылку на матрицу
и номер строки. Мы можем добавить только еще один CRTP-класс для
реализации operator [ ]:

6.7. Упражнения

391

template
class bracket_proxy { ... };

template ctypename Matrix, typename Result>
class crtp_matrix
{
using const_proxy = bracket_proxy;
public:
bracket_proxy operator[](sizet r)
{
return { static_cast(*this),r };
}
const_j>roxy operator[] (size t r) const
{
return { static_cast(*this),r };
}
};
class matrix
: public crtp_matrix cmatrix, double>
{

// ...
};

Заметим, что возможности C++11 используются только для краткости; мы мо­
жем реализовать этот код и в С++03, просто немного более многословно. Новый
класс matrix может предоставить operator [ ] для каждого класса matrix с опе­
ратором приложения с двумя аргументами. Однако в полноценном пакете линей­
ной алгебры мы должны обратить внимание на то, какие матрицы являются из­
меняемыми и возвращаются ли оператором ссылки или значения. Эти различия
могут безопасно обрабатываться с помощью методик метапрограммирования из
главы 5, “Метапрограммирование”.
Хотя подход с применением прокси создает дополнительный объект, наши тес­
ты показали, что использование оператора индексации такое же быстрое, как и
применение оператора приложения. По-видимому современные компиляторы до­
статочно умны, чтобы устранить действительное создание прокси-объектов.

6.7. Упражнения
6.7.1. Ромбовидное наследование без избыточности
Реализуйте ромбовидное наследование из раздела 6.3.2 так, чтобы имя выво­
дилось только один раз. В производных классах следует различать all info () и
my inf os () и вызывать эти две функции там, где нужно.

392

Объектно-ориентированное программирование

6.7.2. Наследование класса вектора
Пересмотрите пример вектора из главы 2, “Классы”. Введите базовый класс
vector_expression для size и operator (). Наследуйте vector от этого базо­
вого класса. Затем создайте класс ones, который представляет собой вектор из од­
них единиц и также наследуется от vector_expression.

6.7.3. Функция клонирования
Напишите CRTP-класс для функции-члена clone (), которая копиру­
ет текущий объект — по аналогии с функцией Java clone (http://en.
wikipedia.org/wiki/Clone_%28Java_inethod%29). Возвращаемым типом этой
функции должен быть тип клонируемого объекта.

Глава 7

Научные проекты
В предыдущих главах мы сосредоточивались главным образом на возможнос­
тях языка C++ и на том, как лучше всего применять их к относительно неболь­
шим учебным примерам. В этой, последней, главе представлены некоторые идеи
о том, как работать с куда более крупными проектами. Первый раздел (раздел
7.1) принадлежит перу друга автора Марио Мулански (Mario Mulansky) и посвя­
щен взаимодействию между библиотеками. Это позволит вам заглянуть за кулисы
библиотеки odeint — обобщенной библиотеки, которая отлично работает в очень
жесткой связи с несколькими другими библиотеками. Затем мы представим азы
понимания того, как выполнимые файлы создаются из многих исходных текстов
и архивов библиотек (раздел 7.2.1) и какие инструменты предназначены для под­
держки этого процесса (раздел 7.2.2). Наконец мы обсудим, как распределить по
нескольким файлам исходные тексты программ (раздел 7.2.3).

7.1. Реализация решателей ОДУ
Автор — Марио Мулански (Mulansky)
В этом разделе мы пройдем основные шаги разработки численной библиоте­
ки. Акцент здесь делается не столько на том, чтобы обеспечить наиболее полную
численную функциональность, сколько на создании надежного дизайна, который
обеспечит максимальную обобщенность. В качестве примера рассмотрим числен­
ные алгоритмы для поиска решения обыкновенных дифференциальных уравнений
(ОДУ). В духе главы 3, “Обобщенное программирование”, нашей целью являет­
ся сделать реализацию насколько возможно универсальной с использованием
обобщенного программирования. Начнем с кратких математических основ алго­
ритмов, за которыми последует их простая реализация. На этой основе мы смо­
жем идентифицировать отдельные части реализации и те из них, которые можно
сделать взаимозаменяемыми, чтобы достичь полностью обобщенной библиоте­
ки. Мы убеждены в том, что после изучения этого подробного примера дизайна
обобщенной библиотеки читатель сможет применить рассмотренный метод и к
другим численным алгоритмам.

394

Научные проекты

7.1.1. Обыкновенные дифференциальные уравнения
Обыкновенные дифференциальные уравнения являются одним из основных
математических инструментов для моделирования физических, биологических,
химических или социальных процессов и являются одной из наиболее важных
концепций в области науки и техники. За исключением нескольких простых слу­
чаев решение ОДУ с помощью аналитических методов не находится, так что мы
вынуждены полагаться на численные алгоритмы для получения по крайней мере
приближенного решения. В этой главе мы будем разрабатывать обобщенную реа­
лизацию алгоритма Рунге-Кутты-4, алгоритма решения ОДУ общего назначения,
широко используемого на практике благодаря своей простоте и надежности.
В общем случае обыкновенное дифференциальное уравнение представляет
собой уравнение, содержащее функцию x(t) от независимой переменной Г, и ее
производные х\ х", ...:
F(x,x',x'r, ... ,х')’

*(' = 'о)=*о.

(7.2)

Здесь мы используем векторную запись х, чтобы показать, что переменная
х может быть многомерным вектором. Как правило, ОДУ определяется для ве­
щественных переменных, т.е. х g Rx, но можно рассматривать и ОДУ с комплекс­
ными значениями, в которых х е
. Функция f (x,t) называется правой частью
ОДУ. Пожалуй, простейшим физическим примером ОДУ является гармонический
осциллятору т.е. подвешенная на пружине точечная масса. Уравнение движения
Ньютона для такой системы имеет вид
j2