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

Красивый C++ [Дж. Гай Дэвидсон] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
Красивый C++
30 главных правил чистого, безопасного
и быстрого кода

Дж. Гай Дэвидсон
Кейт Грегори

2023

ББК 32.973.2-018.1
УДК 004.43
Д94

Дэвидсон Дж. Гай, Грегори Кейт
Д94 Красивый C++: 30 главных правил чистого, безопасного и быстрого кода. —
СПб.: Питер, 2023. — 368 с.: ил. — (Серия «Для профессионалов»).
ISBN 978-5-4461-2272-1

16+

Написание качественного кода на C++ не должно быть трудной задачей. Если разработчик
будет следовать рекомендациям, приведенным в C++ Core Guidelines, то он будет писать исключительно надежные, эффективные и прекрасно работающие программы на C++. Но руководство настолько переполнено советами, что порой трудно понять, с чего начать. Начните
с «Красивого C++»!
Опытные программисты Гай Дэвидсон и Кейт Грегори выбрали 30 основных рекомендаций,
которые посчитали особенно ценными, и дают подробные практические советы, которые помогут улучшить ваш стиль разработки на C++. Для удобства книга структурирована в точном
соответствии с официальным веб-сайтом C++ Core Guidelines.
(В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)

ББК 32.973.2-018.1
УДК 004.43

Права на издание получены по соглашению с Pearson Education Inc. Все права защищены. Никакая часть
данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения
владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством
как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство
не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. Издательство не несет ответственности за
доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги
к изданию все ссылки на интернет-ресурсы были действующими.

ISBN 978-0137647842 англ.
ISBN 978-5-4461-2272-1

© 2022 Pearson Education, Inc.
© Перевод на русский язык ООО «Прогресс книга», 2023
©И
 здание на русском языке, оформление ООО «Прогресс книга»,
2023
© Серия «Для профессионалов», 2023

Оглавление
https://t.me/it_boooks
Избранные рекомендации по C++............................................................................................ 14
Предисловие.......................................................................................................................................17
Вступление...........................................................................................................................................18
О книге............................................................................................................................................ 20
Код примеров.............................................................................................................................. 23
Благодарности....................................................................................................................................24
Об авторах............................................................................................................................................26
От издательства.................................................................................................................................28

ЧАСТЬ I
BIKESHEDDING — ЭТО ПЛОХО
Глава 1.1. P.2. Придерживайтесь стандарта ISO C++.................................................... 30
Что такое стандарт ISO C++................................................................................................... 30
История С++.......................................................................................................................... 30
Инкапсуляция вариаций......................................................................................................... 32
Вариации в окружении времени выполнения...................................................... 32
Вариации на уровне языка C++ и компилятора................................................... 33
Расширения для С++.......................................................................................................... 34
Защита заголовочных файлов....................................................................................... 35
Вариации в основных типах........................................................................................... 35
Нормативные ограничения............................................................................................ 36
Изучение старых способов....................................................................................................37
Обратная совместимость в C++.................................................................................... 37
Прямая совместимость и Y2K......................................................................................... 38
Следите за последними изменениями в стандарте.................................................... 39
IsoCpp........................................................................................................................................ 39
Конференции......................................................................................................................... 40
Другие источники................................................................................................................ 40

6  Оглавление
Глава 1.2. 
F.51. Если есть выбор, используйте аргументы по умолчанию
вместо перегрузки.................................................................................................. 42
Введение........................................................................................................................................ 42
Доработка ваших абстракций: дополнительные аргументы
или перегрузка?.......................................................................................................................... 43
Тонкости разрешения перегрузки..................................................................................... 45
Вернемся к примеру................................................................................................................. 47
Однозначная природа аргументов по умолчанию..................................................... 49
Альтернативы перегрузке...................................................................................................... 50
Иногда без перегрузки не обойтись.................................................................................. 51
Подведем итог............................................................................................................................. 52
Глава 1.3. 
C.45. Не определяйте конструктор по умолчанию, который
просто инициализирует переменные-члены; для этой цели
лучше использовать внутриклассовые инициализаторы членов.....53
Зачем нужны конструкторы по умолчанию................................................................... 53
Как инициализируются переменные-члены.................................................................. 55
Что может случиться, если поддерживать класс будут два человека............... 58
Сборная солянка из конструкторов........................................................................... 58
Аргументы по умолчанию могут запутать ситуацию
в перегруженных функциях............................................................................................ 60
Подведем итог............................................................................................................................. 60
Глава 1.4. 
C.131. Избегайте тривиальных геттеров и сеттеров............................... 62
Архаичная идиома.....................................................................................................................62
Абстракции.................................................................................................................................... 63
Простая инкапсуляция............................................................................................................. 66
Инварианты класса....................................................................................................................69
Существительные и глаголы.................................................................................................. 71
Подведем итог............................................................................................................................. 72
Глава 1.5. 
ES.10. Объявляйте имена по одному
в каждом объявлении........................................................................................... 73
Позвольте представить............................................................................................................73
Обратная совместимость.......................................................................................................76
Пишите более ясные объявления....................................................................................... 77
Структурное связывание........................................................................................................ 78
Подведем итог............................................................................................................................. 79

Оглавление  7

Глава 1.6. 
NR.2. Функции не обязательно должны иметь
только один оператор возврата....................................................................... 80
Правила меняются.....................................................................................................................80
Гарантия очистки........................................................................................................................83
Идиома RAII................................................................................................................................... 85
Пишите хорошие функции..................................................................................................... 88
Подведем итог............................................................................................................................. 90

ЧАСТЬ II
НЕ НАВРЕДИТЕ СЕБЕ
Глава 2.1. 
P.11. Инкапсулируйте беспорядочные конструкции,
а не разбрасывайте их по всему коду............................................................ 92
Все одним глотком.....................................................................................................................92
Что означает инкапсулировать запутанную конструкцию..................................... 94
Назначение языка и природа абстракции...................................................................... 96
Уровни абстракции..................................................................................................................100
Абстракция путем рефакторинга и проведения линии.........................................101
Подведем итог...........................................................................................................................102
Глава 2.2. 
I.23. Минимизируйте число параметров в функциях...........................103
Сколько они должны получать?........................................................................................103
Упрощение через абстрагирование................................................................................105
Делайте так мало, как возможно, но не меньше........................................................107
Примеры из реальной жизни.............................................................................................109
Подведем итог...........................................................................................................................111
Глава 2.3. 
I.26. Если нужен кросс-компилируемый ABI,
используйте подмножество в стиле C.........................................................112
Создавайте библиотеки.........................................................................................................112
Что такое ABI...............................................................................................................................114
Сокращайте до абсолютного минимума........................................................................115
Распространение исключений...........................................................................................118
Подведем итог...........................................................................................................................119
Глава 2.4. 
C.47. Определяйте и инициализируйте
переменные-члены в порядке их объявления........................................121
Подведем итог...........................................................................................................................131

8  Оглавление
Глава 2.5. 
CP.3. Сведите к минимуму явное совместное использование
записываемых данных........................................................................................132
Традиционная модель выполнения................................................................................132
Подождите, это еще не все...................................................................................................134
Предотвращение взаимоблокировок и гонок за данными..................................137
Отказ от блокировок и мьютексов...................................................................................140
Подведем итог...........................................................................................................................143
Глава 2.6. 
Т.120. Используйте метапрограммирование шаблонов,
только когда это действительно необходимо.........................................144
std::enable_if => requires........................................................................................................152
Подведем итог...........................................................................................................................156

ЧАСТЬ III
ПРЕКРАТИТЕ ЭТО ИСПОЛЬЗОВАТЬ
Глава 3.1. 
I.11. Никогда не передавайте владение через простой
указатель (T*) или ссылку (T&).........................................................................158
Использование области свободной памяти................................................................158
Производительность интеллектуальных указателей..............................................161
Использование простой семантики ссылок................................................................163
gsl::owner......................................................................................................................................164
Подведем итог...........................................................................................................................167
Глава 3.2. 
I.3. Избегайте синглтонов..................................................................................168
Глобальные объекты — это плохо....................................................................................168
Шаблон проектирования «Синглтон».............................................................................169
Фиаско порядка статической инициализации...........................................................170
Как скрыть синглтон................................................................................................................173
Только один из них должен существовать в каждый момент
работы кода................................................................................................................................174
Подождите минутку.................................................................................................................176
Подведем итог...........................................................................................................................179
Глава 3.3. 
C.90. Полагайтесь на конструкторы и операторы присваивания
вместо memset и memcpy.................................................................................180
В погоне за максимальной производительностью..................................................180
Ужасные накладные расходы конструкторов.............................................................181
Самый простой класс..............................................................................................................183

Оглавление  9

О чем говорит стандарт.........................................................................................................185
А как же memcpy?....................................................................................................................188
Никогда не позволяйте себе недооценивать компилятор...................................189
Подведем итог...........................................................................................................................191
Глава 3.4. 
ES.50. Не приводите переменные
с квалификатором const к неконстантному типу...................................192
Работа с большим количеством данных........................................................................193
Брандмауэр const.....................................................................................................................195
Реализация двойного интерфейса...................................................................................196
Кэширование и отложенные вычисления....................................................................198
Два вида const............................................................................................................................199
Сюрпризы const........................................................................................................................201
Подведем итог...........................................................................................................................202
Глава 3.5. 
E.28. При обработке ошибок избегайте глобальных состояний
(например, errno)..................................................................................................204
Обрабатывать ошибки сложно..........................................................................................204
Язык C и errno.............................................................................................................................204
Коды возврата............................................................................................................................206
Исключения................................................................................................................................207
...........................................................................................................................208
Boost.Outcome...........................................................................................................................209
Почему обрабатывать ошибки так сложно..................................................................210
Свет в конце туннеля..............................................................................................................212
Подведем итог...........................................................................................................................214
Глава 3.6. 
SF.7. Не используйте using namespace в глобальной области
видимости в заголовочном файле................................................................215
Не делайте этого.......................................................................................................................215
Неоднозначность.....................................................................................................................216
Использование using..............................................................................................................217
Куда попадают символы........................................................................................................219
Еще более коварная проблема..........................................................................................222
Решение проблемы операторов разрешения области видимости..................223
Искушение и расплата...........................................................................................................225
Подведем итог...........................................................................................................................226

10  Оглавление

ЧАСТЬ IV
ИСПОЛЬЗУЙТЕ НОВУЮ ОСОБЕННОСТЬ ПРАВИЛЬНО
Глава 4.1. 
F.21. Для возврата нескольких выходных значений
используйте структуры или кортежи...........................................................228
Форма сигнатуры функции..................................................................................................228
Документирование и аннотирование............................................................................230
Теперь можно вернуть объект...........................................................................................231
Можно также вернуть кортеж............................................................................................234
Передача и возврат по неконстантной ссылке..........................................................237
Подведем итог...........................................................................................................................240
Глава 4.2. 
Enum.3. Старайтесь использовать классы-перечисления
вместо простых перечислений.......................................................................241
Константы....................................................................................................................................241
Перечисления с заданной областью видимости.......................................................244
Базовый тип................................................................................................................................246
Неявное преобразование....................................................................................................247
Подведем итог...........................................................................................................................249
Глава 4.3. 
ES.5. Минимизируйте области видимости.................................................250
Природа области видимости..............................................................................................250
Область видимости блока....................................................................................................251
Область видимости пространства имен........................................................................253
Область видимости класса...................................................................................................256
Область видимости параметров функции....................................................................258
Область видимости перечисления..................................................................................259
Область действия параметра шаблона..........................................................................260
Область видимости как контекст......................................................................................261
Подведем итог...........................................................................................................................262
Глава 4.4. 
Con.5. Используйте constexpr для определения значений,
которые можно вычислить на этапе компиляции.................................263
От const к constexpr.................................................................................................................263
С++ по умолчанию...................................................................................................................265
Использование constexpr.....................................................................................................267
inline...............................................................................................................................................271
consteval........................................................................................................................................272

Оглавление  11

constinit.........................................................................................................................................273
Подведем итог...........................................................................................................................275
Глава 4.5. 
T.1. Используйте шаблоны для повышения уровня
абстрактности кода..............................................................................................276
Повышение уровня абстракции........................................................................................278
Шаблоны функций и абстракция......................................................................................280
Шаблоны классов и абстракция........................................................................................283
Выбор имени — сложная задача......................................................................................285
Подведем итог...........................................................................................................................286
Глава 4.6. 
T.10. Задавайте концепции для всех аргументов шаблона................287
Как мы здесь оказались?.......................................................................................................287
Ограничение параметров....................................................................................................290
Как абстрагировать свои концепции..............................................................................293
Разложение на составляющие через концепции......................................................296
Подведем итог...........................................................................................................................297

ЧАСТЬ V
ПИШИТЕ ХОРОШИЙ КОД ПО УМОЛЧАНИЮ
Глава 5.1. 
P.4. В идеале программа должна быть статически
типобезопасной.....................................................................................................300
Безопасность типов — это средство защиты в C++.................................................300
Объединения..............................................................................................................................302
Приведение.................................................................................................................................304
Целые без знака........................................................................................................................307
Буферы и размеры...................................................................................................................310
Подведем итог...........................................................................................................................311
Глава 5.2. 
P.10. Неизменяемые данные предпочтительнее изменяемых.........312
Неправильные значения по умолчанию.......................................................................312
const в объявлениях функций............................................................................................315
Подведем итог...........................................................................................................................319
Глава 5.3. 
I.30. Инкапсулируйте нарушения правил...................................................320
Сокрытие неприглядных вещей........................................................................................320
Поддержание видимости, что все в порядке..............................................................322
Подведем итог...........................................................................................................................327

12  Оглавление
Глава 5.4. 
ES.22. Не объявляйте переменные, пока не получите
значения для их инициализации...................................................................329
Важность выражений и операторов...............................................................................329
Объявление в стиле C.............................................................................................................330
Объявление с последующей инициализацией..........................................................332
Максимальное откладывание объявления..................................................................333
Локализация контекстно зависимой функциональности.....................................335
Устранение состояния............................................................................................................337
Подведем итог...........................................................................................................................339
Глава 5.5. 
Per.7. При проектировании учитывайте возможность
последующей оптимизации.............................................................................340
Максимальная частота кадров...........................................................................................340
Работа вдалеке от железа.....................................................................................................342
Оптимизация через абстракцию.......................................................................................346
Подведем итог...........................................................................................................................349
Глава 5.6. 
E.6. Используйте идиому RAII для предотвращения
утечек памяти.........................................................................................................350
Детерминированное уничтожение..................................................................................350
Утечка файлов............................................................................................................................353
Почему это так важно.............................................................................................................356
Все это выглядит чересчур сложным: будущие возможности............................358
Где все это получить................................................................................................................361
Заключение........................................................................................................................................364
Послесловие......................................................................................................................................366

Брину,
Шинейд,
Рори и Лоис.
C.47: с любовью, Гай Дэвидсон
Джиму Эллисону, хотя он едва ли прочитает эти строки.
Весь в исследовательской работе. Хлое и Аише, которые
прежде не упоминались на первых страницах книг.
Кейт Грегори

Избранные
рекомендации по C++

P.2. Придерживайтесь стандарта ISO C++ (глава 1.1).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-Cplusplus

P.4. В идеале программа должна быть статически типобезопасной (глава 5.1).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-typesafe

P.10. Неизменяемые данные предпочтительнее изменяемых (глава 5.2).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-mutable

P.11. Инкапсулируйте беспорядочные конструкции, а не разбрасывайте
их по всему коду (глава 2.1).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rp-library

I.3. Избегайте синглтонов (глава 3.2).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-singleton

I.11. Никогда не передавайте владение через простой указатель (T*) или
ссылку (T&) (глава 3.1).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-raw

I.23. Минимизируйте число параметров в функциях (глава 2.2).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-nargs

I.26. Если нужен кросс-компилируемый ABI, используйте подмножество
в стиле C (глава 2.3).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-abi

I.30. Инкапсулируйте нарушения правил (глава 5.3).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Ri-encapsulate

F.21. Для возврата нескольких выходных значений используйте структуры
или кортежи (глава 4.1).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-out-multi

Избранные рекомендации по C++  15

F.51. Если есть выбор, используйте аргументы по умолчанию вместо перегрузки (глава 1.2).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rf-default-args

C.45. Не определяйте конструктор по умолчанию, который просто инициа­
лизирует переменные-члены; для этой цели лучше использовать внутриклассовые инициализаторы членов (глава 1.3).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-default

C.47. Определяйте и инициализируйте переменные-члены в порядке их
объявления (глава 2.4).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-order

C.90. Полагайтесь на конструкторы и операторы присваивания вместо
memset и memcpy (глава 3.3).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-memset

C.131. Избегайте тривиальных геттеров и сеттеров (глава 1.4).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c131-avoid-trivial-gettersand-setters

Enum.3. Старайтесь использовать классы-перечисления вместо простых
перечислений (глава 4.2).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Renum-class

ES.5. Минимизируйте области видимости (глава 4.3).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-scope

ES.10. Объявляйте имена по одному в каждом объявлении (глава 1.5).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-name-one

ES.22. Не объявляйте переменные, пока не получите значения для их инициализации (глава 5.4).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-init

ES.50. Не приводите переменные с квалификатором const к неконстантному
типу (глава 3.4).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-casts-const

Per.7. При проектировании учитывайте возможность последующей оптимизации (глава 5.5).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rper-efficiency

16  Избранные рекомендации по C++

CP.3. Сведите к минимуму явное совместное использование записываемых
данных (глава 2.5).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rconc-data

E.6. Используйте идиому RAII для предотвращения утечек памяти (глава 5.6).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Re-raii

E.28. При обработке ошибок избегайте глобальных состояний (например,
errno) (глава 3.5).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Re-no-throw

Con.5. Используйте constexpr для определения значений, которые можно
вычислить на этапе компиляции (глава 4.4).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rconst-constexpr

T.1. Используйте шаблоны для повышения уровня абстрактности кода
(глава 4.5).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-raise

T.10. Задавайте концепции для всех аргументов шаблона (глава 4.6).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-concepts

T.120. Используйте метапрограммирование шаблонов, только когда это
действительно необходимо (глава 2.6).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-metameta

SF.7. Не используйте using namespace в глобальной области видимости
в заголовочном файле (глава 3.6).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rs-using-directive

NR.2. Функции не обязательно должны иметь только один оператор возврата (глава 1.6).
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rnr-single-return

Предисловие

Я получил истинное удовольствие, прочитав книгу «Красивый C++». Особенно мне понравилось, что она представляет основные рекомендации C++
совсем не так, как C++ Core Guidelines1. В Руководстве правила описываются
довольно кратко, густо сдобрены техническими терминами и предполагают широкое использование средств статического анализа. Эта же книга
рассказывает истории, в большинстве своем взятые из игровой индустрии
и основанные на эволюции кода и методов на протяжении десятилетий.
В ней правила представлены с точки зрения разработчика с акцентом на
преимуществах, которые можно получить, следуя этим правилам, и на неприятностях, которые могут возникнуть в результате их игнорирования.
Каждое правило сопровождается более обширным обсуждением, чем может
предложить C++ Core Guidelines.
Руководство стремится максимально полно охватить все правила. Естественно, подбор правил написания хорошего кода в этой книге по умолчанию не может быть полным руководством по языку. Но если ограничиться
степенью полноты, необходимой для понимания обсуждаемой проблемы,
то станет очевидно: Руководство C++ Core не предназначено для систематического чтения. Я рекомендую прочитать в нем разделы In: Introduction
и P: Philosophy, чтобы получить представление о цели этого руководства
и его концептуальной основе. А для выборочного знакомства с основными
правилами создания хорошего кода, исходя из вкуса, своего видения и опыта, я советую прочитать книгу, которую вы держите в руках. Для истинных
профессионалов это будет легким и увлекательным чтением. А большинство
остальных разработчиков смогут узнать из нее что-то новое и полезное.
Бьерн Страуструп (Bjarne Stroustrup),
июнь 2021 года

1

Руководство на английском языке свободно доступно по адресу https://github.com/
isocpp/CppCoreGuidelines. — Примеч. пер.

Вступление

Сложность разработки программ на C++ уменьшается с выходом каждого нового стандарта и каждой новой книги. Конференций, блогов и книг
более чем достаточно, и это хорошо. Но в мире не хватает инженеров
с достаточно высоким уровнем подготовки для решения вполне реальных
задач.
Несмотря на постоянное упрощение языка, еще многое предстоит узнать
о том, как писать хороший код на C++. Бьерн Страуструп, изобретатель
языка C++, и Герб Саттер (Herb Sutter), руководитель органа по стандартизации C++, посвятили много сил и времени созданию учебных материалов
как для изучения C++, так и для повышения квалификации разработчиков
на C++. Среди них можно назвать The C++ Programming Language1 и A Tour
of C++2, а также Exceptional C++3 и C++ Coding Standards4.
Проблема книг даже такого скромного
объема заключается в том, что они отражают состояние дел на момент их
издания, тогда как C++ — это постоянно развивающийся язык. То, что было
хорошим советом в 1998 году, может
потерять актуальность. Развивающемуся языку нужен развивающийся путеводитель.

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

1

Stroustrup B. The C++ Programming Language, Fourth Edition. — Boston: Addison-Wesley,
2013 (Страуструп Б. Язык программирования C++. 4-е изд.).

2

Stroustrup B. A Tour of C++, Second Edition. — Boston: Addison-Wesley, 2018 (Страус­
труп Б. Язык программирования C++. Краткий курс. 2-е изд.).

3

Sutter H. Exceptional C++. — Reading, MA: Addison-Wesley, 1999 (Саттер Г. Новые
сложные задачи на C++).

4

Sutter H., Alexandrescu A. C++ Coding Standards. — Boston: Addison-Wesley, 2004
(Саттер Г., Александреску А. Стандарты программирования на С++).

Вступление  19

На конференции CppCon в 2015 году Бьерном Страуструпом и Гербом
Саттером в ходе их двух1 основных докладов2 был запущен онлайн-ресурс
C++ Core Guidelines3. В нем содержатся простые и практичные советы
по улучшению стиля кода на C++, чтобы вы могли писать правильный,
производительный и эффективный код с первой попытки. Это постоянно
развивающийся документ, в котором нуждаются специа­листы, пишущие
на C++, и авторы будут рады, если вы пришлете им свои предложения
с исправлениями и улучшениями. Желательно, чтобы все, от новичков до
ветеранов, следовали советам из этого Руководства.
В конце февраля 2020 года на #include discord4 Кейт Грегори (Kate Gregory)
заявила, что хотела бы издать книгу о Core Guidelines, и я, Гай Дэвидсон, сразу
ухватился за эту идею. Кейт выступила на CppCon 20175, где рассмотрела
только десять основных правил. Я разделяю ее энтузиазм по продвижению
передовых приемов программирования.
Я возглавляю отдел инженерно-технических методов в Creative Assembly,
старейшей и крупнейшей британской студии, занимающейся разработкой
игр. В нем я проработал большую часть из последних 20 с лишним лет,
помогая превращать наших замечательных инженеров в великих специа­
листов. По нашему наблюдению, несмотря на доступность и простоту Core
Guidelines, разработчики мало знакомы с этим Руководством. Мы в меру
своих сил и возможностей пропагандируем его и поэтому решили написать
книгу, потому что литературы о рекомендациях и правилах, перечисленных
в Руководстве, пока недостаточно.
Собственно Core Guidelines можно найти по адресу https://isocpp.github.io/
CppCoreGuidelines/CppCoreGuidelines. Оно наполнено замечательными советами, из-за чего порой трудно понять, с чего начать. Можно, конечно, читать
1

Youtube.com. 2021. CppCon 2015: Stroustrup B. Writing Good C++14. Доступно по
адресу https://www.youtube.com/watch?v=1OEu9C51K2A.

2

Youtube.com. 2021. CppCon 2015: Sutter H. Writing Good C++14... By Default. Доступно
по адресу https://www.youtube.com/watch?v=hEx5DNLWGgA.

3

Isocpp.github.io. 2021. C++ Core Guidelines. Copyright © Standard C++ Foundation
and its contributors. Доступно по адресу https://isocpp.github.io/CppCoreGuidelines/
CppCoreGuidelines.

4

#include . 2021. #include . Доступно по адресу https://www.includecpp.org/.

5

Youtube.com. 2021. CppCon 2017: Gregory K. 10 Core Guidelines You Need to Start Using
Now. Доступно по адресу https://www.youtube.com/watch?v=XkDEzfpdcSg.

20  Вступление

все подряд, сверху вниз, но понять и усвоить весь набор рекомендаций без
повторного чтения практически невозможно. Рекомендации организованы
в 22 основных раздела с такими названиями, как Interfaces («Интерфейсы»),
Functions («Функции»), Concurrency («Конкуренция») и т. д. Каждый
раздел включает отдельный набор правил и рекомендаций, иногда исчисляемых единицами, иногда десятками. Рекомендации идентифицируются
по первой букве из названия раздела и номеру рекомендации в разделе.
Например, «F.3. Функции должны быть короткими и простыми» — третья
рекомендация в разделе F, Functions.
Все рекомендации построены по одному формату. Они начинаются с названия, которое представлено как посыл к действию (делайте это, не делайте этого, избегайте этого, предпочитайте это), затем следует причина
и несколько примеров, а также исключения из правила (если они есть).
В конце дается примечание, описывающее, как обеспечить соблюдение
данного правила. Примечания по применению включают и советы авторам
инструментов статического анализа, и советы разработчикам, как проводить
проверку кода. Чтобы читать эти рекомендации, нужен определенный навык; решение о том, какие из них взять на вооружение, является вопросом
личных предпочтений. Позвольте нам продемонстрировать, как начать
пользоваться мудростью этого Руководства.
В C++ есть свои острые углы, есть и свои пыльные тайники, которые не так
часто посещаются в современном C++. Мы хотим увести вас от них и показать, что C++ совсем не трудный и не сложный язык и его использование
вполне можно доверить большинству разработчиков.

О КНИГЕ
В этой книге мы предлагаем 30 избранных рекомендаций по программированию на C++, которые сами считаем лучшими. Мы подробно объясняем
эти рекомендации в надежде, что вы будете придерживаться хотя бы их,
если решите не исследовать остальные советы в Core Guidelines. Рекомендации, которые мы отобрали, не обязательно являются самыми важными,
но они, вне всяких сомнений, изменят ваш код к лучшему. Конечно, полезно ознакомиться также со множеством других рекомендаций и следовать
им. Мы надеемся, что вы прочтете остальные советы и попробуете их

Вступление  21

применить к своему коду. Руководство предназначено для всех разработчиков на C++, с любым уровнем опыта, и эта книга адресована тому же кругу
людей. Материал не усложняется по ходу книги, и главы можно читать
в любом порядке. Они не зависят друг от друга, хотя кое-где присутствуют
ссылки на другие главы. Объем каждой главы примерно 3000 слов, так
что вы можете считать эту книгу скорее томиком для чтения на диване
по вечерам, чем учебником. Цель книги не в том, чтобы научить вас программировать на C++, а чтобы познакомить с советами, как улучшить
свой стиль.
Мы разделили рекомендации на пять частей по шесть глав в соответствии
с первоначальной презентацией Кейт Грегори на CppCon в 2017 году.
В части I «Bikeshedding — это плохо» мы представляем рекомендации,
которые помогут выбрать верное решение из нескольких вариантов и двигаться дальше с минимумом суеты и споров. Понятие bikeshedding1 (bike —
«велосипед», shed — «навес», то есть bikeshedding — «ставить велосипед
под навес». — Примеч. ред.) заимствовано из «закона тривиальности»
К. Норткота Паркинсона (C. Northcote Parkinson), согласно которому
члены организации нередко придают несоразмерное значение тривиальным
вопросам, таким как выбор цвета навеса для велосипедов, вместо критериев испытаний атомной электростанции, к которой он, навес, прилагается.
Какова причина тривиального подхода? Да просто велосипед — это единственная вещь, о которой все хоть что-то знают.
В части II «Не навредите себе» мы предоставляем рекомендации по предотвращению травм при написании кода. Одна из проблем, связанных
с остаточной сложностью C++, заключается в возможности в некоторых
ситуациях выстрелить себе в ногу. Например, несмотря на допустимость
заполнять список инициализации конструктора в любом порядке, никогда
не следует этого делать.
Часть III называется «Прекратите это использовать» и касается элементов
языка, сохраненных ради обратной совместимости, а также советов, которые
раньше считались ценными, но утеряли свою значимость благодаря нововведениям в языке. По мере развития C++ то, что раньше казалось хорошей
1

2021. Доступно по адресу https://exceptionnotfound.net/bikeshedding-the-dailysoftware-anti-pattern/.

22  Вступление

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

Цель книги не в том,
чтобы научить вас
программировать
на C++, а чтобы познакомить с советами, как улучшить
свой стиль.

Далее следует часть IV под заголовком «Используйте новую особенность
правильно». Такие особенности, как концепции, constexpr, структурное
связывание и т. д., требуют осторожности при развертывании. Опять же
C++ — это развивающийся стандарт, и с каждым выпуском появляется
что-то новое, требующее освоения для поддержки. Хотя эта книга не ставит
своей целью научить вас новым возможностям, которые определяются
стандартом C++20, эти рекомендации помогут вам понять, как воспринимать их.
Часть V, последняя, называется «Пишите хороший код по умолчанию».
Здесь описываются простые рекомендации, которые, если следовать им,
помогут вам писать хороший код, не сильно задумываясь о происходящем.
Придерживаясь их, вы научитесь писать красивый идиоматический код
на C++, и он будет понят и оценен вашими коллегами.
На протяжении всей книги, как и в любом хорошем повествовании,
возникает и развивается обсуждение разных тем. Мы, авторы, испытывали особое удовольствие от возможности увидеть мотивы, стоящие за
рекомендациями, и проанализировать широкое их применение. Надеемся,
что такое же удовольствие испытаете и вы при чтении. Многие рекомендации, если внимательно присмотреться, просто иначе формулируют
некоторые из фундаментальных истин разработки программного обеспечения. Знание этих истин значительно улучшит ваши навыки программирования.
Мы искренне надеемся, что эта книга вам понравится и принесет пользу1.
1

Как уже понятно из введения и вступительного слова авторов, эта книга не просто
о программировании, а о программировании, поданном через призму другой книги,
название которой — C++ Core Guideline. Чтобы отличать ее от прочих руководств
по языку С, упоминаемых далее по тексту, она фигурирует как Руководство — с прописной буквы. Все остальные пособия и труды называются в книге просто руководствами. — Примеч. ред.

Вступление  23

КОД ПРИМЕРОВ
Все примеры кода доступны на сайте Compiler Explorer. Мэтт Годболт (Matt
Godbolt) любезно зарезервировал постоянные ссылки для каждой главы,
которые формируются путем присоединения номера главы к базовому
адресу https://godbolt.org/z/cg30-ch. Например, ссылка https://godbolt.org/z/
cg30-ch1.3 приведет вас к примерам кода для главы 1.3. Мы рекомендуем
начать с https://godbolt.org/z/cg30-ch0.0, где приводятся инструкции по использованию веб-сайта и взаимодействию с кодом.
Гай Дэвидсон (Guy Davidson),
@hatcat01 hatcat.com.
Кейт Грегори (Kate Gregory),
@gregcons gregcons.com.
Октябрь 2021 года

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

Годы 2020-й и 2021-й были для нас довольно неспокойными, и мы хотели бы
поблагодарить многих людей, так или иначе оказавших нам поддержку,
когда мы работали над этой книгой.
Конечно же, мы хотим поблагодарить Бьерна Страуструпа и Герба Саттера
за создание рекомендаций Core Guidelines и за то, что побудили нас написать о них. Мы также хотим поблагодарить участников CppCon за их
помощь в обсуждении некоторых из этих рекомендаций.
Наши семьи оказали жизненно необходимую поддержку во время процесса
написания, требующего уединения и сосредоточенности. Без поддержки
со стороны самых близких нам было бы значительно тяжелее работать.
Легион друзей в дискорд-группе #include со штаб-квартирой на inclu­
decpp.org продолжает поддерживать нас в нашей повседневной практике
использования C++ с июля 2017 года1. Мы пожертвуем вам одну десятую
нашего дохода от этой книги. Низкий вам поклон.
Свою помощь нам оказали также несколько членов комитета ISO WG21
C++, поддерживающего стандарт. Мы хотели бы поблагодарить Майкла
Вонга (Michael Wong) и Тони ван Эрда (Tony van Eerd) за их участие.
Все примеры кода доступны на Compiler Explorer2 по постоянным и понятным ссылкам, благодаря любезности Мэтта Годболта, создателя этого
прекрасного сервиса. Мы выражаем ему нашу благодарность и уверяем, что
его усилия оказались, несомненно, очень важными для сообщества C++.
Cppreference.com3 послужил нам отличным исследовательским инструментом при первоначальной подготовке каждой главы, поэтому мы весьма признательны создателю и владельцу сайта Нейту Колю (Nate Kohl),
администраторам Повиласу Канапицкасу (Povilas Kanapickas) и Сергею
1

https://twitter.com/hatcat01/status/885973064600760320

2

https://godbolt.org/z/cg30-ch0.0

3

https://ru.cppreference.com/w

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

Зубкову (Sergey Zubkov), а также Тиму Сонгу (Tim Song) и всем другим
участникам проекта и благодарим их за неустанную поддержку этого прекрасного ресурса. Они — герои сообщества.
После написания главы 3.6 нам стало ясно, что своим вдохновением мы во
многом обязаны статье Артура О’Дуайера (Arthur O’Dwyer). Большое ему
спасибо за его неизменную преданную службу обществу. В его блоге также
можно найти рассказы о предпринимаемых им усилиях по раскрытию некоторых самых ранних приключенческих текстовых игровых программ
1970-х и 1980-х годов1.
Для такой книги, как эта, нужна армия редакторов, поэтому мы выражаем благодарность Бьерну Страуструпу, Роджеру Орру (Roger Orr), Клэр
Макрей (Clare Macrae), Артуру О’Дуайеру, Ивану Чукичу (Ivan uki ),
Райнеру Гримму (Rainer Grimm) и Мэтту Годболту.
Неоценимую помощь нам оказала и команда Addison-Wesley, поэтому мы
выражаем огромную благодарность Грегори Доенчу (Gregory Doench),
Одри Дойл (Audrey Doyle), Асвини Кумару (Aswini Kumar), Менке Мехте
(Menka Mehta), Джули Нахил (Julie Nahil) и Марку Таберу (Mark Taber).

1

https://quuxplusone.github.io/blog

Об авторах

Дж. Гай Дэвидсон впервые познакомился с компьютерами благодаря Acorn
Atom в 1980 году. Еще будучи подростком, он писал игры для различных
домашних компьютеров: Sinclair Research ZX81 и ZX Spectrum, а также Atari
ST. После получения степени по математике в Университете Сассекса он
увлекся театром и играл на клавишных инструментах в соул-группе. В начале 1990-х стал заниматься разработкой приложений для презентаций,
ав 1997-м перешел в игровую индустрию, начав работать в Codemasters
в их лондонском офисе.
В 1999 году перешел в Creative Assembly, где сейчас возглавляет отдел инженерно-технических методов. Работает над франшизой Total War, курируя
дискографию, а также формулируя и развивая стандарты программирования
в команде инженеров. Входит в состав консультативных советов IGGI, группы BSI C++ и комитета ISO C++. Занимает пост ответственного за стандарты
в комитете ACCU и входит в программный комитет конференции ACCU.
Является модератором на дискорд-сервере #include . Отвечает за
внутреннюю политику и нормы в нескольких организациях. Его можно
увидеть на конференциях и встречах по C++, особенно на посвященных
добавлению методов линейной алгебры в стандартную библиотеку.
В свободное время он оказывает наставническую поддержку по вопросам
программирования на C++ через Prospela и BAME in Games; помогает
школам, колледжам и университетам через UKIE, STEMNet и в качестве
Video Game Ambassador; практикует и преподает тай-чи в стиле У; изучает
игру на фортепиано; поет первый бас в Брайтонском фестивальном хоре;
управляет местным киноклубом; является членом BAFTA с правом голоса;
дважды баллотировался (безуспешно) на выборах в местный совет от имени
партии зеленых Англии и Уэльса; пытается выучить испанский. Иногда
его можно встретить за карточным столом играющим в бридж по пенни
за очко. Вероятно, у него есть и другие увлечения: он большой непоседа.
Кейт Грегори познакомилась с программированием в Университете
Ватерлоо в 1977 году и никогда не оглядывалась назад с сомнением или

Об авторах  27

сожалением. Имеет степень в области химического машиностроения, что
лишний раз подтверждает, что диплом не всегда говорит о наклонностях
человека. На цокольном этаже ее сельского дома в Онтарио есть небольшая
комната со старыми компьютерами PET, C64, домашней системой 6502
и т. д., служащими напоминаниями о более простых временах. С 1986 года
вместе с мужем руководит компанией Gregory Consulting, помогая клиентам по всему миру.
Кейт выступала с докладами на пяти континентах, любит искать заковыристые головоломки и затем делиться их решением, а также проводит много
времени, добровольно участвуя в различных мероприятиях, посвященных
языку C++. Самым уважаемым из них является группа #include ,
которая оказывает огромное влияние на эту отрасль, делает программирование на C++ более гостеприимным и дружелюбным. Их дискорд-сервер —
теплое, уютное место для изучения C++ новичками и одновременно каюткомпания для совместной работы над статьями для WG21, позволяющими
взглянуть по-иному на язык, который мы все используем, или же… что-то
среднее между ними двумя.
Ее отрывают от клавиатуры внуки, озера и кемпинги Онтарио, весла для
каноэ и дым костра, а также соблазны аэропортов по всему миру. Гурманка,
игрок в настольные игры, безотказная палочка-выручалочка, не способная
ответить отказом на просьбу о помощи, она так же активна в реальной жизни, как и в интернете, но менее ярка и заметна. После того как в 2016 году
пережила меланому IV стадии, она стала меньше беспокоиться о том, что
думают другие и чего от нее ожидают, и больше о том, чего она хочет для
своего будущего. Это дает свои результаты1.

1

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

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.

I
BIKESHEDDING —
ЭТО ПЛОХО
https://t.me/it_boooks
Глава 1.1 P.2. Придерживайтесь стандарта ISO C++.
Глава 1.2 F.51. Если есть выбор, используйте аргументы по умолчанию
вместо перегрузки.
Глава 1.3 C.45. Не определяйте конструктор по умолчанию, который
просто инициализирует переменные-члены; для этой цели
лучше использовать внутриклассовые инициализаторы
членов.
Глава 1.4 C.131. Избегайте тривиальных геттеров и сеттеров.
Глава 1.5 ES.10. Объявляйте имена по одному в каждом объявлении.
Глава 1.6 NR.2. Функции не обязательно должны иметь только один
оператор возврата.

ГЛАВА 1.1

P.2. Придерживайтесь
стандарта ISO C++

ЧТО ТАКОЕ СТАНДАРТ ISO C++
Эта книга посвящена приемам написания хорошего кода. Поэтому первый совет — придерживайтесь стандарта ISO C++. Но что это означает на
практике?

История С++
C++ первоначально не был стандартизированным языком. Это было расширение языка программирования C, изобретенное Бьерном Страуструпом1. Оно получило название «C с классами». В то время язык C тоже
не был стандартизированным: Бьерн представил свое расширение в виде
препроцессора под названием Cpre. Препроцессор обес­печивал поддержку
классов и производных классов с общедоступными/приватными уровнями
доступа, дружественными классами, перегрузкой операторов присваивания,
конструкторами и деструкторами. Кроме того, препроцессор поддерживал
встраиваемые функции и аргументы функций по умолчанию, а также проверку типов аргументов функций.
В 1982 году Бьерн начал работу над новым проектом под названием C++,
включающим дополнительные возможности, такие как виртуальные функции, перегрузка функций и операторов, ссылки, константы и динамическое
распределение памяти. Он также создал интерфейс C++ для компиляторов C под названием Cfront. Код на C++ передавался Cfront, который
1

Stroustrup B. 1995. A History of C++: 1979–1991, www.stroustrup.com/hopl2.pdf.

P.2. Придерживайтесь стандарта ISO C++  31

затем компилировал его в код на языке C. Он также написал книгу под
названием The C++ Programming Language (широко известную как TCPL),
опубликованную в 1985 году. Она послужила определяющим руководством
по языку C++, благодаря ей стали появляться коммерческие компиляторы.
Даже после широкого распространения этих компиляторов Бьерн продолжал работать над C++, добавляя новые возможности в то, что стало называться C++2.0. К ним относятся множественное наследование, абстрактные
базовые классы, статические и константные функции-члены, защищенный
уровень доступа, а также улучшения существующих возможностей. В тот
период наблюдался лавинообразный рост популярности C++. По оценкам
Бьерна, количество пользователей удваивалось каждые 7,5 месяца.
Появились конференции, журналы и книги, а конкурирующие между
собой реализации компилятора продемонстрировали, что необходимо
нечто более точное и регламентирующее, чем TCPL. В 1989 году Дмитрий
Ленков (Dmitry Lenkov) из HP написал предложение по стандартизации
C++ в Американском национальном институте стандартов (American
National Standards Institute, ANSI). В нем он указывал на необходимость
тщательного и подробного определения каждой особенности языка для
предотвращения неконтролируемого увеличения числа диалектов. Он также определял необходимость реализации дополнительных возможностей,
таких как обработка исключений и создание стандартной библиотеки.
Комитет ANSI C++, X3J16, впервые собрался в декабре 1989 года. Анно­
тированное справочное руководство (Annotated Reference Manual, ARM),
написанное Маргарет Эллис (Margaret Ellis) и Бьерном и опубликованное в 1990 году, стало единым основополагающим описанием всего C++.
Это руководство было создано специально, чтобы ускорить начало работ
по стандартизации ANSI C++.
Конечно, это был не только американский проект, и в нем приняли участие
многие представители других стран. В 1991 году был созван комитет ISO
C++ WG21, и с тех пор эти два комитета проводили совместные заседания
с целью выработать черновой стандарт для публичного рассмотрения через четыре года, с надеждой на появление официального стандарта двумя
годами после. Однако первый стандарт, ISO/IEC 14882:1998, был опубликован только в сентябре 1998-го, почти через девять лет после упомянутого
первого собрания.
Но история на этом не заканчивается. Работа над исправлением ошибок
в стандарте продолжалась, и в 2003 году был опубликован стандарт C++03.

32  Часть I. Bikeshedding — это плохо

Конечно же, работы по добавлению дополнительных возможностей и тогда
не прекратились, было продолжено дальнейшее развитие языка. В число
новых возможностей вошли: auto, constexpr, decltype, семантика перемещения, for с диапазонами, унифицированная инициализация, лямбдавыражения, правосторонние (rvalue) ссылки, статические утверждения,
вариативные шаблоны... Список продолжал пополняться, и график разработки растягивался. В конце концов, следующая версия стандарта вышла
в 2011 году, еще до того, как все его авторы успели забыть, что C++ был
когда-то растущим языком.
Учитывая, что C++03 был исправлением C++98, между первым стандартом и стандартом C++11 имел место 13-летний разрыв. Стало понятно, что
такой длительный период между публикациями стандартов никому не интересен, поэтому была разработана «модель транспорта»: новый стандарт
будет публиковаться каждые три года, и если какая-то особенность языка
не будет готова к этому моменту, отчет о ее готовности пойдет «следующим
рейсом» — пополнит версию, которая будет опубликована три года спустя.
С тех пор стандарты C++14, C++17 и C++20 выходили согласно графику.

ИНКАПСУЛЯЦИЯ ВАРИАЦИЙ
Вариации в окружении времени выполнения
В стандарте очень мало говорится о требованиях к окружению, в котором
выполняется программа на C++. Операционная система не регламентируется. Хранилища файлов не являются обязательными. Экран тоже
необязателен. Получается, что программа, написанная для типичного
окружения рабочего стола, может нуждаться во вводе с помощью мыши
и выводе в оконном режиме, что потребует специального кода для каждой
конкретной системы.
Написание полностью переносимого кода для такой программы невозможно. Стандарт ISO C++ имеет очень маленькую библиотеку по сравнению с такими языками, как C# и Java. Это всего лишь спецификация
для разработчиков, реализующих стандарт ISO C++. Все дело в том, что
стандартные библиотеки C# и Java предоставляются владельцами языка,
но C++ не имеет финансируемой организации по разработке библиотек.
Вы должны использовать уникальные особенности каждой целевой среды
для поддержки тех частей функционала, которые недоступны в стандартной
библиотеке языка. Обычно они предлагаются в виде заголовочного файла
и библиотеки. Как правило, таких файлов очень много в каждой системе.

P.2. Придерживайтесь стандарта ISO C++  33

Насколько возможно, прячьте их за вашими собственными интерфейсами.
Минимизируйте количество вариаций между версиями кодовой базы,
предназначенными для разных систем.
Например, вам может понадобиться узнать, нажимается ли конкретная
клавиша на клавиатуре. Один из возможных подходов — применение
препроцессора для определения используемой платформы и вызов соответствующего фрагмента кода, например:
#if defined WIN32
auto a_pressed = bool{GetKeyState('A') & 0x8000 != 0};
#elif defined LINUX
auto a_pressed = /* в действительности здесь следует большой фрагмент кода */
#endif

Очень неуклюжее решение: оно работает на неправильном уровне абстракции. Код, относящийся к Windows и Linux1, должен находиться в отдельных
файлах, соответствующих системе, и импортироваться через заголовочные
файлы, чтобы использующий их вызов выглядел так:
auto a_pressed = key_state('A');

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

Вариации на уровне языка C++ и компилятора
Разработчики компиляторов C++ должны максимально полно и точно поддерживать стандарт, если намерены объявить свой компилятор соответствующим стандарту. Однако это требование не связывает им руки и оставляет
открытой дверь для добавления дополнительных возможностей или расширений. Например, в GCC были добавлены дополнительные свойства типов
(type trait), такие как __has_trivial_constructor и __is_abstract, до того как
они появились в стандарте. Оба присутствовали в библиотеке свойств типов,
начиная с C++11, под другими именами: std::is_trivially_constructible
и std::is_abstract.
Обратите внимание, что имя __is_abstract начинается с двойного подчеркивания: имена, начинающиеся с двойного подчеркивания, зарезервированы
1

https://stackoverflow.com/questions/41600981/how-do-i-check-if-a-key-is-pressed-on-c

34  Часть I. Bikeshedding — это плохо

стандартом для разработчиков компилятора. Разработчикам не разрешается добавлять новые идентификаторы в пространство имен std, так как
впоследствии они могут быть добавлены в стандарт с совершенно другим
значением. На практике это означает, что программист, работая на C++,
может ненамеренно написать код, который выглядит как использующий
стандартные функции, но в действительности использует функции, характерные для специфического компилятора. Хороший способ защититься от
таких случайностей — написать и протестировать свою программу с несколькими компиляторами и операционными системами, чтобы обнаружить
не закрепленный в стандарте код.
Две упомянутые выше функции были реализованы по причине их полезности
для метапрограммирования. В действительности они оказались настолько
полезными, что позже были добавлены в стандарт. Многие части стандарта,
касающиеся как самого языка, так и стандартной библиотеки, появлялись
сначала как дополнительные возможности в популярных инструментах
и библиотеках. Иногда использование нестандартных функций неизбежно.

Расширения для С++
Некоторые разработчики библиотек добавляют свои расширения. Например, библиотека Qt1 использует так называемые сигналы и слоты для
организации взаимодействий между объектами. С этой целью в библиотеку добавлены три символа: Q_SIGNALS, Q_SLOTS и Q_EMIT. В исходном коде
эти ключевые слова выглядят как любые другие ключевые слова языка.
Qt предоставляет инструмент под названием moc, который анализирует
эти ключевые слова и преобразует их в конструкции, понятные компилятору C++, точно так же, как раньше Cfront преобразовывал код на C++
в конструкции, понятные компиляторам языка C.
Следует иметь в виду, что стандарт предлагает то, чего нет в этих расширениях: строго определенную семантику. Стандарт ISO C++ однозначен,
и это одна из причин, почему его так трудно читать. Можно без ограничений
использовать расширения языка, но не забывать о факторе переносимости.
В частности, Qt прилагает титанические усилия для достижения переносимости кода между различными платформами. Однако никто не гарантирует,
что эти расширения будут присутствовать в других реализациях или что
они будут иметь там такое же значение.
1

https://doc.qt.io/

P.2. Придерживайтесь стандарта ISO C++  35

Защита заголовочных файлов
Рассмотрим пример с директивой #pragma once. Эта простая директива
приказывает компилятору не подключать заголовочный файл во второй
раз и тем самым сокращает время, затрачиваемое на компиляцию единицы
трансляции. Все компиляторы, которые авторы использовали за последние
20 лет, реализуют эту директиву pragma, но задумаемся: что она означает
на самом деле? Может, «остановить синтаксический анализ, пока не будет
достигнут конец файла»? Или «не открывать этот файл во второй раз»?
Получается, притом что видимый эффект одинаков, его значение не имеет
точного определения для всех платформ без исключения.
В результате вы как разработчики не можете быть уверенными, что значение чего-либо сохранится на разных платформах. Даже если сейчас вы
в безопасности, то не можете гарантировать, что останетесь в безопасности
в будущем. Полагаться на такую нестандартную возможность — все равно
что полагаться на ошибку. Это опасно, потому что она может быть изменена или исправлена в любое время (впрочем, смотрите Закон Хайрама1).
В этом случае Руководство рекомендует вместо #pragma once использовать
средства защиты заголовочных файлов, как описано в «SF.8. Защищайте
от повторного подключения все заголовочные файлы». При наличии такой
защиты мы точно знаем, что произойдет.

Вариации в основных типах
Реализации операционных систем не единственный вид системных вариаций. Как вы, возможно, знаете, размеры арифметических типов, таких
как int и char, не стандартизированы. Вы можете считать, что тип int
имеет размер 32 бита, но были времена, когда int имел размер 16 бит.
Иногда в программах бывает нужен тип размером ровно 32 бита, поэтому,
опасаясь совершить ошибку (предположив, что int всегда будет иметь
размер 32 бита: размер этого типа уже изменился один раз, так почему бы
ему не измениться снова?), разработчики вынуждены были использовать
заголовки реализации, чтобы выяснить, какой тип имеет нужный размер,
и определить псевдоним для этого типа:
typedef __int i32; // это древний способ добиться желаемого:
// не используйте его
1

Доступно по адресу https://www.hyrumslaw.com/.

36  Часть I. Bikeshedding — это плохо

Здесь представлен идентификатор i32, который играл роль псевдонима
для типа с именем __int, определяемого платформой. Такой шаг придал
этому коду некоторую безопасность: если бы проект пришлось переносить на другую платформу, то можно было бы узнать название 32-битного
целочисленного типа со знаком, определяемого этой платформой, и просто
обновить определение typedef.
Когда был выпущен следующий стандарт, в данном случае C++11, в библиотеку были добавлены новые типы в заголовочном файле , которые
определяли целочисленные типы фиксированного размера. В этой связи
появилась возможность обновить определение и получить двойную выгоду:
using i32 = std::int32_t;

Во-первых, новый тип можно использовать для проверки своего определения
в будущем: тип, которому присвоен псевдоним, является частью стандарта,
и крайне маловероятно, что он изменится, потому что обратная совместимость очень важна для языка и сохраняется незыблемой. Это объявление
типа останется верным и в последующих версиях стандарта (действительно,
прошло девять лет и вышло три стандарта, а этот код все еще компилируется).
Во-вторых, разработчики смогли перейти на использование нового ключевого слова using, которое позволяет использовать стиль записи слева
направо. В нем идентификатор и его определение разделены знаком
равенства. Этот стиль также можно увидеть в примерах использования
ключевого слова auto:
auto index = i32{0};

Определяемый идентификатор указывается слева от знака равенства, а его
определение — справа.
Когда появились достаточно мощные инструменты рефакторинга, нашлись разработчики, которые сделали решительный шаг и заменили все
экземпляры i32 на std::int32_t, чтобы минимизировать неоднозначность
прочтения кода.

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

P.2. Придерживайтесь стандарта ISO C++  37

использование определенных функций. Это может быть обусловлено нормативными причинами или несовершенством реализации платформы, для
которой вы разрабатываете приложения.
Например, в некоторых отраслях запрещается динамически распределять
память в функциях, производительность которых критически важна.
Распределение памяти — это недетерминированное действие, которое,
кроме всего прочего, может вызвать исключение нехватки памяти; то есть
в динамике невозможно точно спрогнозировать, сколько времени займет
выполнение динамического запроса памяти. Или, например, в некоторых
областях и по тем же причинам запрещена генерация исключений, а это
опять же сразу налагает запрет на динамическое распределение памяти,
потому что std::operator new генерирует исключение std::bad_alloc при
ошибке. Из-за подобных ситуаций ощущается необходимость расширить
и адаптировать Core Guidelines к конкретной среде.
И наоборот, в некоторых областях запрещено использование библиотек,
не прошедших сертификацию отраслевым регулирующим органом. Например, использование библиотеки Boost1 может быть проблематичным
в некоторых окружениях. Очевидно, что становится все более необходимо
шире и чаще использовать стандарт ISO C++.

ИЗУЧЕНИЕ СТАРЫХ СПОСОБОВ
Обратная совместимость в C++
Важно помнить, откуда взялся этот язык, а также что послужило мотивом
его развития. В текущем проекте одного из авторов есть код, который был
им написан в 2005 году. Код выглядит немного странно для современных
программистов, потому что использует давно заброшенные парадигмы,
никаких объявлений auto, никаких лямбда-выражений: он является артефактом истории исходного кода.
Тем не менее он по-прежнему прекрасно компилируется и работает. В своей
карьере вы столкнетесь с кодом разных эпох. Важно использовать последний
стандарт и собирать код с помощью самого свежего компилятора, который
только сможете найти, но также важно знать, откуда взялся язык и что было
раньше, хотя бы для того, чтобы уметь предвидеть будущее.
1

https://www.boost.org

38  Часть I. Bikeshedding — это плохо

Иногда нет возможности использовать последнюю версию стандарта. При
разработке встраиваемых систем регламент, сертификация систем или
устаревшая инфраструктура могут вынудить вас использовать C++11
или даже C++98. C++ полагается на обратную совместимость. Он обратно совместим с C и с предыдущими стандартами. Стабильность в течение
десятилетий является преимуществом. Это одна из сильных сторон C++:
миллиарды строк кода, написанных во всем мире, до сих пор компилируются с помощью современных компиляторов, пусть иногда и с небольшими корректировками. В какой-то момент вас могут попросить обеспечить
поддержку такому коду.

Прямая совместимость и Y2K
С другой стороны, пишите код на века. В конце прошлого века в большинстве старых компьютерных программ, существующих в мире, была обнаружена проблема: для обозначения года использовались только две цифры1.
В те времена память была в дефиците и эффективнее было хранить 74, а не
1974. Разработчики думали: «Эта программа не будет работать через 25 лет,
ее наверняка заменят». Ах, какой пессимизм, а может быть, и оптимизм,
это зависит от точки зрения.
Как только дата пересекла 2000 год, номер года стал представляться как 00, что
отрицательно сказалось на вычислениях
временных интервалов, расчетах процентных платежей и вообще на всем, что распределено во времени.

Стабильность и преемственность кода в течение десятилетий являются преимуществом.

Эта ошибка известна как Y2K или ошибка тысячелетия. Наступил звездный час старых подрядчиков, которые путешествовали по компьютерам
мира, дорабатывая 25-летние системы за немалые деньги. Хотя катастрофа
в значительной степени была предотвращена, потому что проблема была
выявлена загодя и имелось достаточно инженеров, чтобы все исправить.
Однако если бы инженеры-разработчики ПО в свое время планировали
будущее, предполагали, что их код будет работать «вечно», и в то время,
когда они писали код, четырехзначные целые числа занимали бы столько же
места, сколько занимали двузначные числа, то этих неприятностей и тревог
1

https://www.britannica.com/technology/Y2K-bug

P.2. Придерживайтесь стандарта ISO C++  39

удалось бы избежать. Было бы ясно, что двух цифр недостаточно для
представления всех дат, которые могут потребоваться, и понадобилось бы
по крайней мере три цифры, а еще лучше — четыре, просто чтобы легче
пересечь рубеж тысячелетий.
Кстати, это не единственная проблема с датами. В Linux существовала аналогичная проблема с измерением времени в секундах от начала эпохи Unix
1 января 1970 года. Это число хранилось как 32-битное целое число со знаком, то есть оно стало бы отрицательным 19 января 2038 года. Мы говорим
«стало бы», потому что в версии Linux 5.6 эта ситуация была решена.
Итак, важной парой профессиональных навыков является написание кода
с прицелом на будущее и умение читать код из прошлого.

СЛЕДИТЕ ЗА ПОСЛЕДНИМИ
ИЗМЕНЕНИЯМИ В СТАНДАРТЕ
C++ постоянно развивается. С появлением каждой новой версии стандарта
в языке появляются новые возможности и дополнения к библиотекам. Простое механическое использование самых последних новинок не несет особой ценности, их следует использовать там, где они приносят определенную
и конкретную пользу. Однако сообществу C++ очень повезло: у него есть
много отличных учителей и толкователей, готовых открыть и объяснить
все эти новинки. Инженерам нужно держать руку на пульсе тенденций
и тонкостей этого развития. Поиск необходимых ресурсов можно вести
в четырех направлениях.

IsoCpp
Прежде всего, это isocpp.org1. Это дом C++ в интернете, которым управляет Standard C++ Foundation — некоммерческая организация Wa­
shing­ton 501(c) (6), целью которой является поддержка сообщества разработчиков программного обеспечения на C++ и содействие пониманию
и использованию современного стандарта C++ на всех платформах и компиляторах. На этом сайте вы найдете обзор C++, написанный Бьерном,
огромный сборник вопросов и ответов по C++, подробную информацию
1

https://isocpp.org/about

40  Часть I. Bikeshedding — это плохо

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

Конференции
Второй источник — ежегодные конференции, проводимые по всему миру.
На этих конференциях принято записывать все выступления и публиковать их на YouTube для бесплатного просмотра. Это действительно удивительный, очень активный ресурс, и довольно сложно хотя бы просто идти
в ногу с публикуемыми материалами из года в год, не отставать и оставаться
в курсе всех публикаций.
Конференция CppCon находится в ведении Standard C++ Foundation. Она
проходит ранней осенью в США, в городе Аврора, штат Колорадо, и генерирует около 200 часов контента. Ассоциация пользователей C и C++
(Association of C and C++ Users, ACCU) проводит свою ежегодную конференцию в Бристоле, Великобритания, каждую весну, а иногда и осенью.
Она фокусируется в основном на C++, но иногда на ней обсуждаются более
широкие темы, связанные с программированием. Ассоциация генерирует
почти 100 часов контента. Ежегодно в ноябре в Берлине, Германия, проводится встреча пользователей C++, которая генерирует около 50 часов
контента. Конечно, охватить такой объем информации у вас едва ли получится, потому что просмотр хотя бы одного доклада в день в течение, например, года займет у вас массу времени, фактически большую часть этого
года. А ведь есть еще множество других небольших конференций, которые
проходят в таких странах, как Австралия, Беларусь, Израиль, Италия,
Польша, Россия, Испания...

Другие источники
Помимо блогов и конференций, существует также множество книг. Некоторые из них будут упоминаться в ссылках по всему тексту этой книги,
как и цитаты из выступлений на конференциях.
Наконец, на разных чат-серверах, таких как Discord и Slack1, доступны
ежедневные обсуждения. Сервер Discord модерируется группой #include2
1

https://cpplang.slack.com

2

https://www.includecpp.org

P.2. Придерживайтесь стандарта ISO C++  41

и предназначен для программистов на C++. К настоящему времени там
образовалось очень гостеприимное сообщество.
Имея так много доступных ресурсов, вы сможете идти в ногу с развитием
стандарта C++. Разработка кода с опорой на стандарт ISO C++ доступна
каждому. Это важно не только для тех, кто в будущем будет сопровождать
код, кем бы они ни были, включая вас самих, но и для будущих клиентов-заказчиков вашего кода. Язык C++ широко используется во многих
областях: в торговле, промышленности и общественных организациях.
Применение последовательного и надежного подхода к разработке кода
имеет глобальное значение. Итак, будучи ответственными профессионалами, сделайте шаг вперед, поступайте правильно и придерживайтесь
стандарта ISO C++.

ГЛАВА 1.2

F.51. Если есть выбор,
используйте аргументы
по умолчанию вместо
перегрузки
https://t.me/it_boooks

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

F.51. Если есть выбор, используйте аргументы по умолчанию  43

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

ДОРАБОТКА ВАШИХ АБСТРАКЦИЙ:
ДОПОЛНИТЕЛЬНЫЕ АРГУМЕНТЫ
ИЛИ ПЕРЕГРУЗКА?
Рассмотрим для примера следующую функцию:
office make_office(float floor_space, int staff);

Эта функция возвращает экземпляр office — объект, представляющий собой
офисное здание площадью floor_space квадратных метров и с кабинетами
для staff сотрудников. Это одноэтажное здание c кухонными и туалетными помещениями и соответствующим количеством кофемашин, столов
для настольного тенниса и массажных кабинетов. Однажды было решено
расширить предметную область и добавить возможность моделирования
двухэтажных офисных зданий. Это несколько усложнило ситуацию, так
как двухэтажная модель предполагает определение путей эвакуации, более
сложную схему кондиционирования воздуха, наличие лестниц в нужных
местах и, конечно же, горки между этажами или, может быть, пожарного
столба. К тому же нужно сообщить функции-конструктору, что она должна
создать модель двухэтажного офисного здания. Это можно сделать с помощью третьего параметра:
office make_office(float floor_space, int staff, bool two_floors);

Проблема в том, что в этом случае придется просмотреть весь код и добавить false во все вызовы make_office. С другой стороны, можно определить

44  Часть I. Bikeshedding — это плохо

аргумент по умолчанию false для последнего параметра, и тогда ничего
менять не придется. Вот как это выглядит:
office make_office(float floor_space, int staff, bool two_floors = false);

Одна короткая перекомпиляция — и все в порядке. К сожалению, демоны
расширения предметной области еще не закончили: оказывается, одноэтажным офисным зданиям иногда требуется давать названия. Вы, как
отзывчивый на вызовы и просьбы заказчика инженер, решаете расширить
список параметров функции:
office make_office(float floor_space, int staff,bool two_floors = false,
std::string const& building_name = {});

Вы переопределяете функцию и замечаете одну неприятность. Функция
принимает четыре аргумента, причем последний необходим, только если
третий аргумент false и из-за этого функция выглядит запутанной и сложной. Вы решаете добавить перегруженную версию функции:
office make_office(float floor_space, int staff, bool two_floors = false);
office make_office(float floor_space, int staff,
std::string const& building_name);

Теперь у вас есть то, что известно как набор перегруженных функций.
И каждый раз, встретив ссылку на имя функции, компилятор должен выбрать, какую реализацию выбрать, а для этого проверить типы переданных
аргументов. Клиент вынужден вызывать правильную функцию, когда
нужно идентифицировать здание. Наличие идентификации подразумевает
одноэтажный офис.
Например, представьте, что в некотором клиентском коде предпринята
попытка создать офис площадью 24 000 квадратных метров для 200 сотрудников. Офис расположен в одноэтажном здании с названием Eagle
Heights. Вот как должен выглядеть соответствующий вызов:
auto eh_office = make_office(24000.f, 200, "Eagle Heights");

Конечно, вы должны гарантировать соблюдение определенной семантики
в каждой функции и обеспечить, чтобы все функции действовали одинаково. Это тяжкое бремя сопровождения. Возможно, уместнее реализовать
единственную функцию и потребовать от вызывающей стороны явно обозначать свой выбор.
Мы уже слышим, как вы говорите: «Постойте! А если написать приватную
реализацию функции? В таком случае можно гарантировать единообразие

F.51. Если есть выбор, используйте аргументы по умолчанию  45

моделирования, просто вызывая приватную реализацию из перегруженных
версий, и все будет в порядке».
Вы были бы правы, если бы не одно но. Клиенты могут с подозрением отнестись к двум функциям. Их может насторожить возможность несогласованности реализаций. Излишняя осторожность с их стороны может вселить
в них страх и опасения. Одна функция с двумя аргументами по умолчанию
для переключения между алгоритмами выглядит более надежно.
И снова мы слышим ваше возражение: «Это смешно! Я пишу качественный
код, и мои клиенты доверяют мне. У меня весь код охвачен модульными
тестами, и все в порядке. Вот уж спасибо так спасибо!»
К сожалению, даже если вы действительно пишете код высочайшего качества, это не обязательно относится к вашим клиентам. Взгляните еще раз на
инициализацию eh_office и проверьте себя, сможете ли вы заметить ошибку.
А другой человек сможет? Подумайте, а пока мы поговорим о разрешении
перегрузки кода (как выбираются перегруженные версии).

ТОНКОСТИ РАЗРЕШЕНИЯ ПЕРЕГРУЗКИ
Разрешение перегрузки — сложная задача для освоения. Почти 2 % стандарта C++20 посвящены определению работы механизма разрешения
перегрузок. Вот краткий обзор.
Когда компилятор встречает вызов функции, он должен определить, на какую из функций этот вызов ссылается. Перед этим компилятор составляет
список всех идентификаторов. Возможно, что в программе имеется несколько
функций с одинаковыми именами, но с разными параметрами — набор перегруженных версий. Как компилятор определяет, какую из них вызывать?
Сначала он отбирает функции с тем же количеством параметров, с меньшим
количеством и с параметром-многоточием или с большим количеством
параметров, среди которых избыточные параметры имеют аргументы по
умолчанию. Если какой-либо из кандидатов имеет предложение requires
(requires clause, нововведение, появившееся в C++20), то оно должно быть
удовлетворено. Ни один правосторонний (rvalue) аргумент не должен соответствовать неконстантному левостороннему (lvalue) параметру, и любой
левосторонний (lvalue) аргумент не должен соответствовать ссылочному
правостороннему (rvalue) параметру. Каждый аргумент должен иметь возможность быть преобразованным в соответствующий параметр посредством
неявной последовательности преобразований.

46  Часть I. Bikeshedding — это плохо

В нашем примере компилятору передаются две версии make_office, отличающиеся третьим параметром. Одна принимает логическое значение,
которое по умолчанию равно false, а вторая — std::string const&. По количеству параметров обе версии соответствуют операции инициализации eh_office.
Ни в одной из этих функций нет предложения requires, поэтому можно
пропустить этот шаг. Точно так же нет ничего экзотического в привязках
ссылок.
Наконец, каждый аргумент должен быть преобразован в соответствующий
параметр. Первые два аргумента не требуют преобразования. Третий аргумент — это char const*, который, очевидно, преобразуется в std::string
через неявный конструктор, являющийся частью интерфейса std::string.
Но, к сожалению, это еще не все.
Когда имеется несколько перегруженных версий функции, они ранжируются по параметрам, чтобы упростить поиск наиболее подходящей. Версия
F1 считается предпочтительнее версии F2, если неявные преобразования
для всех аргументов F1 не хуже, чем у F2. Кроме того, в F1 должен быть
хотя бы один параметр, неявное преобразование которого лучше соответствующего неявного преобразования в F2.
Слово «лучше» настораживает. Как ранжируются последовательности
неявных преобразований?
Существует три типа последовательностей неявных преобразований:
стандартная, определяемая пользователем и последовательность преобразований с многоточием.
Стандартная последовательность имеет три ранга: точное соответствие,
продвижение и преобразование. Точное соответствие означает отсутствие
необходимости преобразования и является предпочтительным рангом.
Это также может означать преобразование левостороннего (lvalue) аргумента в правосторонний (rvalue).
Продвижение означает расширение представления типа. Например, объект
типа short может быть продвинут до объекта типа int (такое преобразование называется целочисленным продвижением), а объект типа float может
быть продвинут до объекта типа double, что известно как продвижение
с плавающей точкой.
Преобразования отличаются от продвижения возможностью изменения
значения, что может отрицательно сказаться на точности. Например,

F.51. Если есть выбор, используйте аргументы по умолчанию  47

значение с плавающей точкой можно преобразовать в целое число, округлив
до ближайшего целого. Кроме того, целочисленные значения и значения
с плавающей точкой, перечисления без указания области видимости, указатели и типы указателей на члены могут быть преобразованы в логическое
значение. Эти три ранга являются концепциями, унаследованными от языка C, и от них невозможно отказаться из-за необходимости поддерживать
совместимость с C.
Это частично охватывает стандартные последовательности преобразований.
Преобразования, определяемые пользователем, выполняются двумя способами: либо с помощью неявного конструктора, либо с помощью неявного
оператора преобразования. Именно этот тип преобразований мы ожидаем
в нашем примере: наш аргумент char const* преобразуется в std::string
через неявный конструктор, который принимает char const*. Это так же
очевидно, как нос на вашем лице. Но с какой целью мы втянули вас в это
обсуждение особенностей разрешения перегрузок?

ВЕРНЕМСЯ К ПРИМЕРУ
В примере выше клиент ожидает, что к аргументу char const* будет применено определяемое пользователем преобразование в std::string, и этот
временный правосторонний (rvalue) аргумент будет передан в виде ссылки
на константу в третьем параметре второй функции.
Однако, как отмечалось выше, стандартные преобразования имеют приоритет перед определяемыми пользователем. В предыдущем разделе, описывая
преобразования, мы выяснили, что имеется стандартное преобразование из
указателя в логическое значение. Если вы когда-либо рассматривали старый код, передающий простые указатели между функциями, то наверняка
видели такие конструкции:
if (ptr) {
ptr->do_thing();
}

Условное выражение в операторе if является указателем, а не логическим
значением, но указатель может быть преобразован в false, если он равен
нулю. Это более краткий идиоматический способ записи:
if (ptr != 0) {
ptr->do_thing();
}

48  Часть I. Bikeshedding — это плохо

В современном C++ мы все реже видим простые указатели, тем не менее
следует помнить, что это совершенно нормальное и разумное преобразование. Именно это стандартное преобразование компилятор сочтет более
предпочтительным и выберет его вместо, казалось бы, очевидного определяемого пользователем преобразования из char const* в std::string const&.
К удивлению клиента, компилятор вызовет перегруженную версию, которая
принимает логическое значение в третьем аргументе.
Чья это ошибка? Ваша или клиента? Если бы клиент записал вызов так:
auto eh_office = make_office(24000.f, 200, "Eagle Heights"s);

ошибка не возникла бы. Литеральный суффикс сигнализирует о том, что
этот объект на самом деле является объектом std::string, а не char const*.
Так что в этом случае явно виноват клиент. Он должен знать о правилах
преобразования.
Однако это не оправдывает выбранное вами решение. Вы должны реализовать
интерфейс так, чтобы его проще было использовать правильно, чем неправильно. Пропустить литеральный суффикс и тем самым допустить ошибку очень
легко. Также подумайте, что случится, если перегруженная версия функции,
принимающая логическое значение, будет добавлена после определения конструктора, принимающего std::string const&. До этого момента клиентский
код будет действовать в соответствии с ожиданиями и с литеральным суффиксом, и без него. Но после добавления перегруженной версии компилятор
начнет выбирать лучшее преобразование и клиентский код может неожиданно
начать действовать не так, как ожидалось.
Кто-то может посчитать этот пример неубедительным и попробовать заменить bool на более подходящий тип, например определить перечисление
для использования вместо логического значения:
enum class floors {one, two};
office make_office(float floor_space, int staff,
floors floor_count = floors::one);
office make_office(float floor_space, int staff,
std::string const& building_name);

К сожалению, и этот подход не является выходом из спорной ситуации.
Был введен новый тип, только чтобы способствовать правильному использованию набора перегруженных функций. Спросите себя, действительно ли
такое решение выглядит яснее этого:
office make_office(float floor_space,int staff,bool two_floors = false,
std::string const& building_name = {});

F.51. Если есть выбор, используйте аргументы поумолчанию  49

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

Вы должны реализовать
интерфейс так, чтобы
его проще было использовать правильно, чем
неправильно.

ОДНОЗНАЧНАЯ ПРИРОДА АРГУМЕНТОВ
ПО УМОЛЧАНИЮ
Преимущество аргумента по умолчанию в том, что любое преобразование
сразу становится очевидным. Вы можете видеть, что char const* преобразуется в std::string const&. Нет никакой двусмысленности в выборе
преобразования, потому что оно может произойти только в одном месте.
Кроме того, как упоминалось выше, наличие единственной функции
дает больше уверенности, чем перегруженный набор. Если вы подобрали
хорошее имя для своей функции и хорошо спроектировали ее, то вашему
клиенту не придется задумываться о том, какую версию вызвать. Но, как
показывает пример, это проще сказать, чем сделать. Аргумент по умолчанию сообщает клиенту, что функция обладает гибкостью, предоставляет
альтернативный интерфейс к своей реализации и гарантирует единство
семантики.
Единственная функция также позволяет избежать дублирования кода.
Создавая перегруженную версию, вы исходите из самых лучших побу­
ждений. Конечно, это так. Но перегруженные версии действуют немного
по-разному, и вы решаете инкапсулировать оставшееся сходство кодов
в одной функции, которую вызывают обе перегруженные версии. Однако
со временем перегруженные версии начинают во многом перекрываться,
потому что становится все труднее отделить фактические различия их
применения. В конечном итоге вы столкнетесь с проблемой усложнения
сопровождения базы кода по мере разрастания функционала.
Есть одно ограничение. Аргументы по умолчанию должны определяться
в обратном порядке по списку параметров. Например, такое объявление
будет допустимым:
office make_office(float floor_space, int staff,bool two_floors,
std::string const& building_name = {});

50  Часть I. Bikeshedding — это плохо

А это — недопустимое:
office make_office(float floor_space, int staff,bool two_floors = false,
std::string const& building_name);

Если последнюю функцию вызвать только с тремя аргументами, то становится невозможно однозначно сказать что-либо о последнем аргументе,
с каким параметром он должен быть связан: с two_floors или building_name?
Надеемся, мы смогли убедить вас, что перегрузку функций, несмотря на
ее неоспоримые достоинства, не следует воспринимать поверхностно.
Мы лишь слегка коснулись проблем разрешения перегрузки. Есть еще множество тонкостей, которые нужно изучить, если вы хотите по-настоящему
понять, какая из перегруженных версий будет выбрана. Обратите внимание,
что мы не рассматривали последовательности преобразований с многоточием и не обсуждали, что произойдет, если добавить в описанную схему
шаблонную функцию. Однако если вы абсолютно уверены в необходимости использовать перегруженные версии, то мы вас убедительно просим:
пожалуйста, не смешивайте аргументы по умолчанию с перегруженными
функциями. Такая смесь трудно поддается анализу и расставляет ловушки
для неосторожных. Это не тот стиль определения интерфейса, который
проще использовать правильно, чем неправильно.

АЛЬТЕРНАТИВЫ ПЕРЕГРУЗКЕ
Перегрузка функций сигнализирует клиенту, что доступ к части функциональности, абстракции, можно обеспечить несколькими способами. Функции
с одним и тем же идентификатором можно вызвать с разными наборами аргументов. На самом деле, вопреки ожиданиям, фундаментальный строительный
блок API был описан как набор перегруженных функций, а не как функция.
Однако в несколько надуманном примере для этой главы можно обнаружить, что набор перегруженных функций:
office make_office(float floor_space, int staff, floors floor_count);
office make_office(float floor_space, int staff,
std::string const& building_name);

не так очевиден, как набор отдельных функций:
office make_office_by_floor_count(float floor_space, int staff,
floors floor_count);
office make_office_by_building_name(float floor_space, int staff,
std::string const& building_name);

F.51. Если есть выбор, используйте аргументы по умолчанию  51

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

ИНОГДА БЕЗ ПЕРЕГРУЗКИ НЕ ОБОЙТИСЬ
Описываемая рекомендация начинается словами: «Если есть выбор». Иногда может не быть возможности определить функцию с другим именем.
Например, может быть только один идентификатор конструктора. Поэто­му
если потребуется дать возможность создавать экземпляры класса несколькими способами, то вам действительно придется реализовать перегруженные версии конструктора.
Точно так же и операторы могут иметь единственное значение, очень
ценное для ваших клиентов. Если по какой-то причине вы написали
свой класс строк, то ваши клиенты предпочтут объединять строки таким
способом:
new_string = string1 + string2;

а не:
new_string = concatenate(string1, string2);

То же верно в отношении операторов сравнения. Однако маловероятно, что при перегрузке операторов вам понадобится аргумент по умолчанию.
Стандарт предоставляет точку настройки std::swap и ожидает, что вы напишете перегруженную версию этой функции, оптимальную для вашего
класса. В Core Guidelines имеется рекомендация «C.83. Для типов-значений желательно определить функцию swap cо спецификатором noexcept»,
а она прямо предлагает создать перегруженную функцию. Однако и в этом
случае крайне маловероятно, что при перегрузке функции понадобится
аргумент по умолчанию.

52  Часть I. Bikeshedding — это плохо

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

ПОДВЕДЕМ ИТОГ
Мы рассмотрели, как влияет рост базы кода на архитектуру API, исследовали простой пример перегрузки и увидели, что именно при этом может пойти
не так. В главе, посвященной тонкостям перегрузки, мы кратко пробежались по правилам выбора перегруженной версии компилятором. С учетом
работы по этим правилам мы показали, что может состояться вызов совсем
не той из перегруженных версий, которая ожидалась. В частности, ошибка
в нашем примере была вызвана предоставлением логического параметра
с аргументом по умолчанию в перегруженной версии, что открывает широкие возможности для преобразования других нелогических аргументов
в этот параметр. Нашей целью было показать, что аргументы по умолчанию
предпочтительнее перегрузки функций и смешивание перегрузки с аргументами по умолчанию — весьма рискованное предприятие.
Пример, конечно же, был так себе, но факт остается фактом: для неосторожного инженера перегрузка таит серьезную опасность. Избежать опасности или минимизировать ее можно, разумно использовав аргументы по
умолчанию и отказавшись от перегрузки. Желающие могут изучить все
последствия перегрузки на своем любимом онлайн-ресурсе. Советуем
сделать это, если когда-нибудь вы решите проигнорировать нашу вполне
конкретную рекомендацию.

ГЛАВА 1.3

C.45. Не определяйте
конструктор
по умолчанию, который
просто инициализирует
переменные-члены; для этой
цели лучше использовать
внутриклассовые
инициализаторы членов

ЗАЧЕМ НУЖНЫ КОНСТРУКТОРЫ
ПО УМОЛЧАНИЮ
Начнем эту главу с краткого обзора. Рассмотрим рекомендацию из Core
Guideline «NR.5. Не используйте двухфазную инициализацию». Она относится к привычке вызывать функцию инициализации после создания
объекта. Эта практика зародилась еще в прошлом веке, когда язык C был
самым актуальным и нужно было сначала разместить объект в стеке или
в динамической памяти, а затем инициализировать его. Наиболее опытные
программисты определяли функцию, которая принимала указатель на
структуру в памяти, и давали ей имя my_struct_init или похожее.
Процесс состоял из двух фаз: размещения в памяти и последующей инициализации. Здесь что угодно может пойти не так: вы можете вставлять
все больше и больше кода, выполняющегося между размещением в памяти

54  Часть I. Bikeshedding — это плохо

и инициализацией, и вдруг обнаружить, что использовали объект до инициализации.
Затем появились C++ и конструкторы, и эта проблема исчезла навсегда.
Для объектов, размещаемых статически, компоновщик создаст список
их конструкторов, выполняемый до main(), и функцию для обхода этого
списка. Компоновщик имеет полное представление о том, сколько места
будут занимать эти объекты, поэтому функция сможет выделить память
для них, инициализировать их все, а затем вызвать main().
Для автоматических объектов компилятор выделяет некоторое пространство в стеке и инициализирует их в этой памяти. Для объектов, размещаемых в динамической памяти, оператор new вызовет оператор new для
выделения памяти и конструктор — для инициализации объекта в этой
памяти. Объекты, локальные для потоков выполнения, поддержка которых
появилась в C++11, ведут себя почти так же, как статически размещаемые
объекты, за исключением того, что их экземпляры создаются для каждого
потока выполнения, а не для каждого экземпляра программы.
Мы надеемся, что вы уяснили четкую и последовательную схему, избавля­
ющую от целого класса ошибок, связанных с использованием объекта до его
готовности к этому. Объединение размещения в памяти и инициализации
в одну операцию избавило от проблемы двухфазной инициализации.
Однако проблема не исчезла полностью и насовсем. У инженеров сохранилась привычка сначала создавать экземпляры объектов, а затем модифицировать их. Классы разрабатываются со значениями по умолчанию,
и клиенты затем должны корректировать эти значения в соответствии
с контекстом.
Эта привычка просто сместила проблему в другое место. Конструктор
по умолчанию подходит не для всех классов. К сожалению, долгое время
контейнеры, предоставляемые некоторыми реализациями C++, не работали с данными, не имеющими конструкторов по умолчанию. Конструктор
по умолчанию в таких случаях предоставляется не как часть предметной
области, а как часть области решения. Конечно, это означает возможность
его использования и в предметной области, что вносит еще б льшую неразбериху в вопрос о правильном использовании класса.
Некоторые классы по своей природе должны иметь конструктор по умолчанию. Например, представьте объявление пустой строки. Для этой операции
нет значимого API, если только вы не решите добавить перегруженную

C.45. Не определяйте конструктор по умолчанию  55

версию конструктора специально для пустых строк со специальным параметром. API std::string распознает этот случай и предоставляет конструктор по умолчанию, создающий строку с нулевой длиной. Конструктор по
умолчанию является очевидным решением. Действительно, все стандартные
контейнеры предоставляют конструкторы по умолчанию.
Но не думайте, что ваш класс должен в обязательном порядке создаваться
с помощью конструктора по умолчанию. Убедитесь, что знаете обо всех
опасностях, прежде чем разрешить пользователям создавать экземпляры
вашего класса без какой-либо спецификации.

КАК ИНИЦИАЛИЗИРУЮТСЯ
ПЕРЕМЕННЫЕ-ЧЛЕНЫ
Вернемся к основной теме этой главы и рассмотрим процесс инициализации.
При создании объекта сначала резервируется память в соответствии
с классом хранения, а затем вызывается конструктор. Однако для объектов встраиваемых типов правила немного отличаются. Если конструктор
не определен, то члены класса инициализируются значениями по умолчанию. Если имеются члены встраиваемых типов, то они по умолчанию
не инициализируются.
Это плохо: если не гарантировать инициализацию каждого члена класса,
есть риск получить недетерминированное поведение программы. В таких случаях остается только пожелать вам удачи в отладке. Много лет
назад один из авторов работал над игрой и использовал динамическое
хранение данных, реализованное на C++. Игра поставлялась в виде двух
запускаемых библиотек: одна для разработки и одна для распространения.
Версия библиотеки для разработки была собрана с неопределенным макросом NDEBUG, вследствие чего срабатывали дополнительные проверки,
и с помощью стандартной библиотеки можно было получить всевозможную отладочную информацию. При вызове оператора new память
инициализировалась значением 0xcd. Когда вызывался оператор delete,
он заполнял освобождаемый блок памяти значением 0xdd. Эта особенность позволяла выявлять попытки обращения к висячим указателям.
Из соображений производительности библиотека, предназначенная для
распространения, не делала этого, оставляя память как есть после выделения и освобождения.

56  Часть I. Bikeshedding — это плохо

Игра была многопользовательской. Компьютер каждого игрока отправлял
свои ходы через интернет в мгновение ока, и все компьютеры должны были
интерпретировать их одинаково. Для этого все компьютеры должны были
находиться в идентичном состоянии с точки зрения модели игры; иначе
модели игры на отдельных компьютерах оказывались в особой ситуации,
и это приводило бы к рассогласованию результатов и невозможности продолжать игру. Такие несоответствия редко проявлялись в версиях игры, использующих библиотеку для разработки, потому что все они получали блоки выделенной памяти, заполненные значениями 0xcd. Сбои наблюдались
только в версиях с библиотекой, предназначенной для распространения,
и их было невероятно сложно отладить, потому что любые рассогласования
в работе модели не замечались игроками, пока эти расхождения не начинали проявлять себя явно, а это происходило через достаточно большой
промежуток времени после их возникновения.
До этой ситуации было невероятно трудно убедить команду разработчиков игры в важности инициализации каждой переменной-члена в каждом
конструкторе. Когда наконец это дошло до всех ее членов, проблема
исчезла. Детерминизм — ваш союзник, когда дело доходит до отладки,
поэтому обеспечьте детерминизм, реализовав предсказуемое конструи­
рование всех объектов, и инициализируйте все переменные-члены без
исключения.
Есть три места, где можно инициализировать переменные-члены. Первое
место — это тело функции-конструктора. Рассмотрим следующий класс:
class piano
{
public:
piano();
private:
int number_of_keys;
bool mechanical;
std::string manufacturer;
};

Вот как можно определить конструктор этого класса:
piano::piano()
{
number_of_keys = 88;
mechanical = true;
manufacturer = "Yamaha";
}

C.45. Не определяйте конструктор по умолчанию  57

Это вполне адекватное определение. Конструктор инициализирует все
члены, причем в порядке их объявления. Здесь представлена инициализация в теле функции. Однако это не самый оптимальный подход. Перед
выполнением тела функции-конструктора члены класса были инициализированы значениями по умолчанию. Сначала вызывался конструктор по
умолчанию std::string, а затем — оператор присваивания с char const*.
Фактически наш конструктор переопределяет значения в членах, а не
инициализирует их.
Современные компиляторы достаточно интеллектуальны, чтобы заметить
шаблон присваивания значений в конструкторе и оптимизировать его.
std::string — это шаблонный класс, и высока вероятность, что все его
выполнение доступно для компилятора. Он увидит избыточность инициа­
лизации и уберет ее. Однако такое поведение поддерживается не для всех
классов. Поэтому желательно производить инициализацию с использованием списка инициализаторов, а не в теле функции.
Вот как можно изменить конструктор для этого:
piano::piano()
: number_of_keys(88)
, mechanical(true)
, manufacturer("Yamaha")
{}

Такой подход требует от инженера помнить о необходимости поддержки
конструктора по умолчанию при добавлении переменных-членов, и код
очень похож на шаблонный, который просто увеличивает размер файла.
Есть и третье место, где можно определить значения по умолчанию. Оно находится еще ближе к месту действия: в определении самого класса. Инициализаторы определяют значения по умолчанию для переменных-членов
объекта, если в конструкторе не указано другое. Вернемся к определению
нашего класса, чтобы посмотреть, как определить эти инициализаторы:
class piano
{
public:
// piano(); // больше не нужен
private:
int number_of_keys = 88;
bool mechanical = true;
std::string manufacturer = "Yamaha";
};

58  Часть I. Bikeshedding — это плохо

Так намного лучше. В этом примере мы избавились от лишней функциичлена и определили характеристики фортепиано по умолчанию: 88-клавишное механическое пианино фирмы Yamaha. Такое решение имеет свою
цену, которую нельзя игнорировать, а именно: эти значения по умолчанию
экспортируются в составе объявления класса, от которого, возможно,
будет зависеть множество других файлов с исходным кодом. Изменение
любого из этих значений может потребовать повторной компиляции неизвестного количества файлов. Однако есть веские причины считать эту
цену оправданной.

ЧТО МОЖЕТ СЛУЧИТЬСЯ, ЕСЛИ ПОДДЕРЖИВАТЬ
КЛАСС БУДУТ ДВА ЧЕЛОВЕКА
Обычно класс поддерживается одним человеком. Этот инженер определяет
абстракцию, реализует ее в классе, проектирует API и имеет полное представление о происходящем.
Конечно, всякое случается. Человека, поддерживавшего класс, могут на
время перевести в другой проект, или, что еще хуже, он может уйти совершенно внезапно, не выполнив надлежащую передачу. Многое может
осложниться без строгой дисциплины передачи информации через документацию, встречи и другие мероприятия, которые типичному инженеру
кажутся досадной тратой времени.

Сборная солянка из конструкторов
Когда над классом работают несколько человек, начинают возникать несоответствия. Большая часть Core Guidelines посвящена уменьшению вероятности возникновения несоответствий. Непротиворечивый код проще
читать, он содержит меньше сюрпризов. Подумайте, что могло бы случиться
с классом piano, если бы на него набросились сразу три сопровождающих
разработчика:
class piano
{
public:
piano()
: number_of_keys(88)
, mechanical(true)
, manufacturer("Yamaha")

C.45. Не определяйте конструктор по умолчанию  59

}

{}
piano(int number_of_keys_, bool mechanical_,
std::string manufacturer_ = "Yamaha")
: number_of_keys(number_of_keys_)
, mechanical(mechanical_)
, manufacturer(std::move(manufacturer_))
{}
piano(int number_of_keys_) {
number_of_keys = number_of_keys_;
mechanical = false;
manufacturer = "";

private:
int number_of_keys;
bool mechanical;
std::string manufacturer;
};

Это лишь пример, но подчас приходится видеть подобные вещи в реальности. Обычно конструкторы отделяются друг от друга множеством строк.
Все конструкторы могут быть определены в определении класса. Поэтому не сразу видно, что существует три очень похожих конструктора: они
прячутся за большим количеством строк реализации. По имеющемуся
коду порой можно даже кое-что рассказать о разных сопровождающих
программистах. Разработчик третьего конструктора, похоже, не знает
о списках инициализации. Кроме того, присваивание пустой строки члену manufacturer является избыточным, из чего можно сделать вывод, что
разработчик не знает, как работают конструкторы и инициализация по
умолчанию.
Что еще более важно, первый и третий конструкторы присваивают переменным-членам разные значения по умолчанию. Подобное обстоятельство
можно заметить и проследить в этом простом примере, но представьте
не столь очевидную ситуацию. Вызывающий код может передавать один,
два или три аргумента и в разных случаях получать разное поведение, чего
не хочет ни один пользователь. Наличие аргументов по умолчанию в перегруженных конструкторах тоже должно вызывать беспокойство.
Что произойдет, если применить внутриклассовые инициализаторы членов?
Ниже размещен пример такого кода:
class piano
{
public:
piano() = default;

60  Часть I. Bikeshedding — это плохо
piano(int number_of_keys_, bool mechanical_, std::string manufacturer_)
: number_of_keys(number_of_keys_)
, mechanical(mechanical_)
, manufacturer(manufacturer_)
{}
piano(int number_of_keys_) {
number_of_keys = number_of_keys_;
}
private:
int number_of_keys = 88;
bool mechanical = true;
std::string manufacturer = "Yamaha";
};

Теперь все значения по умолчанию приведены в соответствие. Наличие
внутриклассовых инициализаторов членов сообщило авторам конструкторов, что значения по умолчанию уже выбраны и им не нужно и не следует
присваивать свои значения.

Аргументы по умолчанию могут запутать ситуацию
в перегруженных функциях
Аргументы по умолчанию в конструкторах сбивают с толку. Они подразумевают значение по умолчанию для чего-либо, но между этим значением
по умолчанию и объявлением переменной-члена существует когнитивная
дистанция. Можно придумать правдоподобные причины для добавления
в конструктор параметра по умолчанию, например: в класс был добавлен
новый член и, чтобы не подвергать изменению весь клиентский код, вы
решили взять на себя технический долг и определить значение по умолчанию. Однако этот технический долг необходимо погасить: в идеале — добавить дополнительный конструктор, который принимает все требуемые
параметры, и объявить устаревшим прежний конструктор.

ПОДВЕДЕМ ИТОГ
Выбор определять или не определять конструкторы по умолчанию должен делаться осознанно. Не все классы имеют осмысленные значения по
умолчанию. Инициализация переменных-членов может происходить в трех
местах: в теле функции-конструктора, в списке инициализации конструктора и в объявлениях переменных-членов, известных как инициализаторы
членов по умолчанию.

C.45. Не определяйте конструктор по умолчанию  61

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

ГЛАВА 1.4

C.131. Избегайте
тривиальных геттеров
и сеттеров
https://t.me/it_boooks

АРХАИЧНАЯ ИДИОМА
Тривиальные геттеры (методы чтения свойства) и сеттеры (методы записи
в свойство) — пережиток ранних этапов развития C++. Обычно они выглядят так:
class x_wrapper
{
public:
explicit x_wrapper(int x_) : x(x_) {}
int get_x() const { return x; } // это геттер
void set_x(int x_) { x = x_; } // это сеттер
private:
int x;
};

Функции get и set просто обращаются к свойству класса и возвращают или
изменяют его значение. На первый взгляд, этот подход имеет определенные
преимущества. Можно поискать в коде имена get_x и set_x, чтобы увидеть,
где изменяется или извлекается значение свойства x. Также можно установить точку останова в функциях и в отладчике перехватить все экземпляры,
где извлечено или изменено значение. Этот подход отвечает требованиям
сохранения конфиденциальности данных: данные инкапсулируются за API.
Но эти функции тривиальны. Они просто препятствуют прямому доступу
к x. Core Guideline не рекомендует такой подход. Однако из примера бессмысленного класса x_wrapper трудно понять, почему это не самое лучшее

C.131. Избегайте тривиальных геттеров и сеттеров  63

решение. Ниже мы приведем несколько более реалистичных примеров.
Но пока поговорим об абстракции, зачем она нужна и почему так важна
в программировании на C++. А попутно рассмотрим немного истории,
познакомим вас с инвариантами классов и покажем важность правильного выбора существительных и глаголов для генерации идентификаторов
функций-членов.

АБСТРАКЦИИ
Одна из основных задач языка программирования — помощь в определении
и воплощении абстракций. Итак, что фактически мы делаем, когда определяем абстракции с помощью языка программирования C++?
Мы трансформируем фрагменты памяти с числами в представления объектов нашей предметной области. Это одна из самых сильных сторон C++,
а также одна из его основных целей.
Дом можно представить как набор чисел. Они могут определять размеры
земельного участка, на котором тот построен; высоту; площади помещений;
количество этажей, комнат, окон и чердаков.
Такая абстракция реализована как последовательность чисел, или полей,
объединенных в запись. Запись определяет порядок расположения полей, а это означает, что, имея набор записей, последовательно хранящихся
в памяти, можно легко и просто перейти к любой записи в наборе и извлечь любое поле из этой записи с помощью простых арифметических
действий.
На заре развития вычислительной техники много времени уходило на
работу с наборами таких записей или таблиц. Простая обработка данных
включала сбор некоторых данных и создание записей или чтение некоторых
записей и создание дополнительных данных. Жизнь была простой, и мы
были счастливы.
Если вы хоть немного знакомы с C++, то без труда узнаете структуры
в записях, переменные-члены в полях и массивы структур в таблицах.
Для простой обработки данных представляется разумным напрямую читать
и изменять поля в записях.
Но представьте, что требуется нечто большее, чем перебор записей. До появления классов приходилось хранить данные в структурах и вызывать

64  Часть I. Bikeshedding — это плохо

функции для работы с данными в них. До появления конструкторов копирования и операторов присваивания не было никакой возможности передавать
экземпляры структур функциям, только указатели на них. До появления
уровней доступа можно было напрямую изменять любые поля в структурах, поэтому было очень важно знать, что делается с данными и что все
остальные ожидают от этих данных. Порой это вызывало значительные
умственные нагрузки.
Примечательно, что обычно данные, находящиеся за пределами области
видимости выполняемой в данный момент функции, были напрямую
доступны для модификации. Они могли совместно использоваться несколькими функциями путем помещения их в область видимости, что
позволяло одной функции записывать данные, а другим — читать их.
Вот как это выглядело:
int modifying_factor;
void prepare_new_environment_data(int input)
{
int result = 0;
/* ... подготовить новое значение данных на основе аргумента input */
modifying_factor = result;
}
int f2(int num)
{
int calculation = 0;
/* ... выполнить некоторые вычисления */
return calculation * modifying_factor;
}

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

C.131. Избегайте тривиальных геттеров и сеттеров  65

и все надеялись, что ее обновит кто-то другой. Каждый считал, что другие
инженеры рассуждают о данных точно так же, как он. Каждый верил, что
через полгода все остальные инженеры продолжат рассуждать о данных
так же, как делали это прежде.
Важно помнить, что, как только данные объявляются в заголовочном файле,
теряется не только контроль над ними, но и всякая надежда на контроль,
и что бы вы ни имели в виду под этим фрагментом данных, начинает безжалостно действовать закон Хайрама: «Если число пользователей API
достаточно велико, то неважно, что вы обещаете в контракте: любое наблюдаемое поведение системы будет зависеть от чьих-то действий»1.
Вы больше не владеете данными, но при этом несете за них ответственность.
Поиск в базе кода идентификатора с полезным и, следовательно, общепринятым именем превращается в кошмар оценки контекста.
В конце концов способные инженеры научились прятать часть данных за
парой функций, одной для записи, а другой для чтения. Это не упрощало
рассуждений о данных, но поиск в коде мест вызова этих функций вкупе
с точками останова в них позволял выявлять, при каких обстоятельствах
изменяются данные.
А вот самые способные инженеры давали функциям говорящие имена,
отражающие их назначение в предметной области, стремясь писать самодокументирующийся код. Например, если часть данных представляла высоту над уровнем моря, они могли назвать функции следующим образом:
/* elevation.h */
void change_height(int new_height);
int height();
/* elevation.c */
int height_ = 0;
void change_height(int new_height)
{
height_ = new_height;
}
int height()
{
return height_;
}
1

https://www.hyrumslaw.com

66  Часть I. Bikeshedding — это плохо

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

ПРОСТАЯ ИНКАПСУЛЯЦИЯ
Вместе с классами появились уровни доступа и функции-члены. О! Уровни доступа! Наконец данные принадлежат мне. Я могу сделать их своей
собственностью, объявив приватными, и тогда никто не сможет коснуться
их, кроме как через одну из общедоступных функций-членов. О! Функциичлены! В C можно было хранить указатели на произвольные функции
в структурах, но функции-члены с неявным указателем на экземпляр
в первом параметре стали настоящим прорывом, настолько просто их было
использовать. Мир заиграл новыми красками.
Стиль программирования, заключающийся в сокрытии данных за парой
функций, перестал быть просто блестящей идеей: он превратился в обычную практику, реализуемую на уровне языка. Был предложен новый
совет, четкий и ясный: «Сделай все свои данные приватными». Было
добавлено новое ключевое слово class, отличающее классы от структур
struct. Для обратной совместимости члены структур по умолчанию оставались общедоступными, а члены классов — приватными (закрытыми).
Так сохранялась и поддерживалась идея о том, что данные должны быть
закрытыми.
Вместе с возможностью хранить данные в приватном интерфейсе появилась возможность предотвратить постороннее вмешательство в эти данные.
Вы можете создать объект и предоставить функции get/set для доступа
к данным. Это и есть инкапсуляция.
Инкапсуляция — один из основных принципов объектно-ориентированного
программирования: ограничение доступа к частям объекта и объединение данных с функциями для их обработки. Это своеобразный механизм
защиты, помогающий инженерам и пользователям ясно представлять

C.131. Избегайте тривиальных геттеров и сеттеров  67

причинно-следственные связи. Внезапно инкапсуляция стала доступна
программистам на C, и произошло следующее.
До:
struct house
{
int plot_size[2];
int floor_area;
int floor_count;
int room_count;
int window_count;
};

После:
class house
{
public:
int get_plot_x() const;
int get_plot_y() const;
int get_floor_area() const;
int get_floor_count() const;
int get_room_count() const;
int get_window_count() const;
void
void
void
void
void

set_plot_size(int x, int y);
set_floor_area(int area);
set_floor_count(int floor_count);
set_room_count(int room_count);
set_window_count(int window_count);

private:
int plot_size[2];
int floor_area;
int floor_count;
int room_count;
int window_count;
};

Одновременно появились стандарты программирования, требующие, например, чтобы «все данные были закрытыми и имели собственные методы чтения
и записи». Кое-где даже предписывалось снабжать функции get квалификатором const. Залитые солнцем долины кода, свободного от ошибок, лежали
прямо за следующим холмом. И все напитки были за счет заведения.
Но постойте-ка, а что в действительности мы получили? Проблемы и неприятности, которые преследовали нас раньше, никуда не исчезли. Мы все

68  Часть I. Bikeshedding — это плохо

еще вынуждены беспокоиться о возможности изменения данных другими.
В чем же тогда смысл функций получения данных?
Вот мы и добрались до сути данной рекомендации. Не поступайте так.
Не пишите геттеры и сеттеры, которые не делают ничего, кроме пересылки
данных между точкой вызова и объектом. Нет смысла делать данные приватными, если вы просто решили открыть к ним доступ другими способами.
Для этого достаточно сделать данные общедоступными. Пряча данные за
функциями get и set, вы ничего не добавляете к пониманию вашего класса
пользователями. Просто получая или изменяя данные с помощью методов,
вы никак не используете всю мощь C++. Хуже того, вы заменили компактный, хотя и потенциально опасный API struct house с общедоступными
членами раздутым API class house. Его труднее охватить одним взглядом.
Кроме того, по-прежнему остается неясной семантика изменения количества этажей. Такое решение выглядит очень странно.
Функции get и set не делают интерфейс более безопасным или менее подверженным ошибкам. Их использование — просто еще один путь для проникновения ошибок.
Вы должны задать себе вопрос: почему вас
беспокоит возможность изменения данных
Нет смысла делать
данные приватными,
другими? Станут ли недействительными
если вы просто решили
экземпляры вашего класса, если произойоткрыть к ним доступ
дет произвольное изменение переменныхдругими способами.
членов? Наложены ли какие-то ограничения на ваши данные? Если да, то ваша
функция set должна выполнять какие-то дополнительные действия.
Например, обеспечить выполнение условия, что площадь пола не должна
превышать площадь участка:
void house::set_floor_area(int area)
{
floor_area = area;
assert(floor_area < plot_size[0] * plot_size[1]);
}

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

C.131. Избегайте тривиальных геттеров и сеттеров  69
class account
{
public:
void set_balance(int);1
int get_balance() const;
private:
int balance;
};

Более удачное решение содержит бизнес-логику:
class account
{
public:
void deposit(int);
void withdraw(int);
int balance() const;
private:
int balance_;
};

Теперь при изменении баланса будут выполняться действия, определяемые
бизнес-логикой. Функция deposit увеличит баланс, а функция withdraw
уменьшит его.

ИНВАРИАНТЫ КЛАССА
Условие, согласно которому площадь пола не может превышать размер
участка, известно как инвариант класса. В более общем смысле инвариант — это условие, которое должно выполняться для всех действительных
экземпляров класса. Условие задается при создании общедоступных функций-членов и поддерживается между их вызовами. Инвариантом для класса
account может быть условие, согласно которому значение balance не должно
опускаться ниже 0, или сумма всех поступлений и списаний должна быть
равна балансу. Пример инварианта для класса house, согласно которому
площадь пола не может превышать размер участка, выражается в функции
void house::set_floor_area(int area). Именно выражение этого инварианта
класса делает метод записи в свойство нетривиальным.
1

Никогда не используйте тип float в финансовых вычислениях. Ошибки округления
и представления будут накапливаться и приводить к неправильным результатам.

70  Часть I. Bikeshedding — это плохо

Взгляните на следующее определение класса. Сможете ли вы самостоятельно найти инварианты?
class point
{
public:
void set_x(float new_x) {
x_ = new_x; }
void set_y(float new_y) {
y_ = new_y; }
float get_x() const {
return x_; }
float get_y() const {
return y_; }
private:
float x_;
float y_;
};

Не нашли? И правильно, потому что их здесь нет. Изменение координаты x не влияет на координату y. Они могут меняться независимо, а точка
остается точкой. Функции-члены не добавляют ничего нового. Именно от
возникновения подобной ситуации предостерегает данная рекомендация.
А вот еще один пример:
class team
{
public:
void add_player(std::string name) {
if (players_.size() < 11) players_.push_back(name); }
std::vector get_players() const {
return players_; }
private:
std::vector players_;
}

Инвариант класса заключается в ограничении числа игроков в команде:
их может быть не более 11.
Уровни доступа позволяют разработчику класса поддерживать инварианты
класса, проверяя влияние операций на переменные-члены и убеждаясь, что
все данные сохраняют допустимые значения.
В этой рекомендации приводится причина: «Тривиальный геттер или
сеттер не добавляет семантической ценности; элемент данных с тем же

C.131. Избегайте тривиальных геттеров и сеттеров  71

успехом может быть общедоступным». Как было показано в примерах с домом и с точкой, это именно так. Нет семантической разницы между struct
house и class house. Функции-члены в классе point избыточны. Существует,
конечно, операционная разница: наличие методов доступа позволяет установить точки останова, чтобы выявить все места, где происходит чтение
или изменение переменных-членов, но это не влияет на семантическую
природу абстракции дома или точки.
При создании класса и проектировании его интерфейса обычно думают об
аспектах абстракции, моделируемой числами: в конце концов, программы
пишутся для компьютера. Иногда, однако, возникает искушение сделать
эти аспекты числовыми элементами данных. Если клиент захочет что-то
изменить, он вызовет функцию, чтобы установить новое значение, и после
некоторой проверки, связанной с поддержкой инвариантов, соответству­
ющий элемент данных модифицируется должным образом.

СУЩЕСТВИТЕЛЬНЫЕ И ГЛАГОЛЫ
Но такой подход не учитывает назначение классов. Класс — это абстракция
идеи, а не набор данных. Причина, объясняющая, почему change_height
и height считаются хорошими идентификаторами функций, заключается
в том, что они описывают происходящее в реальности. Инженера-заказчика
волнует не факт изменения целочисленной величины, а факт изменения
высоты объекта. Для него важно не значение члена height_, а опять же
высота объекта. Это два разных уровня абстракции, и опускание деталей
абстракции — плохая практика, излишне увеличивающая мыслительную
нагрузку на заказчика из предметной области.
Приватные данные и реализация моделируют абстракцию как физическую
сущность. Ожидается, что общедоступные функции-члены будут наполнены
определенным смыслом, связанным с самой абстракцией-сущностью, а не
с ее реализацией. Это точно не относится к функциям get и set. Разделение
на общедоступное и приватное определяет различные уровни абстракции.
В частности, этот подход со всей очевидностью напоминает концепцию
«модель — представление — контроллер» (Model-View-Controller, MVC).
Представьте для примера старый ядерный реактор, состоящий из активной
зоны и пульта управления со множеством кнопок, переключателей и ползунков, а также ряды и ряды индикаторов и стрелочных приборов. Индикаторы и приборы дают информацию о состоянии активной зоны; они — ваши

72  Часть I. Bikeshedding — это плохо

«глаза». Кнопки, переключатели и ползунки каким-то образом изменяют
параметры работы активной зоны; они — ваши исполнительные механизмы
(контроллеры).
То же можно видеть в API. Функции-члены со спецификатором const
позволяют получить информацию об объекте. Другие функции-члены
управляют объектом. Переменные-члены моделируют объект. При выборе
идентификаторов функций представлений и контроллеров для первых
имеет смысл выбирать имена существительные, а для вторых — глаголы,
но не идентификаторы get и set.
Поскольку модель сконструирована из приватных переменных-членов,
подобную реализацию можно обновлять по мере ее развития. Это сложно
сделать, используя пары get/set: обновление реализации означает обновление кода во всех точках вызова функций get/set. Одно из существенных
преимуществ правильно определенного общедоступного интерфейса — он
гораздо стабильнее приватного. А это означает, что клиентский код вряд ли
потребуется менять с изменением реализации абстракции.
После всего вышесказанного следует отметить, что иногда в программах
нужно лишь передавать пакеты связанных данных и не утруждать себя
созданием класса, в котором нет никакой необходимости, хотя бы просто
потому, что данные могут быть всего лишь блоками байтов, прочитанных
из файла или сетевого соединения. В таких случаях предпочтительнее
использовать структуры. Тогда данные будут общедоступны, а геттеры
и сеттеры окажутся не нужны и не важны.

ПОДВЕДЕМ ИТОГ
zz

Тривиальные геттеры и сеттеры не добавляют в интерфейс ничего
полезного.

zz

Нетривиальные сеттеры должны гарантировать соблюдение инвариантов класса.

zz

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

zz

Несвязанные данные без инвариантов класса можно объединять
в структуры.

ГЛАВА 1.5

ES.10. Объявляйте имена
по одному в каждом
объявлении
https://t.me/it_boooks

ПОЗВОЛЬТЕ ПРЕДСТАВИТЬ
Объявление вводит имя в программу. Оно также может ввести имя повторно: множественные объявления вполне допустимы, так как без них
невозможны предварительные объявления. Определение — это особый
вид объявления, оно содержит в себе достаточно деталей, чтобы дать возможность использовать то, что упомянуто в объявлении.
Существует на удивление большое количество типов объявлений. Например, функцию можно объявить так:
int fn(int a);

Это очень распространенный вид объявлений. Оно вводит имя fn типа
«функция, принимающая и возвращающая целое число». Вы можете вызывать эту функцию в своем коде, не определяя ее заранее. Также можно
объявить шаблон функции или класса, например:
template
int fn(T a, U b);
template
class cl{
public:
cl(T);
private:
T t;
};

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

74  Часть I. Bikeshedding — это плохо

можно объявить частичные и явные специализации шаблонов, а также
явные экземпляры шаблонов:
template int fn(T a, int b); // частичная специализация
template int fn(float a, int b); // явная специализация
template class cl; // явный экземпляр

Можно объявить пространство имен:
namespace cg30 {
... // дополнительные объявления
}

Можно объявить внешние ссылки для взаимодействия с программами на
других языках:
extern "C" int x;

Можно объявить атрибут:
// Предупреждать, если возвращаемое значение отбрасывается,
// а не присваивается
[[nodiscard]] int fn(int a);

Существует также набор объявлений, которые можно делать только внутри
блока, например в теле функции. К ним относятся объявления asm, в которых можно указать явные инструкции на языке ассемблера для вставки
в код, генерируемый компилятором:
asm {

}

push rsi
push rdi
sub rsp, 184

Объявив псевдоним, можно создать более короткое и удобное в обращении
имя типа:
using cl_i = cl;

Аналогично можно объявить псевдоним для вложенного пространства имен:
namespace rv = std::ranges::views;

Объявления using позволяют вводить в текущий блок имена из других
пространств имен:
using std::string // После этого объявления можно больше не печатать префикс
// std:: для обращения к типу string в текущем блоке

ES.10. Объявляйте имена по одному в каждом объявлении  75

Директива using, в отличие от объявления using, позволяет внедрить в текущий блок пространство имен целиком:
using namespace std; // Никогда не поступайте так в глобальном
// пространстве имен. Никогда. Никогда, никогда.
// Ни для какого пространства имен.

Объявление using enum внедряет содержимое перечисления в текущий
блок:
using enum country; // перечисление country может быть определено
// где-то еще

Объявление static_assert можно делать в блоке:
static_assert(sizeof(int) == 4); // Проверят, что тип int имеет размер
// четыре байта

Непрозрачные объявления enum позволяют объявлять перечисления без
их определения, просто указав базовый тип и, следовательно, его размер:
enum struct country : short;

Можно даже объявить «ничто»:
;

Наконец, есть простые объявления.
Далее мы сосредоточимся на объявлении объектов. Смысл следующего
объявления должен быть вам понятен:
int a;

Здесь создается экземпляр целочисленного типа с именем a. Пока ничего
сложного. Конечно, этот экземпляр не инициализирован (если только
не объявлен в глобальном пространстве имен), а его идентификатор не имеет
большого значения, поэтому не будем заострять на нем наше внимание.
Следует отметить, что в подобных объявлениях стандарт позволяет указывать списки идентификаторов, разделенных запятыми. Каждый идентификатор интерпретируется как отдельное объявление с теми же специ­
фикаторами. Например:
int a, b, c, d;

Здесь создаются четыре экземпляра целочисленного типа с именами a,
b, c и d.

76  Часть I. Bikeshedding — это плохо

ОБРАТНАЯ СОВМЕСТИМОСТЬ
Взгляните на следующее объявление:
int* a, b, c, d;

Объекты какого типа оно создает?
Надеемся, вы заметили ловушку и сказали, что a — это указатель на int,
а b, c и d — это экземпляры типа int (это одна из причин, почему при объявлении указателей звездочка часто указывается рядом с идентификатором,
а не с типом). Конечно, вы заметили, потому что ждали подвоха, но иногда в практике приходится сталкиваться с ошибками компиляции, когда
программист полагал, что все эти объекты являются указателями на int.
Такое объявление законно, но неразумно. Это ловушка для невнимательных, и ее следует исключить из языка. По крайней мере, таково мнение
опытных разработчиков. К сожалению, такие объявления по стандарту
языка считаются допустимыми. Причина — соображения обратной совместимости. Язык программирования C допускает это, потому что раньше
требовал объявления всех объектов в начале функции. Эти объявления
помогали компилятору определить, насколько должен вырасти стек, и вычислить адреса для всех данных в точке входа в функцию. Это была удобная
оптимизация. Однако разумнее объявлять каждый объект в отдельной
строке, например, так:
int* a;
int b;
int c;
int d;

Обратную совместимость невозможно переоценить. Одна из фундаментальных причин успеха C++ заключалась в его способности компилировать
миллионы строк уже существующего, созданного значительно ранее, кода.
Когда вы создаете новый язык, такой как Rust или Go, никакого кода на
нем не существует, разве только образцы, разработанные самим авторомизобретателем языка. Язык C++, напротив, позволяет выбирать части
существующих программ и постепенно заменять их более четкими абстракциями. Например, техника позднего связывания функций была доступна
в C с самого начала: вы могли сохранять указатели на функции в структурах
и вызывать определенные члены структур в нужные моменты. C++ формализовал этот подход и ввел ключевое слово virtual, сигнализирующее
о том, что это было намерением программиста. Но при желании вы все еще
можете делать что-то по-старому, не используя ключевое слово virtual:

ES.10. Объявляйте имена по одному в каждом объявлении  77
using flump_fn_ptr = void(*)(int);
struct flumper {
flump_fn_ptr flump;
...
};
void flump_fn(int);
void fn()
{
flumper fl { flump_fn; }
fl.flump(7);
}

Существует крайне ограниченное количество случаев, когда может возникнуть потребность в использовании этого старого подхода вместо виртуальных функций, но он все еще поддерживается. Его применение не лучшая
идея, но вы все-таки можете к нему прибегнуть.
Точно так же и в целом: несмотря на непрекращающееся развитие языка C++, сохраняется возможность скомпилировать и запустить старый
код, за исключением некоторых конструкций, таких как std::auto_ptr, на
замену которой пришел тип std::unique_ptr.

ПИШИТЕ БОЛЕЕ ЯСНЫЕ ОБЪЯВЛЕНИЯ
Вернемся к теме рассматриваемой рекомендации. Требование раннего объявления может привести к необходимости объявления довольно большого
количества переменных в начале функции. В Core Guidelines представлен
следующий пример:
char *p, c, a[7], *pp[7], **aa[10];

Если бы в одном объявлении не допускалось перечислять несколько имен,
его можно было бы расширить и оно стало бы таким:
char *p; char c; char a[7]; char *pp[7]; char **aa[10];

или, что еще лучше, таким:
char
char
char
char
char

*p;
c;
a[7];
*pp[7];
**aa[10];

78  Часть I. Bikeshedding — это плохо

Пять строк когнитивной недвижимости вместо одной.
«Но, — можете подумать вы, — что, если моей функции требуется много
переменных? Что делать в таком случае? Перечислить каждую в своей
строке и занять половину экрана? А если мне понадобится сгруппировать
соответствующие переменные, то тогда для отделения групп друг от друга
придется вставить еще и пустые строки. В таком случае я буду вынужден
просматривать массу кода, прежде чем доберусь до фактического начала
функции. Это ухудшит читабельность. Такая идея мне не нравится».
Ключевыми словами здесь являются «сгруппировать соответствующие
переменные». Это напоминает термин «формирование абстракции». Если
у вас есть переменные, связанные каким-то образом, то формализуйте
эту связь, определив структуру, и назовите ее так, чтобы зафиксировать
и идентифицировать эту связь. Добавьте необходимую функциональность
в структуру. После этого вы сможете заменить несколько строк объявлений
одним объявлением экземпляра этой структуры. Всегда будьте готовы
создать полезную абстракцию.

СТРУКТУРНОЕ СВЯЗЫВАНИЕ
Из этого правила есть исключение, которое применяется, когда речь идет
об использовании структурного связывания. Это нововведение появилось
в C++17, оно позволяет связать массив или тип класса, не являющегося
объединением, со списком имен. Вот пример простейшего случая:
int a[2] = {1, 2};
auto [x, y] = a; // x = a[0], y = a[1]

Аналогично выполняется связывание с типом кортежа:
std::unordered_map dictionary;
auto [it, flag] = dictionary.insert({"one", 1});
// it — указывает на элемент
// flag — признак успешного выполнения операции вставки

Точно так же работает связывание со структурой для ее распаковки:
struct s {
int a;
float b;
};

ES.10. Объявляйте имена по одному в каждом объявлении  79
s make_s();
auto [sa, sb] = make_s(); // sa имеет тип int
// sb имеет тип float

В каждом случае в одном объявлении упоминается несколько имен. Однако
мы можем уменьшить любую путаницу, объявив имена в отдельных строках, например:
std::unordered_map dictionary;
auto [it,
// it указывает на элемент
flag] // flag — признак успешного выполнения операции вставки
= dictionary.insert({"one", 1});

То, что выглядит упрощением для одного, может оказаться усложнением
для другого, поэтому выбор способа — это дело личного вкуса.

ПОДВЕДЕМ ИТОГ
Многие рекомендации в Core Guidelines способствуют написанию удобочитаемого и легко воспринимаемого кода. Текущая рекомендация касается
старых привычек и сводится к трем соображениям.
zz

Существует много видов объявлений.

zz

Имена могут вводиться где угодно.

zz

Не запутывайте объявления, вводя более одного имени в строке, если
этого не требует язык.

ГЛАВА 1.6

NR.2. Функции
не обязательно должны
иметь только один оператор
возврата

ПРАВИЛА МЕНЯЮТСЯ
Меня1 удивляет, что даже в ХХI веке, вот уже 20 лет, люди все еще спорят об этом. Этот раздел Core Guidelines называется Non-rules and myths
(«Надуманные правила и мифы»). В него попадает удивительно распространенный совет, согласно которому функции должны иметь только один
оператор возврата.
Требование единственности оператора возврата в функции — это староепрестарое правило. Как же легко забываются успехи, достигнутые в программировании. Я, вернее, мои родители купили мне первый компьютер
в далеком 1981 году. Это был Sinclair ZX81 с процессором NEC Z80, работающим на частоте 3,25 МГц. Операционная система, вшитая в ПЗУ объемом
8 Кбайт, включала интерпретатор BASIC, позволяющий писать простые
программы, которым был доступен скудный 1 Кбайт ОЗУ.
Мне было 14 лет, я хотел писать игры и обнаружил, что лучший способ
для этого — полностью отказаться от интерпретатора BASIC и писать на
языке ассемблера Z80. С помощью руководства Mastering Machine Code on
Your ZX81 Тони Бейкера (Toni Baker)2, поразительной книги Programming
1

Экскурс в историю. Один из авторов, Дж. Гай Дэвидсон, делится собственным опытом
постижения этой рекомендации.

2

Baker T. Mastering Machine Code on Your ZX81. — Reston, VA: Reston Publishing
Company, Inc., 1982.

NR.2. Функции не обязательно должны иметь только один оператор возврата  81

the Z80 Роднея Закса (Rodnay Zaks)1 и ассемблера я смог написать и продать школьным друзьям свои первые игры.
Писать на ассемблере Z80 было гораздо труднее, чем на BASIC. В частности, в BASIC были номера строк и идея подпрограмм. Я мог выполнять
ветвление алгоритма с помощью оператора GOTO, или же GOSUB, который
вернет меня туда, где интерпретатор обработал ключевое слово RETURN.
По мере накопления опыта программирования на ассемблере Z80 я стал
замечать все больше общих концепций между ним и BASIC и понимать
природу языков программирования. Я научился проводить аналогии между
номерами строк и счетчиком команд, между операторами GOTO и GOSUB
и командами jp и call.
Z80 также позволял мне делать ужасные в своем роде вещи: манипулировать порядком выполнения операторов. Например, я выработал привычку
писать свой код так, что, если требовалось выполнить два шага, A и B,
и шаг B сам по себе был полезной частью функциональности, я помещал A
перед B, чтобы не приходилось вызывать B. Программа просто естественным образом переходила к шагу B после выполнения A. Такой стиль имел
побочный эффект: было трудно сказать, как я попал в B — путем перехода
из А или из какого-то другого места, но это не имело для меня значения,
потому что я досконально знал свой код.
Нет, правда.
Еще я мог изменить стек, чтобы вернуться, минуя произвольное количество точек вызова, потратив всего одну инструкцию. Так было быстрее.
Поскольку я досконально знал свой код, я мог таким способом ускорять
свои игры. Все это имело значение.
Нет, правда имело.
Затем я пересел за ZX Spectrum: у него было больше оперативной памяти
(16 Кбайт), а также цвет и звук! Однако с расширением масштабов моей
платформы росли мои амбиции и мой код, и отлаживать его становилось
все труднее. Иногда я не мог понять, откуда я попал в эту точку в программе
и какой код уже был выполнен. Я быстро осознал, что слишком усложняю
себе жизнь различными махинациями и манипуляциями, и стал искать
компромиссы между скоростью выполнения и простотой понимания кода.
Я решил, что дополнительные такты не стоят потери понимания кода, если
я потом не смогу устранить все ошибки. Я узнал, что писать сверхбыстрый
1

Zaks R. Programming the Z80. — Berkeley, CA: Sybex, 1979.

82  Часть I. Bikeshedding — это плохо

код Z80 весело и довольно легко, но отладить его практически невозможно:
необходимо найти золотую середину между производительностью и удобочитаемостью. Это был ценный урок.
В результате я изменил стиль написания кода. Я разбивал его на многократно используемые части и детально описывал, где какая часть начинается
и где заканчивается. Я перестал уменьшать указатель стека, переходить
сразу к внутренним полезным частям больших функций. Жить стало значительно легче.
Далее я перешел на Atari ST с процессором Motorola 68000, работающим
на частоте 8 МГц, и поистине выдающимся объемом ОЗУ 512 Мбайт.
Мой стиль программирования, основанный на детальной организации всех
частей программы, продолжал поддерживать меня. Каждая часть имела
только одну точку входа, и управление всегда возвращалось туда, откуда
был сделан вызов. Я говорил всем, что это единственный верный путь:
я был ортодоксально фанатичен в своем рвении.
Как оказалось, я был не единственным, кто писал такой код. Программисты на FORTRAN и COBOL тоже приходили к подобным ограничениям,
если обжигались на собственной неосторожности вольного обращения
с текстами программ. Так родилась простая мудрость: «один вход, один
выход». Всякая функция должна иметь одну и только одну точку входа,
и должно быть только одно место, куда она возвращает управление:
в точку вызова.
Правило один вход, один выход было частью философии структурного программирования, возникшей из письма Эдсгера Дейкстры (Edsger Dijkstra)
редактору под заголовком GOTO statement considered harmful (о вреде
оператора GOTO)1. Книга Structured Programming2 по-прежнему прекрасно
читается, и я советую прочитать и письмо, и книгу. Они рассказывают, как
развивалось программирование в течение десятилетия.
К сожалению, старые привычки трудно искоренить. Кроме того, причины появления этих привычек с течением времени стираются из памяти.
Эволюция программирования, инновации уменьшают значимость старой
мудрости. Функции в C++ сами избавляют от необходимости следовать идее «одного входа». Синтаксис C++ делает невозможным переход
1

www.cs.utexas.edu/users/EWD/ewd02xx/EWD215.PDF

2

Dahl O. J., Dijkstra E. W., Hoare C. A. R. Structured Programming (Дал У., Дейкстра Э.,
Хоор К. Структурное программирование).

NR.2. Функции не обязательно должны иметь только один оператор возврата  83

в середину функции. Точно так же нет возможности вернуться куда-либо
еще, кроме как в точку вызова (это не касается обработки исключений
и сопрограмм).
Вы можете спросить, какое это имеет отношение к множественным операторам возврата. Дело в том, что, к сожалению, в умах программистов
произошла путаница с предлогами, и «выход В одно место» превратился
в «выход ИЗ одного места».
Роковая случайность.

ГАРАНТИЯ ОЧИСТКИ
Посмотрим на пример, демонстрирующий мудрость использования
единственного оператора возврата. Предположим, что следующая функция вызывает старую библиотеку на C, экспортирующую функции для
получения, управления и отображения ресурсов по целочисленному
идентификатору:
int display(int ID)
{
auto h = get_handle_to_resource(ID);
if (h == 0) {
return 0;
}
auto j = manipulate_resource(h);
if (j < 0) {
release_handle(h);
return 0;
}
auto k = supply_resource_to_system(h, j);
if (k < 0) {
return 0; /* программист забыл освободить дескриптор */
}
display_resource(h);
release_handle(h);
return 1;
}

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

84  Часть I. Bikeshedding — это плохо

проблемы — оставить в функции единственный оператор возврата, перед
которым выполняется освобождение дескриптора, например, так:
int display(int ID)
{
auto result = 0;
auto h = get_handle_to_resource(ID);
if (h != 0) {
auto j = manipulate_resource(h);
if (j >= 0) {
auto k = supply_resource_to_system(h, j);
if (k >= 0) {
display_resource(h);
result = 1;
}
}
}
release_handle(h);
return result;
}

Ан нет, это неправильно. Функцию release_handle следует вызывать, только
если дескриптор был успешно получен. Попробуем иначе:
int display(int ID)
{
auto result = 0;
auto h = get_handle_to_resource(ID);
if (h != 0) {
auto j = manipulate_resource(h);
if (j >= 0) {
auto k = supply_resource_to_system(h, j);
if (k >= 0) {
display_resource(h);
result = 1;
}
}
release_handle(h);
}
return result;
}

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

NR.2. Функции не обязательно должны иметь только один оператор возврата  85

состояние в виде отслеживания возвращаемого значения, что предполагает дополнительную нагрузку на читающего код. Нагрузка, казалось бы,
небольшая, но есть риск ее увеличения, особенно после того, как будет
вычислено правильное значение. И этот риск сам тоже будет расти с ростом кода функции.
Преимущество же этого подхода заключается в том, что функция re­lea­se_
hand­le вызывается независимо от происходящего, хотя она и должна быть
вызвана в правильной ветви if.
Гарантия очистки дескриптора — весьма веский аргумент в пользу единственного оператора возврата. Это разумный совет.
Ошибка в первой реализации display заключалась в том, что дескриптор
ресурса освобождался не перед каждым выходом из функции. Для исправления мы перестроили функцию так, что все соответствующие пути
заканчивались одним вызовом release_handle , после которого можно
безопасно вызвать return.
Выдающейся особенностью C++ является детерминированная, определяемая программистом очистка, обеспечиваемая деструкторами. И никто
не сможет опровергнуть такое мнение. Введение этой возможности одним
махом устранило целый класс ошибок. Под детерминированностью я подразумеваю, что вы точно знаете, когда будет вызван деструктор. В случае
с автоматическими объектами, которые создаются в стеке, этот момент
наступает, когда они покидают область видимости.

ИДИОМА RAII
Для гарантий выполнения кода вместо потока управления безопаснее
использовать идиому, известную как получение ресурсов при инициализации (Resource Acquisition Is Initialization, RAII). Согласно этой идиоме
функции получения и освобождения дескрипторов объединяются в одну
структуру:
int display(int ID)
{
struct resource {
resource(int h_) : h(h_) {}
~resource() { release_handle(h); }
operator int() { return h; }

86  Часть I. Bikeshedding — это плохо
private:
int h;
};

}

resource r(get_handle_to_resource(ID));
if (r == 0) {
return 0;
}
auto j = manipulate_resource(r);
if (j < 0) {
return 0;
}
auto k = supply_resource_to_system(r, j);
if (k < 0) {
return 0;
}
display_resource(r);
return 1;

Обратите внимание, что этот код сигнализирует об ошибках не путем
вызова исключений, а путем выполнения операторов возврата с разными
значениями, сигнализирующими об успехе или неудаче. Если бы это была
библиотека C++, а не библиотека C, можно было бы ожидать, что функция
будет генерировать исключение, а не просто возвращать. Как в таком случае
выглядел бы наш пример?
void display(int ID)
{
struct resource {
resource(int h_) : h(h_) {}
~resource() { release_handle(h); }
operator int() { return h; }
private:
int h;
};

}

resource r(get_handle_to_resource(ID));
auto j = manipulate_resource(r);
supply_resource_to_system(r, j);
display_resource(r);

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

NR.2. Функции не обязательно должны иметь только один оператор возврата  87

успехе или неудаче. При использовании исключений для сигнализации
об ошибках нет необходимости в операторе возврата: код предполагает
успех и просто вызывает исключение, если терпит неудачу. Он не вычисляет
значение и не возвращает его.
Эта структура очень полезна и стоит того, чтобы извлечь ее из этой функции и сделать доступной для других пользователей. В реальной жизни
существует много программ, взаимодействующих с библиотеками на C
и содержащих нечто подобное:
template
struct RAII
{
RAII(T t_) : t(t_) {}
~RAII() { release_fn r; (t); }
operator T() { return t; }
private:
T t;
};

Здесь T обычно является встроенным или другим простым типом.
Следует признать, что исключения используются далеко не во всех программах на C++. Генерация исключения требует раскручивания стека
и уничтожения всех автоматических объектов, созданных между try и catch.
Для этого программа вынуждена хранить дополнительные данные, занимающие память. C++ используется в самых разных окружениях, иногда
с весьма жесткими ограничениями на объем доступной памяти или время
выполнения. Одному из авторов однажды довелось быть свидетелем реальной драки на стоянке из-за буфера 1 Кбайт, внезапно ставшего доступным
после какой-то хитрой оптимизации.
Компиляторы позволяют запретить обработку исключений, что дает в результате меньшие по размеру и более быстрые двоичные файлы. Но это
опасное решение.
Во-первых, вам придется довольствоваться неполноценной обработкой
ошибок. И это в то время, как dynamic_cast генерирует исключение, если
приведение к типу ссылки оказывается невозможным. Стандартная библиотека вызывает исключение, если ей не удается разместить новый
объект в памяти. Неправильный доступ к объекту std::variant тоже вызовет исключение.
Во-вторых, уменьшение размеров двоичных файлов и увеличение скорости выполнения не гарантируются. Добавление сложного кода обработки

88  Часть I. Bikeshedding — это плохо

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

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

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

ПИШИТЕ ХОРОШИЕ ФУНКЦИИ
В Core Guidelines имеется около 50 рекомендаций, касающихся функций.
Мы рассматриваем две из них в отдельных главах, однако считаем нужным
упомянуть некоторые другие в контексте использования нескольких операторов возврата. Например, рекомендация «F.2. Функция должна выполнять
одну логическую операцию» предлагает разбивать большие функции на
более мелкие и с подозрительностью относиться к функциям с большим
количеством параметров. Один из побочных эффектов следования этому
совету: ваши функции, скорее всего, будут иметь единственный оператор
возврата с результатом функции.
То же относится к рекомендации «F.3. Функции должны быть короткими
и простыми». В ней приводится пример функции, занимающей 27 строк
и включающей три оператора return. Однако в последнем примере, где
часть логики помещена в две вспомогательные функции, главная функция занимает почти в три раза меньше строк и по-прежнему содержит три
оператора return, определяющих результат на основе входных параметров.
Рекомендация «F.8. Отдавайте предпочтение чистым функциям» — отличный, но трудный в реализации совет. Чистыми называют функции,
не ссылающиеся на состояние за пределами своей области видимости. Это
делает их распараллеливаемыми, простыми для понимания, проще поддающимися оптимизации и опять же более короткими и простыми.

NR.2. Функции не обязательно должны иметь только один оператор возврата  89

Важно отметить, что железных советов очень и очень мало. Типичные рекомендации, как правило, реализуются на уровне языка. Например, совет
«не допускайте утечки ресурсов» реализуется функциями-деструкторами
и интеллектуальными указателями из стандартной библиотеки.
Наличие единственного оператора return может служить признаком соблюдения других хороших практик, но это не универсальное правило. Это
дело вкуса и стиля. Взгляните на следующую функцию:
int categorize1(float f)
{
int category = 0;
if (f >= 0.0f && f < 0.1f) {
category = 1;
}
else if (f >= 0.1f && f < 0.2f) {
category = 2;
}
else if (f >= 0.2f && f < 0.3f) {
category = 3;
}
else if (f >= 0.3f && f < 0.4f) {
category = 4;
}
else {
category = 5;
}
return category;
}

и сравните ее с этой функцией:
int categorize2(float f)
{
if (f >= 0.0f && f < 0.1f)
return 1;
}
if (f >= 0.1f && f < 0.2f)
return 2;
}
if (f >= 0.2f && f < 0.3f)
return 3;
}
if (f >= 0.3f && f < 0.4f)
return 4;
}
return 5;
}

{
{
{
{

90  Часть I. Bikeshedding — это плохо

Какая из них лучше? Они обе делают одно и то же. Для обеих любой компилятор, скорее всего, сгенерирует идентичный код. Вторая содержит несколько операторов возврата, но сама короче. Первая содержит дополнительное
состояние, но четко определяет последовательность взаимоисключающих
условий, а не скрывает эту информацию за отступами. В зависимости от
вашего опыта, используемых языков программирования, усвоенных советов
по оформлению кода и целого ряда других условий вы будете выбирать тот
или иной подход. Объективно ни один из этих подходов не лучше другого;
и совет «Каждая функция должна иметь только один оператор возврата»
нельзя назвать однозначно разумным.

ПОДВЕДЕМ ИТОГ
Все мы хотели бы иметь однозначные и ясные правила, которые можно
было бы легко и точно применять, но таких золотых правил очень мало,
и обычно они реализованы на уровне языка программирования. Правило
единственного оператора возврата устарело и подлежит отмене в контексте
C++.
zz

Разберитесь, из чего проистекает то или иное общераспространенное
правило.

zz

Различайте возврат результатов и вызов исключений.

zz

Определите, какие правила являются делом вкуса.

II
НЕ НАВРЕДИТЕ СЕБЕ

Глава 2.1 P.11. Инкапсулируйте беспорядочные конструкции,
а не разбрасывайте их по всему коду.
Глава 2.2 I.23. Минимизируйте число параметров в функциях.
Глава 2.3 I.26. Если нужен кросс-компилируемый ABI, используйте
подмножество в стиле C.
Глава 2.4 C.47. Определяйте и инициализируйте переменные-члены
в порядке их объявления.
Глава 2.5 CP.3. Сведите к минимуму явное совместное использование
записываемых данных.
Глава 2.6 T.120. Используйте метапрограммирование шаблонов,
только когда это действительно необходимо.

ГЛАВА 2.1

P.11. Инкапсулируйте
беспорядочные конструкции,
а не разбрасывайте их
по всему коду

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

ВСЕ ОДНИМ ГЛОТКОМ
Одно из качеств, присущих великому инженеру, — способность замечать,
когда что-то начинает выходить из-под контроля. Мы все отлично замечаем, когда что-то уже вышло из-под контроля. Кто из нас не начинал обзор
кода с фразы: «Здесь намешано довольно много всего, поэтому было трудно
разобраться». К сожалению, этот навык редко позволяет предвидеть начало
возникновения беспорядка в коде.
Рассмотрим пример. Допустим, программе необходимо прочитать некоторые настройки из внешнего файла, указанного в командной строке.
Они объявлены в виде пар «ключ/значение». Возможных параметров всего
с десяток, но мы, как умные инженеры, решаем написать для этой цели отдельную функцию. Назовем ее parse_options_file. Она будет принимать
имя файла, извлеченное из командной строки. Если имя файла не указано,
то функция не будет вызвана. Учитывая сказанное, получаем сигнатуру
функции:
void parse_options_file(const char*);

P.11. Инкапсулируйте беспорядочные конструкции  93

Функция имеет простую реализацию: открыть файл, читать настройки
строку за строкой, изменять состояние соответствующим образом — и так
до тех пор, пока не будет достигнут конец файла. Вот как это могло бы
выглядеть в коде:
void parse_options_file(const char* filename)
{
auto options = std::ifstream(filename);
if (!options.good()) return;
while (!options.eof())
{
auto key = std::string{};
auto value = std::string{};
options >> key >> value;
if (key == "debug")
{
debug = (value == "true");
}
else if (key == "language")
{
language = value;
}
// и т.д. ...
}
}

Круто! Можно легко добавлять новые и удалять старые настройки, и все
в одном месте. Если файл содержит неподдерживаемые настройки, то можно
просто уведомить пользователя в конечном объявлении else.
Через пару дней понадобилось добавить новую настройку, значением которой может быть несколько слов. Пустяки! Ведь есть возможность просто прочитать весь текст до конца строки. Для этого создадим буфер на
500 символов и сделаем из него строку. Это будет выглядеть так:
else if (key == "tokens")
{
char buf[500];
options.getline(&buf[0], 500);
tokens = std::string{buf};
}

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

94  Часть II. Не навредите себе

его позже и применять настройки, только когда это допустимо. Имейте в виду,
сейчас мы отслеживаем состояние, и об этом обстоятельстве нужно помнить.
Спустя месяц файлы с недопустимыми настройками начинают вызывать
волнение и клиенты просят применять настройки, только если все они
действительные. Что ж, вздохнем и примемся за дело. Можно создать объект с настройками, содержащий новое состояние, и возвращать его, только
если все встреченные настройки действительные. Здесь пригодится тип
std::optional. Извлекаем свою функцию из репозитория и обнаруживаем,
что бывшая прекрасная, аккуратная, ухоженная, элегантная, красивая,
неповторимая функция пользуется, наверное, большой популярностью,
потому что другие инженеры добавили в нее свои настройки. На текущий
момент функция обрабатывает 115 настроек. Поддерживать ее будет проблематично, но ничего страшного, ведь это просто набор значений, которые
будут получены в функции, а затем переданы в точку вызова...
Но не спешите. Остановитесь и подумайте. У вас есть 600-строчная функция, наполненная самыми разными данными и огромным количеством
условных операторов. Вы действительно считаете себя новым Дональдом
Кнутом?1 Что здесь произошло?
Мы создали (или позволили создать) запутанную конструкцию. Одна функция с несколькими экранами в длину и несколькими вкладками в глубину,
все еще неуклонно растущая, — вот что это такое. Эта функция пострадала
от расползания области видимости, и нужно обязательно найти способ
ограничить ее, прежде чем все вокруг рухнет: мы-то прекрасно понимаем,
что это только вопрос времени, когда ошибки начнут сыпаться как из рога
изобилия и нам придется постоянно поддерживать и чинить это чудовище.
Необходимо обязательно выпросить время для рефакторинга, прежде чем
катастрофа поглотит и код, и вашу карьеру разработчика.

ЧТО ОЗНАЧАЕТ ИНКАПСУЛИРОВАТЬ
ЗАПУТАННУЮ КОНСТРУКЦИЮ
В начале главы мы говорили, что рассмотрим понятия инкапсуляции, сокрытия информации и абстрагирования. Пришло время выполнить обещание.
Инкапсуляция — это процесс включения одного или нескольких элементов в единую сущность. Как ни странно, эта сущность так и называется:
1

https://ru.wikipedia.org/wiki/Кнут,_Дональд_Эрвин

P.11. Инкапсулируйте беспорядочные конструкции  95

инкапсуляция. C++ предлагает несколько механизмов инкапсуляции. Наиболее очевидный из них — класс. Возьмите некоторые данные и функции,
заключите их в пару фигурных скобок и поместите ключевое слово class
(или struct) и идентификатор перед открывающей скобкой. Другой механизм — перечисления: возьмите множество констант, заключите их в пару
фигурных скобок, а впереди поместите ключевое слово enum и идентификатор. Определения функций — еще одна форма инкапсуляции: возьмите
набор инструкций, заключите их в пару фигурных скобок и поместите
впереди идентификатор и пару круглых скобок, которые, в свою очередь,
могут содержать параметры.
Но можно и продолжить! Не забывайте про пространства имен. Возьмите
множество определений и объявлений, заключите их в пару фигурных
скобок и поместите впереди ключевое слово namespace и необязательный
идентификатор. Файлы с исходным кодом тоже являются инкапсуляцией:
возьмите множество определений и объявлений, поместите их в файл и сохраните в файловой системе под некоторым именем. Модули — это первый
новый механизм инкапсуляции, появившийся за долгое время. Они подобны
файлам с исходным кодом: возьмите множество определений и объявлений,
поместите их в файл, добавьте ключевое слово export в начале и сохраните
в файловой системе под некоторым именем.
Любой имеющий опыт работы с модулями скажет, что инкапсуляция —
это еще не все. В каждом из перечисленных примеров мы просто объединили какие-то элементы вместе и дали им общее имя. Будучи умными
программистами, мы всегда стараемся объединять взаимосвязанные
элементы. Сокрытие информации — более тонкая работа, требующая
принятия более обдуманных решений. Собрав элементы в единую сущность, нужно решить, какие из них должны быть доступны внешнему миру,
а какие — нет. Сокрытие информации подразумевает наличие некоторой
инкапсуляции, но инкапсуляция не означает, что имеет место сокрытие
информации.
Некоторые механизмы инкапсуляции в C++ поддерживают сокрытие информации. Классы предлагают уровни доступа. Приватные члены скрыты
от клиентов структуры. Скрывая реализацию, мы освобождаем клиентов
от бремени поддержки инвариантов класса и предотвращаем нарушение
этих инвариантов. Перечисления не поддерживают сокрытия информации,
то есть они не дают возможности сделать доступными только несколько
членов. Функции прекрасно скрывают информацию: идентификатор и тип
возвращаемого значения общедоступны, но реализация скрыта. Пространства имен могут открывать объявления и скрывать определения, охватывая

96  Часть II. Не навредите себе

несколько файлов. Заголовочные файлы
и файлы с исходным кодом по своим
действиям во многом подобны модулям.

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

Давайте посмотрим, как в нашем примере
может помочь инкапсуляция. Имеется
множество настроек, которые обрабатываются в одной функции. Также у нас
может быть отдельная функция для обработки каждой настройки. Эти функции вызываются в операторах if,
проверяющих ключ. Функции могут возвращать логическое значение
в зависимости от допустимости значения параметра.
Выглядит неплохо: все настройки инкапсулированы в отдельные функции,
и можно легко добавлять дополнительные функции для новых настроек,
нужно лишь расширить функцию синтаксического анализа для каждого
из них. Мы даже можем зафиксировать возвращаемые значения для проверки файла с настройками. Но нам все еще нужно создать объект, который
будет применять настройки, если они допустимы. Поэтому его тоже нужно
расширить при добавлении новой настройки. Об этом можно упомянуть
в документации, и в любом случае другие инженеры получат хорошие примеры в виде уже реализованных функций.
Ваше чутье должно подсказать вам, где именно что-то может пойти не так.
Вы все еще надеетесь, что другие инженеры поступят правильно, добавляя
новые настройки. Но они ведь могут неправильно понять природу процесса проверки, забыть проверить функцию в операторе if или ошибиться
в тексте с именем настройки. Да, вы значительно улучшили ситуацию,
но иногда инкапсуляции и сокрытия информации недостаточно. Чтобы
решить оставшиеся проблемы, придется использовать большую пушку:
абстрагирование.

НАЗНАЧЕНИЕ ЯЗЫКА И ПРИРОДА АБСТРАКЦИИ
Абстрагирование — коварное слово. Дело не в том, что результатом абстрагирования является абстракция, так же как результатом инкапсуляции
является инкапсуляция. Рассмотрим процесс абстрагирования, применив
тот же подход, что мы использовали для знакомства с идеями инкапсуляции
и сокрытия информации.

P.11. Инкапсулируйте беспорядочные конструкции  97

Буквально «абстрагирование» означает «вычленение». В контексте программирования это означает выявление и выделение важных частей проблемы,
их вычленение и отбрасывание остального. Мы отделяем важные части от
деталей реализации и помечаем абстракции идентификаторами. Возьмем
для примера функции: мы объединяем набор инструкций в единую сущность и даем ей имя. Функция — это абстракция с именем, значимым для
предметной области. То же относится к классам: класс — это абстракция
с именем, значимым для предметной области, содержащая соответству­
ющие функции, которые моделируют поведение, подразумеваемое именем.
Выбор того, что именно должно лежать в пределах абстракции, а что
оставаться за ее границами, — целое искусство. Этим абстрагирование отличается от простой инкапсуляции. Кроме того, мы использовали слово
«искусство», потому что нет механистического метода определения, где
провести черту между тем, что относится к абстракции, а что — нет. Способность выделять абстракцию приходит с практикой и опытом.
Но вернемся к нашей задаче. Попытаемся проанализировать файл с парами
«ключ/значение» и применить результаты к окружению, если они допустимы. Функция имеет хорошее говорящее имя: parse_options_file. Проблема
заключается в безопасном добавлении произвольных пар «ключ/значение».
Действительно ли идентификация полного набора пар имеет отношение
к parse_options_file? Дело происходит внутри области видимости? Можно ли отделить настройки от функции?
В настоящий момент мы просто извлекаем ключи из файла и проверяем
их в постоянно растущей цепочке операторов if-else, потому что оператор switch-case не поддерживает выбор вариантов по строкам. Такая
конструкция выглядит как ассоциативный контейнер. На самом деле для
этой задачи идеально подошло бы сопоставление ключей с указателями на
функции с помощью встроенного класса map. В этом случае наша функция
существенно упростилась бы и превратилась в одно обращение к map и последующий вызов соответствующей функции.
auto options_table = std::map
{{"debug"s, set_debug},
{"language"s, set_language}}; // сюда добавляются другие настройки
void parse_options_file(const char* filename) {
auto options = std::ifstream(filename);
if (!options.good()) return;
while (!options.eof()) {
auto key = std::string{};

98  Часть II. Не навредите себе

}

}

auto value = std::string{};
options >> key >> value;
auto fn = options_table.find(key);
if (fn != options_table.end()) {
(*(fn->second))(value);
}

Важной частью этой функции является анализ файла с настройками и выполнение некоторых действий для каждого из ключей. К сожалению, вместе с этим утрачена способность значений содержать пробелы. Оператор
шеврона (>>) прекращает извлечение, встретив пробел. Мы еще вернемся
к этой проблеме.
И все же эта функция выглядит намного лучше. Осталось только инициализировать сопоставление ключей и указателей на функции. К сожалению,
мы только что переместили проблему из одного места в другое. Инициализатор — это еще одно место, где пользователи могут споткнуться: легко забыть
обновить инициализатор. Может быть, эту задачу можно автоматизировать?
Да, можно. Вместо сопоставления ключей с указателями на функции можно
сопоставить их с объектами функций с помощью конструкторов и создавать
статические объекты, а не функции. Конструктор может вставить адрес
объекта в map. На самом деле все объекты-функции могут наследовать один
базовый класс, который сделает это автоматически. Кроме того, имея базовый класс, мы сможем добавить проверочную функцию (validate) перед
основной совершающей (commit). Кажется, все сходится.
auto options_table = std::map{};
class command {
public:
command(std::string const& id) {
options_table.emplace(id, this);}
virtual bool validate(std::string const&) = 0;
virtual void commit(std::string const&) = 0;
};
class debug_cmd : public command {
public:
debug_cmd() : command("debug"s) {}
bool validate(std::string const& s) override;
void commit(std::string const& s) override;
};
debug_cmd debug_cmd_instance;

P.11. Инкапсулируйте беспорядочные конструкции  99
class language_cmd : public command {
public:
language_cmd() : command("language"s) {}
bool validate(std::string const& s) override;
void commit(std::string const& s) override;
};
language_cmd language_cmd_instance;

Что дальше? Несмотря на то что мы анализируем файл с настройками, по
сути, мы просто читаем последовательность символов. Эта последовательность не обязательно должна быть получена из файла: она может поступить из самой командной строки. Поэтому давайте переименуем функцию
в parse_options и изменим входной параметр на std::istream. Если параметр,
переданный в командной строке, не распознается как один из поддерживаемых ключей, его можно распознать как имя файла, попытаться открыть
его и выполнить рекурсивный вызов.
void parse_options(std::istream& options) {
while (options.good()) {
auto key = std::string{};
auto value = std::string{};
options >> key >> value;
auto fn = options_table.find(key);
if (fn != options_table.end()) {
if ((*(fn->second))->validate(value)) {
(*(fn->second))->commit(value);
}
} else {
auto file = std::ifstream(key);
parse_options(file);
}
}
}

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

100  Часть II. Не навредите себе

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

УРОВНИ АБСТРАКЦИИ
Другой способ перейти от одной функции синтаксического анализа к более
мелким наборам функций — сгруппировать связанные действия в отдельные
функции, например, так:
void parse_options_file(const char* filename)
{
auto options = std::ifstream(filename);
if (!options.good()) return;
while (!options.eof())
{
auto key = std::string{};
auto value = std::string{};
options >> key >> value;
parse_debug_options(key, value);
parse_UI_options(key, value);
parse_logging_options(key, value);
// и т. д. ...
}
}

Этот подход действительно решает проблему инкапсуляции запутанной
конструкции: теперь у нас есть несколько функций, каждая из которых
обрабатывает отдельную категорию. Однако это решение лишь сместило
проблему, но не улучшило ситуацию. Инженеры, на плечи которых ляжет
сопровождение, должны будут решить, в какую функцию добавить свой
синтаксический анализатор. Когда эти функции станут слишком большими, придется решать вопрос об их дальнейшем разделении. Такой подход
не учитывает уровни абстракции.
Чтобы понять суть уровней абстракции, рассмотрим семиуровневую
модель OSI1. Эта модель делит коммуникационную систему на уровни
абстракции. Каждый уровень определяет интерфейс для следующего
уровня, но не для предыдущего. Инженеры работают на том уровне, который соответствует их специальности. Например, гипотетически вы ин1

https://ru.wikipedia.org/wiki/Сетевая_модель_OSI

P.11. Инкапсулируйте беспорядочные конструкции  101

женер-программист, а не инженер-электронщик. На уровне 1, физическом
уровне, вы чувствовали бы себя очень некомфортно, и вам было бы гораздо удобнее работать на уровне 7, прикладном уровне. Возможно, вы
слышали термин «full-stack-инженер». Такой инженер способен продуктивно работать на всех уровнях. Но это мифическое создание вряд ли
встречается в реальности.
Уровни абстракции в задаче синтаксического анализа можно описать так,
как показано ниже.
1. Уровень потоковой передачи, доставляющий поток данных в...
2. Уровень синтаксического анализа, доставляющий отдельные символы в...
3. Уровень словаря, сопоставляющий символы с токенами и доставляющий их в...
4. Уровень токенов, проверяющий ввод и обновляющий значения.
Все эти абстракции являются отдельными, непересекающимися частями
задачи.

АБСТРАКЦИЯ ПУТЕМ РЕФАКТОРИНГА
И ПРОВЕДЕНИЯ ЛИНИИ
Ключом к абстрагированию является осознание, где провести линию, разделяющую уровни. Как отмечалось выше, это искусство, а не наука, но есть
три вещи, на которые следует обратить внимание.
Во-первых, это излишняя детализация. Оцените, тратится ли время на
выполнение действий, которые кажутся совершенно не связанными с поставленной задачей? В Core Guidelines, в описании обсуждаемой рекомендации, приводится пример бесконечного цикла for, выполняющего чтение
файла, проверку и перераспределение памяти для структуры. Можно также
представить проектирование вычурных структур данных для локального
использования. Можно ли использовать такие структуры данных вне этого
контекста? Случится ли такое когда-нибудь? Если ответ положительный,
то нужно взять эту структуру и переместить ее в общую библиотеку. Разделение деталей по разным библиотекам — это форма абстракции, которая
применима как к примеру в Руководстве, так и к понятию извлечения
структур данных.

102  Часть II. Не навредите себе

Во-вторых, примите во внимание многократное повторение. Какие закономерности имеют место? Приходилось ли копировать какие-то фрагменты
кода? Можно ли выразить их в виде отдельной функции или шаблона?
Выделите этот код в функцию, дайте ей имя и радуйтесь, что определили
абстракцию.
В-третьих, держите в голове все то же «изобретение колеса». Этот аспект
немного отличается от повторения и является комбинацией первых двух
пунктов. На определение и именование фундаментальных алгоритмов
было потрачено много времени. Убедитесь, что в стандартной библиотеке
нет ничего похожего.
Повторение — это признак, что существует алгоритм, ожидающий, пока
его откроют, а хороший алгоритм — это просто краткое описание того, что
делает фрагмент кода. В 2013 году Шон Пэрент (Sean Parent) выступил
с докладом под названием C++ Seasoning1, большую часть первой половины которого он посвятил мантре «откажись от простых (необработанных)
циклов». Он советовал использовать существующий алгоритм, такой как
std::find_if, или реализовать известный алгоритм в виде шаблона функции
и внести его в библиотеку с открытым исходным кодом, или разработать
совершенно новый алгоритм, написать о нем статью и стать знаменитым,
выступая с докладами. Это отличный совет, который поможет вам избавиться от запутанного кода с повторениями.

ПОДВЕДЕМ ИТОГ
Запутанный код не позволяет читателю с первого взгляда понять происходящее. Избегайте этого.

1

zz

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

zz

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

zz

Определяйте, как инкапсулировать все эти части.

https://www.youtube.com/watch?v=W2tWOdzgXHA

ГЛАВА 2.2

I.23. Минимизируйте число
параметров в функциях

СКОЛЬКО ОНИ ДОЛЖНЫ ПОЛУЧАТЬ?
Взгляните на следующее объявление функции:
double salary(PayGrade grade, int location_id);

Приятно видеть double salary («удвоить зарплату») в начале строки кода,
но, к сожалению, цель этой функции — не удвоить вам зарплату, а сообщить о размере зарплаты для заданного разряда в заданном месте. Кроме
того, как отмечалось выше, для финансовых расчетов предпочтительнее
использовать целочисленные типы, поэтому давайте изменим объявление
этой функции так:
int salary(PayGrade grade, int location_id);

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

104  Часть II. Не навредите себе

Это не проблема: мы легко можем добавить в функцию параметр, представляющий стаж работы. Стаж будет измеряться в годах, поэтому определим
параметр типа int:
int salary(PayGrade grade, int location_id, int years_of_service);

Возможно, увидев пару целочисленных параметров, вы уже понимаете, что
может возникнуть ошибка, если соответствующие аргументы перепутать
местами. Вы мысленно ставите галочку, чтобы посмотреть, сколько разных
местоположений существует, и преобразовать второй параметр в более
специализированный тип, определив перечисление.
Проходит время, механизм оплаты снова меняется, и возникает необходимость добавить еще один параметр — численность команды. Кто-то выразил недовольство, что уровни оплаты труда руководителей не отражают
дополнительное бремя управления командами, насчитывающими более
десяти человек. Вместо того чтобы добавить новый разряд, что потребовало бы огромного количества бюрократических переговоров, было решено
добавить новое правило вычисления размера оплаты труда руководителей,
которое, как и в случае с выслугой лет, вводит небольшой повышающий
коэффициент. Для реализации этого правила мы добавляем четвертый
параметр, который передает количество подчиненных:
int salary(PayGrade grade, int location_id, int years_of_service,
int reports_count);

Граница reports_count зависит от уровня оплаты. Для руководителей
больших команд вступает в силу множитель оплаты выше определенного
уровня. Однако информация о том, применяется ли множитель, важна для
других функций. После обширного обсуждения о преимуществах возврата
std::pair по сравнению с добавлением указателя на bool в список параметров команда, выступавшая за std::pair, проиграла, и теперь
сигнатура функции выглядит так:
int salary(PayGrade grade, int location_id, int years_of_service,
int reports_count, bool* large_team_modifier);

Оказывается, есть несколько десятков мест, границы которых довольно
размыты, поэтому тип перечисления не подходит для второго параметра.
Сейчас эта функция сильно перегружена. Наличие в сигнатуре трех целочисленных параметров, идущих подряд, — это потенциальная ловушка для
невнимательных. Если у руководителя с семилетним стажем в подчинении
имеется пять человек, то перемена местами этих двух значений при обращении к функции, скорее всего, даст кажущееся достоверным, но ошибочное

I.23. Минимизируйте число параметров в функциях  105

значение. Такую ошибку трудно обнаружить из-за свойственной человеку
«слепоты к словам».
Кроме того, функция не только запрашивает информацию из базы данных,
но также выполняет некоторые дополнительные вычисления, одно из которых зависит от другого. Функция называется salary (зарплата) и кажется
безобидной, но в действительности за кулисами она выполняет множество
действий. Если для приложения, использующего эту функцию, важна высокая производительность, то это может иметь значение.

УПРОЩЕНИЕ ЧЕРЕЗ АБСТРАГИРОВАНИЕ
Рассматриваемая рекомендация предполагает, что двумя наиболее распространенными причинами слишком большого количества параметров
в функциях являются следующие.
1. Отсутствие абстракций.
2. Нарушение принципа «одна функция, одна ответственность».
Рассмотрим их подробнее.
Цель функции salary — вычислить значение для заданного состояния.
Сначала функция имела два элемента состояния, но по мере изменения
требований их количество выросло. Однако одно оставалось неизменным:
функция вычисляла зарплату по информации о сотруднике. Поразмыслив
после появления третьего параметра в сигнатуре функции, программисты
решили, что разумнее инкапсулировать параметры в единую абстракцию
и назвать ее SalaryDetails.
В этом и заключается отсутствие абстракции. Если у вас появился набор
состояний, необходимый для достижения цели, и состояния эти, возможно,
связаны между собой, то высока вероятность, что вы обнаружили абстракцию. Соберите эти состояния в один класс, дайте ему имя и сформируйте
взаимные связи между ними в виде инвариантов класса.
Применив этот процесс к функции salary, получаем структуру SalaryDetails:
struct SalaryDetails
{
SalaryDetails(PayGrade grade_, int location_id_, int years_of_service_,
int reports_count_);
PayGrade pay_grade;
int location_id;

106  Часть II. Не навредите себе
int years_of_service;
int reports_count;
};

и сигнатуру функции:
int salary(SalaryDetails const&);

Это лишь часть необходимых улучшений. В сигнатуре конструктора все еще
присутствует три целочисленных параметра, готовых, как капканы, к поимке
невнимательных. На самом деле в Core Guideline имеется рекомендация,
предупреждающая такую практику: «I.24. Избегайте смежных параметров
одного типа из-за риска перепутать местами аргументы с разными значения­
ми в вызове функции». Хорошо, что существуют методы смягчения этой
проблемы, такие как строгая типизация, так что не все потеряно.
По мере изменения требований к вычислению заработной платы в структуру SalaryDetails могут вноситься соответствующие уточнения. Более
того, salary можно сделать функцией-членом абстракции SalaryDetails,
а large_team_modifier превратить в предикат, то есть в функцию, возвращающую true или false, и создать класс:
class SalaryDetails
{
public:
SalaryDetails(PayGrade grade_, int location_id_, int years_of_service_,
int reports_count_);
int salary() const;
bool large_team_manager() const;
private:
PayGrade pay_grade;
int location_id;
int years_of_service;
int reports_count;
};

Клиентский код в таком случае мог бы выглядеть так:
auto salary_details = SalaryDetails(PayGrade::SeniorManager, 55, 12, 17);
auto salary = salary_details.salary();
auto large_team_manager = salary_details.large_team_manager();

Если решение с использованием функций-членов вам не подходит, то
переменные-члены можно объявить общедоступными, а клиентский код
будет выглядеть так:
auto salary_details = SalaryDetails(PayGrade::SeniorManager, 55, 12, 17);
auto salary = calculate_salary(salary_details, &large_team_manager);

I.23. Минимизируйте число параметров в функциях  107

Обобщим то, что обсуждали выше. Нам потребовалась функциональность
для получения значения из некоторого состояния. Количество состояний
росло. Мы абстрагировали состояние в класс и добавили функции-члены,
делающие то, что изначально требовалось от оригинальной функции.
Уделим немного времени и обсудим, откуда взялись данные, используемые
в примере вызова функции. Мы явно указали 55, 12 и 17, но такой подход к передаче аргументов маловероятен. Скорее всего, где-то в системе
существует класс Employee, содержащий эту информацию, и она просто
передавалась в функцию salary, например, так:
for (auto const& emp : employees)
auto final_salary = calculate_salary(
PayGrade::SeniorManager, emp.location, emp.service, emp.reports);

Когда мы видим такой вызов функции, то сразу удивляемся, почему эта
функция не член класса источника данных. Правомерен вопрос: «Почему
salary не является функцией-членом класса Employee?»
Возможно, автор не может изменить класс Employee, так как он определен
в стороннем коде. В таком случае лучше передать весь класс Employee
в функцию salary через константную ссылку и позволить функции самой
обращаться к классу, например:
for (auto const& emp : employees)
auto final_salary = calculate_salary(PayGrade::SeniorManager, emp);

Оба решения уменьшают количество параметров, которые принимает
функция salary, что и является целью данной рекомендации.

ДЕЛАЙТЕ ТАК МАЛО, КАК ВОЗМОЖНО,
НО НЕ МЕНЬШЕ
Означает ли это, что наборы параметров всегда должны преобразовываться
в классы?
Конечно, нет. Рекомендация лишь предлагает минимизировать количество
параметров функций. В рамках x64 ABI существует соглашение по умолчанию о быстром вызове с четырьмя регистрами. Функция с четырьмя
параметрами будет выполняться немного быстрее, чем функция, принимающая класс по ссылке. Вам решать, стоит ли заменять четыре параметра
простых типов одним параметром-классом. Конечно, если функция принимает дюжину параметров, то определенно предпочтительнее создать

108  Часть II. Не навредите себе

класс для инкапсуляции состояния.
Но это не жесткое правило, а просто
рекомендация, которую следует примерять к своему контексту.

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

Вторая часть обсуждения рекомендации посвящена нарушению правила
«одна функция — одна ответственность». Согласно этому простому
правилу, функция должна делать
что-то одно, но качественно. Примером нарушения этого принципа
может служить функция realloc .
Если вы будете действовать как добропорядочный программист на C++,
то можете никогда не столкнуться с этим зверем. Он живет в стандартной
библиотеке C, где объявлен в заголовке с такой сигнатурой:
void* realloc(void* p, size_t new_size);

Эта функция решает несколько задач. Основная — изменение объема блока
памяти, на который указывает p. Если говорить точнее, она увеличивает или
уменьшает объем блока памяти, на который указывает p, до размера new_size
в байтах. Если объем существующего блока нельзя просто увеличить, то
функция выделяет новый блок требуемого размера, а затем копирует в него
содержимое старого блока. Эта операция копирования не поддерживает
семантику конструктора копирования, а просто копирует байты, поэтому
порядочный программист на C++ вряд ли с ней столкнется.
Если в параметре new_size передать ноль, то поведение функции определится конкретной реализацией. Вы могли бы подумать, что в этом случае
она просто освободит блок, но это верно не для всех реализаций.
Функция возвращает адрес нового или расширенного блока памяти. Если
запрошен блок б льшего размера и в системе недостаточно памяти для его
размещения, то вызывающему коду возвращается пустой указатель.
Итак, функция решает две задачи: изменяет размер блока памяти и, возможно, перемещает его содержимое. Явно видно смешивание разных уровней
абстракции, и второе действие обусловлено несостоятельностью первого.
Эту функциональность логично реализовать как единственную функцию
с именем resize, например:
bool resize(void* p, size_t new_size);

I.23. Минимизируйте число параметров в функциях  109

Она могла бы просто попытаться увеличить или уменьшить размер блока,
а в случае неудачи выделить новый блок и переместить в него данные.
Отпала бы необходимость в поведении, определяемом реализацией. То есть
хорошим тоном было бы разделить уровни абстракции, связанные с перераспределением памяти и перемещением данных.
Принцип «одна функция — одна ответственность» обусловлен связностью.
В программной инженерии под связностью понимается степень взаимозависимости сущностей. Мы надеемся, что пример выше достаточно ясно
демонстрирует высокую связность, существующую внутри функции. Высокая связность — это хорошо. Она способствует улучшению удобочитаемости
и возможности повторного использования, а также снижению сложности.
Если у вас не получается подобрать хорошее имя для своей функции,
это может говорить о том, что она решает слишком много разных задач.
Для чего-то сложного всегда нелегко подобрать хорошее имя.
В случае с классами высокая связность подразумевает тесную связь функций-членов и данных. Связность увеличивается, когда функции-члены
выполняют небольшое количество связанных действий с небольшим набором данных. Высокая связность и слабая сопряженность часто идут рука
об руку. Более полную информацию по этой теме можно получить в книге
Structured Design1, изданной более 40 лет тому назад, но продолжающей
оставаться актуальной и по сей день.

ПРИМЕРЫ ИЗ РЕАЛЬНОЙ ЖИЗНИ
Если присмотреться внимательно, можно увидеть, что второй принцип
просто иначе выражает первый. Когда функция накапливает обязанности,
ее следует абстрагировать, превратить в очень конкретную запись в словаре
предметной области. Давайте рассмотрим примеры, показанные в самом
Core Guideline.
Первый — функция merge. Это чудище имеет следующую сигнатуру:
template
last1,
last2,
comp);

Yourdon E., Constantine L. Structured Design: Fundamentals of a Discipline of Computer
Program and Systems Design (2 ed.). — N. Y.: Yourdon Press, 1978 (Йордан Э. Структурное
проектирование и конструирование программ).

110  Часть II. Не навредите себе

Сигнатура выглядит более или менее читабельной, но даже притом, что она
скопирована с cppreference.com, для этой книги нам пришлось ее немного
сократить, чтобы уместить по ширине страницы, напечатать несколько раз,
внимательно вычитать и исправить несколько ошибок.
Здесь явно есть возможность абстрагирования. Функция принимает набор
итераторов, отмечающих пару диапазонов. Начиная с C++20, можно явно
определять диапазоны в виде пар итераторов, отмечающих начало и конец
диапазонов. Это позволяет упростить функцию, объединив вместе первые
четыре параметра с итераторами:
template
constexpr OutIt merge(InRng r1, InRng r2, OutIt dest, Compare comp);

Детали реализации диапазонов находятся на более низком уровне абстракции. В данном же случае нас интересует только объединение двух
диапазонов. Другой способ определить диапазон — передать указатель
на его начало и количество элементов. Во втором примере в Руководстве
приводится функция с такой сигнатурой:
void f(int* some_ints, int some_ints_length);

Еще один объект, появившийся в C++20, — std::span. Core Guidelines
сопровождается библиотекой поддержки, которая так и называется —
Guidelines Support Library. Это набор классов, определенных в пространстве имен gsl, которые можно использовать для следования некоторым
рекомендациям. Класс std::span был разработан на основе gsl::span
и реализован в точности так, как описано выше: указатель на начало последовательности данных и длина объединены в один объект, что дает
следующую сигнатуру:
void f(std::span some_ints);

В обоих примерах мы выполнили абстрагирование на основе параметров.
Существует еще один способ определения диапазона — с указателем
и контрольным значением. Этот метод встроен в язык в виде строкового литерала и выражается в виде указателя на массив символов с завершающим нулем (NUL, 0x00). Этот заключительный нулевой символ
и есть контрольное значение. Более специализированную версию объекта
std::span — std::string_view — можно создать из пары итераторов, из указателя и счетчика, а также из указателя и контрольного значения.

I.23. Минимизируйте число параметров в функциях  111

ПОДВЕДЕМ ИТОГ
Абстракции могут конструироваться множеством способов, но часто наступает момент, когда клиент хочет большего от своей функции. Мы в ответ не можем просто сказать: «Нет, я не собираюсь добавлять еще один
параметр, нарушая принцип I.23» — и сложить руки на груди, выражая
протест. Это не очень любезно по отношению к вашим коллегам, которые
одновременно являются вашими клиентами. Но что делать, если нет больше
значимых абстракций, которые можно было бы ввести?
Большое количество параметров может служить сигналом, что сложность
выходит из-под контроля и нужно что-то делать. Если параметры не поддаются дальнейшему абстрагированию, то, возможно, проблема заключается
в самом идентификаторе функции. Если к функции предъявляется слишком
много требований, то это говорит о ее важности для предметной области.
Однако вместо попытки заключить эту важность в одну абстракцию, возможно, настала пора подумать о разделении функции. Можно, например,
написать перегруженные версии для разных сценариев использования или
разделить действия на несколько функций с разными именами, соответствующими конкретным операциям. Можно также написать шаблонную
функцию. Всегда есть другие варианты, кроме добавления дополнительных
параметров.
Важно помнить, что, независимо от причины добавления дополнительных
параметров в функцию, к этому нельзя относиться легкомысленно и этот
шаг следует рассматривать как крайнюю меру и повод для переосмысления
природы самой функции. Минимизируйте количество параметров, разделяйте похожие параметры, упрощайте функции и радуйтесь обнаружению
абстракций.
zz

Большое количество параметров увеличивает нагрузку на пользователя.

zz

Организуйте параметры в структуры; возможно, это поможет обнаружить скрытые абстракции.

zz

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

ГЛАВА 2.3

I.26. Если нужен
кросс-компилируемый ABI,
используйте подмножество
в стиле C

СОЗДАВАЙТЕ БИБЛИОТЕКИ
Создавать библиотеки на C++ просто. Скомпилируйте файлы с исходным
кодом, объедините их в библиотеку, опишите экспортируемые элементы
в заголовочном файле или в модуле, если ваш компилятор поддерживает их,
и передайте файл библиотеки с заголовочным файлом или определением
модуля вашему клиенту.
К сожалению, это еще не все. Существует множество мелких деталей, которые необходимо учитывать. Цель этой рекомендации — показать, как свести
к минимуму накладные расходы, не допустить, чтобы огромный объем
работы лег на ваши плечи в какой-то непредсказуемый момент в будущем,
и реализовать таким образом возможность создавать библиотеки, которые
будут служить сообществу много лет.
Вспомним, как работает компоновщик. Он сопоставляет отсутствующие
символы из объектных файлов или библиотек с экспортированными символами из других объектных файлов или библиотек. Объявления в заголовке
ссылаются на определения в файле библиотеки.
Например, представьте, что вы написали библиотеку с функцией:
id retrieve_customer_id_from_name(std::string const& name);

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

I.26. Если нужен кросс-компилируемый ABI, используйте подмножество  113

тратить время на перекомпиляцию. Возможно, ваш исходный код является
вашей собственностью, и вы не хотите передавать его другим.
Судя по имени, эта функция извлекает буфер с символами по полученной
ссылке на строку, посылает эту строку в запросе к базе данных, находящейся
где-то в облаке, и возвращает идентификатор, извлеченный из ответа на
запрос. Реализация строки может содержать длину, за которой следует
указатель на буфер, поэтому получение буфера с символами и передача
его в базу данных является тривиальной задачей.
А теперь представьте, что ваша библиотека стала безумно популярной, возможно, потому, что в базе данных полно полезной информации и каждый
хочет получить кусочек. Число ваших клиентов сначала исчислялось сотнями, потом тысячами и, наконец, десятками тысяч, и все они с радостью
отдают вам свои деньги в обмен на разработанную вами функциональность. Внезапно начинают появляться жалобы, что библиотека дает сбой
во время выполнения именно этой функции. Струйка жалоб превращается
в поток, а затем в настоящий ревущий водопад гнева. Вы приступаете к исследованиям и замечаете, что обновился набор инструментов: изменились
компилятор, компоновщик и стандартная библиотека.
Следующим шагом вы выполняете сборку библиотеки и модульных тестов с новым набором инструментов и запускаете полное тестирование.
Все тесты выполняются успешно. Нет никакого сбоя. Вы пытаетесь воспроизвести ошибку, но безрезультатно. Так что же произошло в действительности?
Проблема заключалась в изменении определения std::string. Когда вы
только создали библиотеку, тип std::string был реализован как длина, за
которой следовал указатель. Но потом ваши клиенты перешли на использование обновленного набора инструментов и стандартной библиотеки,
реализующей тип std::string как указатель, за которым следует длина.
Теперь ваша функция получает std::string const& с иной организацией
памяти. При попытке разыменовать указатель происходит разыменование
длины и попадание в защищенную область памяти, приводящее к завершению по ошибке.
Повторная сборка библиотеки и модульных тестов выполнялась с помощью
нового набора инструментов, поэтому повсюду использовалось определение
std::string, в котором первым следует указатель, а за ним длина. Как результат, все тесты выполнились успешно. Ошибка проявлялась, только
когда новый код клиента вызывал старый код вашей библиотеки.

114  Часть II. Не навредите себе

ЧТО ТАКОЕ ABI
Произошедшее обусловлено изменением в ABI. Прикладной программный
интерфейс (API) вашей библиотеки не изменился, но изменился ABI.
Термин ABI может быть не знаком вам. Эта аббревиатура расшифровывается как Application Binary Interface — прикладной двоичный интерфейс.
Прикладной программный интерфейс (API) является руководством для
людей и определяет действия, которые можно выполнять с библиотекой,
а прикладной двоичный интерфейс (ABI) является руководством для машин и определяет особенности взаимодействия с библиотекой. ABI можно
интерпретировать как скомпилированную версию API.
ABI определяет не только особенности размещения объектов в стандартной библиотеке, но и порядок передачи их функциям. Например,
согласно System V AMD64 ABI1 первые шесть целочисленных аргументов или аргументов-указателей передаются функциям в регистрах RDI,
RSI, RDX, RCX, R8 и R9. Этому ABI следуют Unix и Unix-подобные
операционные системы. Также к ведению ABI относятся обработка и распространение исключений, формат пролога и эпилога функции, организация таблицы виртуальных методов, соглашения о вызове виртуальных
функций и т. д.
Необходимость следования требованиям ABI — одна из причин, почему код,
скомпилированный для одной операционной системы, не будет работать
в другой, даже на одной и той же аппаратной платформе, такой как x86.
У этих операционных систем могут быть разные ABI. Конечно, если бы
существовал один общий ABI, то это не имело бы значения, но ABI тесно
связаны с характеристиками оборудования. Привязка к единственному
ABI приведет к снижению производительности. Библиотека, которая не использует типы из другой библиотеки в сигнатурах своих функций, не может
пострадать от нарушения ABI, если та библиотека изменится. Со временем
все библиотеки меняются, и от этого никуда не деться.
Поэтому важно поддерживать стабильный ABI. Любое изменение функции,
типов возвращаемых значений, количества и порядка аргументов или специ­
фикации noexcept — все это нарушения ABI и изменения API. Изменение
определений типов или структур данных в приватном интерфейсе не явля­
ется изменением API, но является нарушением ABI.
1

https://wiki.osdev.org/System_V_ABI

I.26. Если нужен кросс-компилируемый ABI, используйте подмножество  115

Даже простое декорирование имен (name mangling) может нарушить
ABI, потому что ABI может определять стандартный способ уникальной
идентификации функций для поддержки связывания вместе библиотек, написанных на разных языках, например C и Pascal. Возможно, вам
уже приходилось видеть объявление extern "C" в заголовочных файлах.
Это сигнал для компилятора, что объявленные имена функций должны
добавляться в таблицу экспортируемых с применением схемы именования,
используемой компилятором C на этой платформе.
Предыдущая гипотетическая проблема с библиотекой, которая принимает
std::string, вряд ли вас побеспокоит. Современные компоновщики просто не позволят клиенту скомпоновать библиотеки с разными ABI. Для
этого может использоваться, например, встраивание версии ABI в декорированные имена символов или в объектный файл. Тогда при попытке
связать конфликтующие версии ABI компилятор может выдать сообщение
об ошибке. Трудозатраты, связанные с ABI, заключаются в выявлении таких
конфликтов и являются одной из обязанностей инженера.
Запомните правило: «Если нужен кросс-компилируемый ABI, используйте
подмножество в стиле C».
Таким образом, мы объяснили природу кросс-компилируемого ABI, обосновали важность его соблюдения. Теперь перейдем к подмножеству в стиле C.

СОКРАЩАЙТЕ ДО АБСОЛЮТНОГО МИНИМУМА
Итак, что включает подмножество в стиле C? Типы языка C часто называют
встроенными. Это элементарные типы, в определении которых не участвуют
никакие другие типы. Их можно было бы назвать атомарными, но, к сожалению, в C++ этот термин имеет другое значение. Однако вы можете
рассматривать их как основные строительные блоки всех других типов.
К ним относятся1:
void
bool
char
int
float
double
1

https://ru.cppreference.com/w/cpp/language/types

116  Часть II. Не навредите себе

Некоторые из этих типов можно модифицировать, добавляя и убирая поддержку знака или изменяя размер. Ключевые слова signed и unsigned можно
применить к типам char и int. Ключевое слово short можно применить к int.
Ключевое слово long можно применить к int и double. Более того, ключевое
слово long можно дважды применить к int. Фактически ключевое слово int
можно полностью опустить и оставить только модификаторы знака и/или
размера. Модификаторы также можно применять в любом порядке, например, тип long unsigned long полностью допустим.
На первый взгляд это может показаться несколько громоздким. Если бы код,
содержащий long unsigned long, был предоставлен на экспертную оценку,
велика вероятность, что у экспертов возникли бы вопросы касательно обос­
нованности его использования. Наличие модификатора размера порождает
каверзный вопрос: «Насколько велико число типа int?» Правильный ответ
на него: «Зависит от реализации». Стандарт же, со своей стороны, определяет некоторые гарантии размеров, уменьшая неопределенность:
zz short int
zz long int

и int имеют размер не менее 16 бит;

имеет размер не менее 32 бит;

zz long long int

имеет размер не менее 64 бит;

zz 1 == sizeof(char) REQUIRES
Когда появился C++98, я1 первым делом посмотрел, как контейнеры взаи­
модействуют с алгоритмами. В то время я работал в игровой компании
в Лондоне и пытался убедить некоторых инженеров, что C++ — это то, что
нужно (возможно, я несколько опережал события). Я продемонстрировал им
очень короткие функции, способные выполнять поиск и сортировку коллекций. Однако в ту пору тип std::vector был проблемным. Всякий раз, когда
происходило превышение его текущей емкости и запускалась про­цедура
изменения размера, его содержимое копировалось поэлементно в новое
место с помощью цикла for, а затем старые версии удалялись. Конечно,
такое решение было обусловлено требованием стандарта: новые значения
должны создаваться на новом месте вызовом конструктора копирования,
а прежние — уничтожаться вызовом их деструкторов. Однако в большинстве ситуаций вполне подошла бы очевидная оптимизация, заключавшаяся
в использовании memcpy вместо поэлементного копирования.
memcpy — это функция C, которую никогда не следует использовать напря-

мую, так как она просто копирует содержимое памяти в указанное место без
вызова конструктора копирования. Это означает, что, несмотря на сохранность данных, любая дополнительная инициализация, скажем регистрация
объектов в службе уведомлений, и действия, связанные с уничтожением,
например отмена регистрации в службе уведомлений, не будут выполнены.
memcpy — это деталь реализации, не предназначенная для повседневного
использования.
Мои коллеги послушали меня, развернулись и ушли. Я был удручен и решил
написать класс векторов, использующий memcpy вместо конструктора копирования и деструктора, как это должно было быть. Я назвал его mem_vector. Этот
класс предназначался для использования только с типами, не имеющими
конструкторов или деструкторов, и он получился на славу. Все были в восторге от него и нашли ему хорошее применение в сочетании с алгоритмами.
Я был на седьмом небе примерно три дня, пока кто-то все не испортил, добавив поддержку конструктора. Однако mem_vector не был предназначен для
этого, и вскоре стало ясно, что он не может использоваться в этой ситуации,
пока код не будет доработан, а это означало возврат на старый путь.
В чем я действительно нуждался, так это в способе выбора между использованием memcpy и копированием с последующим уничтожением каждого
1

Экскурс в историю с Гаем Дэвидсоном. Рассмотрим еще один популярный прием.

Т.120. Используйте метапрограммирование, только когда это необходимо  153

элемента в отдельности. Для воплощения идеи пришлось приложить некоторые усилия, потому что понадобилось реализовать перегруженные
функции-члены, которые выбирались бы для семейств типов, а не каких-то
конкретных. Я хотел иметь возможность объявить (напомню, что это было
еще до появления C++11):
template
void mem_vector::resize(size_type count);

вместе с:
template
void mem_vector::resize(size_type count);

и пусть бы компилятор сам выбирал реализацию mem_vector, соответствующую характеру параметра шаблона, а не конкретному его типу. Однажды
я услышал о SFINAE (Substitution Failure Is Not An Error — ошибка подстановки не является ошибкой), и внезапно меня осенило: я могу встроить
пустую структуру с именем trivial во все соответствующие типы. Если бы
это удалось, я мог бы объявить вторую функцию resize так:
template
void mem_vector::resize(size_type count, T::trivial* = 0);

Если бы структура-член trivial не существовала, то функция не рассматривалась бы. Но это решение просто перемещает проблему в другое
место. По-прежнему не было никакого способа принудительно удалить
структуру-член trivial, если класс переставал быть тривиальным. Эта проблема была особенно характерна для случаев структур, наследующих от
других тривиальных структур, когда родительская структура становится
нетривиальной. Я впал в отчаяние.
Но все оказалось не так плохо: мы вместе смогли кое-что придумать, используя встроенные функции, характерные для компилятора, а затем появился C++11. Внезапно проблема была решена благодаря удивительной
мощи std::enable_if и заголовку type_traits. О чудный день!
Однако была одна маленькая проблема. Код был почти нечитаемым. Такие
типы, как:
std::enable_if::type

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

154  Часть II. Не навредите себе

Проблема с этим приемом TMP заключается в том, что он запутывает код
и не позволяет быстро понять происходящее. Честно говоря, для меня это
больше похоже на шум. Да, накопив опыт, мы научились читать и понимать такой код, но новым программистам приходилось проходить долгий
период обучения.
Однако сложность этого приема не помешала широкому его внедрению.
Поискав по старым репозиториям на GitHub, можно быстро найти фрагменты кода с тщательно продуманными перегрузками функций, отличающимися предложениями std::enable_if с различной степенью детализации.
Комитет не остался глух к этой проблеме, и в C++17 была добавлена новая
возможность — оператор if constexpr.
Он стал избавлением от нескольких классов проблем, которые исторически
заставляли использовать enable_if. С его помощью можно оценить выражение во время компиляции и выбрать подходящее исполнение. Например:
if constexpr(sizeof(int) == 4)
{
// тип int имеет размер 32 бита
}

В частности, его можно использовать там, где прежде нужно было использовать enable_if:
if constexpr(std::is_move_constructible_v)
{
// шаблон функции, специализированный для перемещаемого типа
}

Вместо перегрузки шаблонов функций для разных типов теперь можно
выразить разницу в одной функции. Например, следующую пару перегруженных функций:
template
void do_stuff()
{
...
}
template ::type>
void do_stuff()
{
...
}

Т.120. Используйте метапрограммирование, только когда это необходимо  155

можно заменить на:
template
void do_stuff()
{
if constexpr(std::is_move_constructible_v)
{
...
}
else
{
...
}
}

Однако комитет на этом не остановился. После продолжительного периода обсуждения в язык были добавлены понятия, которые принесли
с собой ограничения и предложения requires. Это значительно упростило
определение ограничений на типы для специализации. Вот как выглядит
предложение requires:
template
requires std::is_move_constructible_v
void do_stuff()
{
...
}

Эта функция доступна только для типов, имеющих конструктор перемещения. Концептуально этот подход идентичен std::enable_if, но намного
проще и понятнее. Он дает возможность избежать TMP и четко обозначить
намерения.
Но и это еще не все. Одна из исследовательских групп комитета — SG7 —
занимается вопросами интроспекции, которая в значительной степени
осуществляется с применением TMP. Цель этой группы — добавить в язык
средства, созданные с использованием TMP, и устранить наконец необходимость в TMP как таковом. Интроспекция — важная часть метапрограммирования, и исследовательская группа пытается соединить множество
идей, чтобы выработать согласованную стратегию и решение проблем
метапрограммирования. Разрабатываются дополнительные возможности,
основанные на интроспекции, одна из которых — метаклассы. Они должны
позволить программистам определять не только подставляемые типы, но
и форму классов, чтобы дать клиентам метакласса возможность создавать
экземпляры, не заботясь о стандартном коде.

156  Часть II. Не навредите себе

Все эти возможности, как предполагается, повысят ясность кода: вместо
добавления дополнительных понятий, требующих изучения и освоения,
они позволят упростить большую часть существующего кода и устранят
ненужные угловые скобки.
Мы надеемся, что Руководство убедило вас в сложности TMP, объяснило,
почему его следует использовать только в крайнем случае. Конечно, иногда
метапрограммирование возможно, но только при вдумчивом и осторожном
использовании шаблонов.
Существует афоризм, который каждый инженер должен держать близко
к сердцу: «Умный код — это просто. Простой код — это умно».
Лучшим считается код, взглянув на который читатель скажет: «Здесь нет
ничего особенного. Все очевидно». Однако в адрес метапрограммирования
шаблонов такое можно услышать чрезвычайно редко.

ПОДВЕДЕМ ИТОГ
zz

Метапрограммирование в C++ моделируется с помощью шаблонов
функций и классов.

zz

Анализ, проводимый комитетом по C++, предлагает интроспекцию.

zz

Наиболее полезные приемы метапрограммирования были явно перенесены в язык.

zz

Эта тенденция продолжается и направлена на устранение необходимости в метапрограммировании шаблонов.

III
ПРЕКРАТИТЕ ЭТО
ИСПОЛЬЗОВАТЬ

Глава 3.1 I.11. Никогда не передавайте владение через простой
указатель (T*) или ссылку (T&).
Глава 3.2 I.3. Избегайте синглтонов.
Глава 3.3 C.90. Полагайтесь на конструкторы и операторы
присваивания вместо memset и memcpy.
Глава 3.4 ES.50. Не приводите переменные с квалификатором const
к неконстантному типу.
Глава 3.5 E.28. При обработке ошибок избегайте глобальных состояний
(например, errno).
Глава 3.6 SF.7. Не используйте using namespace в глобальной области
видимости в заголовочном файле.

ГЛАВА 3.1

I.11. Никогда
не передавайте
владение через
простой указатель (T*)
или ссылку (T&)

ИСПОЛЬЗОВАНИЕ ОБЛАСТИ
СВОБОДНОЙ ПАМЯТИ
Владение — важное обстоятельство. За ним стоит ответственность, под
которой в C++ подразумевается уборка и освобождение ресурсов за собой. Если вы что-то создали, то по завершении всех процессов должны
это уничтожить и освободить память. Для объектов со статическим и автоматическим классами хранения это совершенно тривиальный вопрос,
но для объектов, динамически размещаемых в области свободной памяти,
это настоящее минное поле.
Память, выделенную из свободной области, легко потерять. Ее можно вернуть только с помощью указателя, назначенного этой выделенной памяти,
который является единственным доступным дескриптором. Если указатель
покинет область видимости, не будучископированным, то освободить
память не удастся. Такое явление известно как утечка памяти. Например:
size_t make_a_wish(int id, std::string owner) {
Wish* wish = new Wish(wishes[id], owner);
return wish->size();
}

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

I.11. Не передавайте владение через простой указатель или ссылку  159

функцию и вернуть из нее указатель, чтобы вызывающая сторона могла
взять на себя ответственность и удалить объект позже, освободив память.
Wish* make_a_wish_better(int id, std::string owner) {
Wish* wish = new Wish(wishes[id], owner);
return wish;
}

Это правильно оформленный код, хотя мы бы не назвали его современным
в идиоматическом смысле. К сожалению, он провоцирует возникновение
определенных сложностей: вызывающий обязан стать владельцем объекта
и гарантировать его уничтожение с помощью оператора delete по окончании использования. Также существует опасность удалить объект до того,
как он действительно перестанет быть нужным. Если make_a_wish получит
указатель от другого объекта, то как сообщить этому другому объекту, что
указатель больше не нужен?
Исторически функции такого типа приводили к истощению области
свободной памяти из-за размещения объектов-зомби, право собственности на которые не было четко обозначено и которые никогда не удалялись. Передачу права собственности можно оформить несколькими
способами. Автор функции, назвав ее allocate_a_wish, может тем самым
подсказать клиенту, что для результата была выделена память и теперь
вся ответственность за освобождение этой памяти возлагается на того,
кто функцию вызывал.
Это довольно слабый способ обозначения передачи права владения.
Он не является обязательным к исполнению, и многое зависит от того,
помнит ли клиент об ответственности и действует ли надлежащим образом. Также требуется, чтобы автор внедрил реализацию в интерфейс.
А это уже просто плохая привычка, потому что неявно раскрывает детали
реализации клиенту и не позволяет вам изменить их без риска привнесения
заблуждений.
Подобное именование может показаться слабым решением, но в действительности оно не настолько слабо, как, скажем, упоминание о необходимости освобождать память, размещенное в документации на сервере в каком-то
удаленном, темном, потаенном месте. И не настолько слабо, как комментарии
в заголовочном файле, которые никто никогда не читает. Но даже притом,
что это меньшее зло, такой подход все же ненадежен.
Еще хуже возврат значения через ссылку вместо указателя. Как вызыва­
ющий код узнает, что объект был уничтожен? Имея такое значение, он
может лишь надеяться, что другой поток не уничтожит его между делом,

160  Часть III. Прекратите это использовать

и может, конечно, убедиться, что объект используется, прежде чем вызывать другую функцию, которая способна инициировать его уничтожение.
Такой подход требует учитывать слишком много контекста при разработке.
Работающие с особенно старым кодом могут увидеть экземпляры std::auto_
ptr. Это первая попытка решить данную проблему, которая в итоге была
стандартизирована в C++98. Тип std::auto_ptr содержит сам указатель
и обеспечивает семантику перегруженного указателя, действуя как дескриптор объекта. Экземпляры std::auto_ptr можно передавать, освобождая от
права собственности при копировании. Выходя из области видимости при
сохранении за ними прав собственности, они удаляют хранимые в памяти
объекты. Однако необычная семантика копирования означает, что объекты std::auto_ptr нельзя без опаски хранить в стандартных контейнерах.
Этот класс считался устаревшим уже во втором стандарте (C++11) и был
удален в третьем (C++14).
Однако комитет ничего не удаляет из языка без замены, а внедрение семантики перемещения позволило создать объекты, владеющие указателями, для которых хранение в контейнерах не было проблемой. Поскольку
std::auto_ptr был объявлен устаревшим в C++11, ему на смену были введены типы std::unique_ptr и std::shared_ptr. Они известны как «умные» или
«интеллектуальные» указатели и полностью решают проблему владения.
Получив объект std::unique_ptr, код становится владельцем того, на что
указывает этот объект. При выходе из области видимости он удаляет хранимый объект. Однако, в отличие от std::auto_ptr, std::unique_ptr не имеет
флага, определяющего право собственности, поэтому его можно безопасно
хранить в стандартных контейнерах. Причина, почему ему не нужен флаг,
заключается в том, что его нельзя скопировать, его можно только переместить, поэтому нет никакого недопонимания в отношении того, кто в данный
момент владеет содержащимся в нем объектом.
Получив объект std::shared_ptr, вы начинаете интересоваться тем, на
что он указывает. Когда он выходит из области видимости, этот интерес
пропадает. Когда объектом больше никто не интересуется, он удаляется.
Право собственности распределяется между всеми имеющими отношение
к хранимому объекту. Объект не будет уничтожен, пока остается хотя бы
один объект, интересующийся им.
По умолчанию для хранения динамических объектов следует использовать
std::unique_ptr, а тип std::shared_ptr должен использоваться, только если
рассуждения о времени жизни и владении невероятно запутаны. Но даже

I.11. Не передавайте владение через простой указатель или ссылку  161

в этом случае применение типа std::shared_ptr следует рассматривать как
признак технической недоработки, обусловленной несоблюдением соответствующей абстракции. Одним из примеров может служить программа
просмотра Twitter, которая распределяет твиты по разным столбцам. Твиты
могут содержать изображения, которые делают их большими и заставляют
размещаться в нескольких разных столбцах. Твит должен продолжать существовать в памяти программы, только когда он виден в одном из столбцов, но
пользователь может в какой-то момент решить, что твит больше не нужен,
прокрутив все столбцы так, что он исчезнет отовсюду. Можно сохранить
контейнер твитов и использовать счетчики, подсчитывая ссылки на твиты
вручную, но это решение просто дублирует абстракцию std::shared_ptr на
другом уровне косвенности. В частности, вспомним, что решение о сроке
жизни твита принимает пользователь, а не программа. Такая ситуация
должна быть редкостью.

ПРОИЗВОДИТЕЛЬНОСТЬ
ИНТЕЛЛЕКТУАЛЬНЫХ УКАЗАТЕЛЕЙ
Иногда использование интеллектуальных указателей может быть нежелательно. Копирование std::shared_ptr не обходится без затрат.
Тип std::shared_ptr должен быть потокобезопасным, что приводит к дополнительным затратам, отрицательно сказывается на производительности.
Потокобезопасным является только блок управления std::shared_ptr,
но не сам ресурс. Он может быть реализован как пара указателей, один
из которых указывает на хранимый объект, а другой — на мехаПо умолчанию для хранения динизм подсчета. Копирование обнамических объектов следует исходится недорого с точки зрения
пользовать std::unique_ptr, а тип
std::shared_ptr должен использопередачи памяти, но механизм
ваться, только если рассуждения
подсчета должен будет получить
о времени жизни и владении
мьютекс и увеличить счетчик
невероятно запутаны. Но даже
ссылок при его копировании. Ко­
в этом случае применение типа
гда std::shared_ptr выйдет из обstd::shared_ptr следует рассмаласти видимости, механизму подтривать как признак технической
держки снова придется получить
недоработки, обусловленной немьютекс и уменьшить счетчик
соблюдением соответствующей
ссылок, а при обнулении счетчиабстракции.
ка — уничтожить объект.

162  Часть III. Прекратите это использовать

Тип std::unique_ptr проще и дешевле. Его можно только перемещать, но
не копировать, поэтому может существовать только один его экземпляр.
Соответственно, когда экземпляр std::unique_ptr покидает область видимости, он должен удалить хранимый объект. Никакого подсчета не требуется. И все же на хранение указателя на функцию, которая удалит объект,
расходуется дополнительная память. Тип std::shared_ptr тоже содержит
такой объект как часть подсчета.
Об этих накладных расходах можно не беспокоиться, пока они не превратятся в горячую точку в отчетах профилировщика. Безопасность
интеллектуального указателя — очень ценное обретение. Однако иногда,
обнаружив, что применение интеллектуальных указателей отражается на
производительности, и посмотрев, куда они передаются, можно заметить,
что совместное использование или передача прав собственности вообще
не нужны. Например:
size_t measure_widget(std::shared_ptr w) {
return w->size(); // (предполагается, что w != null)
}

Этой функции не требуется владеть указателем. Она просто вызывает
другую функцию и возвращает полученное значение. Следующая функция
будет работать так же хорошо:
size_t measure_widget(Widget* w) {
return w->size(); // (предполагается, что w != null)
}

Обратите внимание, что произошло с w или, скорее, чего не произошло.
Указатель не передавался другой функции, не использовался для инициа­
лизации другого объекта, и его время жизни никак не было продлено.
Если бы функция выглядела так:
size_t measure_widget(Widget* w) {
return size(w); // (Вы правильно подумали...)
}

тогда другое дело. Вы не владеете w, поэтому не можете им распоряжаться.
Функция size может создать копию w и кэшировать ее для последующего
использования. Соответственно, если вы не уверены в реализации этой
функции и не отвечаете за возможное ее изменение в будущем, такая передача w может оказаться небезопасной. Если объект, на который указывает w,

I.11. Не передавайте владение через простой указатель или ссылку  163

будет уничтожен позже, то копия w будет указывать на несуществующий
объект и ее разыменование может привести к катастрофе.
Эта функция принимает объект по указателю и передает его другой функции. Такая последовательность действий подразумевает владение, которое
не передается в сигнатуре функции. Не передавайте владение через простые указатели.
Правильный способ реализации функции:
size_t measure_widget(std::shared_ptr w) {
return size(w);
}

Теперь вы даете функции size() возможность заявить о своей заинтересованности в std::shared_ptr. Если вызывающая функция впоследствии уничтожит w, то копия, созданная функцией size(), останется действительной.

ИСПОЛЬЗОВАНИЕ ПРОСТОЙ
СЕМАНТИКИ ССЫЛОК
Простой указатель не единственный способ передачи объектов не по
значению. Для этого также можно использовать ссылки. Использование
ссылок — предпочтительный механизм передачи по ссылке. Взгляните на
следующую версию measure_widget:
size_t measure_widget(Widget& w) {
return w.size(); // (Ссылки не могут быть пустыми,
// разве что по злому умыслу)
}

Это решение лучше предыдущих, потому что перекладывает бремя проверки
существования объекта на вызывающий код. Он должен разыменовать объект и заплатить штраф за разыменование пустого указателя. Однако, если
w передается дальше, возникает та же проблема с владением, что и раньше.
Если ссылка хранится как часть другого объекта, а объект ссылки уничтожается, то ссылка перестает быть действительной.
Сигнатура функции должна сообщать вызывающей стороне все, что та
должна знать о владении. Если сигнатура включает T*, то вызывающая

164  Часть III. Прекратите это использовать

сторона может передать указатель на объект или пустой указатель и не
беспокоиться о его времени жизни. Вызывающий код просто передает
объект по ссылке в функцию, а затем продолжает работу. Если сигнатура
включает T&, то вызывающая сторона может передать ссылку на объект
и не беспокоиться о его времени жизни. Здесь применимы все те же рассуждения.
Если сигнатура включает std::unique_ptr, это означает, что вызывающая сторона должна отказаться от владения объектом. Если сигнатура
включает std::shared_ptr, это говорит о том, что вызывающая сторона
должна допускать владение объектом совместно с вызываемой функцией
и не может быть уверена в том, когда объект будет уничтожен.
Отклонившись от этих правил, вы рискуете внести в свой код трудноразличимые и болезненные ошибки, которые повлекут утомительные споры
о праве собственности и ответственности. В конечном итоге объекты будут
или уничтожаться слишком рано, или не уничтожаться вообще. Не передавайте право собственности с использованием простых указателей или
ссылок. Если ваша функция принимает указатель или ссылку, не передавайте ее конструктору или другой функции, не осознавая при этом, какая
ответственность за это ложится на вас.

GSL::OWNER
Мы рассмотрели передачу и возврат значений по простому указателю
и ссылке и отметили, что это не самая лучшая идея. Пользователи могут
сделать неверный вывод о праве владения объектом. Они могут захотеть
обрести право собственности, когда это не предусмотрено. Решение данной
проблемы — использовать интеллектуальные указатели.
К сожалению, работая с устаревшим кодом, вы не всегда можете сильно его
изменить. Он может быть частью зависимости другого устаревшего кода,
который полагается на ABI. Замена простых указателей интеллектуальными
изменит представление объектов в памяти, нарушив ABI.
Настал момент представить вашему вниманию библиотеку поддержки рекомендаций (Guidelines Support Library, GSL). Это небольшая библио­тека
средств поддержки от Core Guidelines. В самом Руководстве много рекомендаций, которые очень трудно соблюсти. Ярким примером таких трудностей
является использование простых указателей: как сообщить о праве владения

I.11. Не передавайте владение через простой указатель или ссылку  165

в отсутствие возможности использовать интеллектуальные указатели? GSL
предоставляет типы, помогающие выполнить эту рекомендацию.
GSL состоит из пяти частей:
zz

GSL.view — типы в этой части позволяют различать владеющие и невладеющие указатели, а также указатели на единственный объект
и указатели на первый элемент последовательности;

zz

GSL.owner — указатели с правом владения, включая std::unique_ptr
и std::shared_ptr, а также stack_array (массив, размещенный в стеке)
и dyn_array (массив, размещенный в куче);

zz

GSL.assert — предвосхищая предложение о контрактах, предоставляет
два макроса: Expects и Ensures;

zz

GSL.util — ни одна библиотека не обходится без набора разных полезных вещей, и они здесь;

zz

GSL.concept — коллекция предикатов типов.

GSL появилась до выхода стандарта C++17, и некоторые части GSL,
в частности раздел concept, были заменены стандартом C++. Библиотека
доступна на GitHub по адресу https://github.com/Microsoft/GSL. Просто добавьте
директиву #include , чтобы получить в свое распоряжение полный
набор объектов.
Этот раздел в большей степени посвящен одному из типов представлений, gsl::owner. Рассмотрим пример:
#include
gsl::owner produce()
{
gsl::owner i = new int;
return i;
}

// Превращает вас в счастливого владельца
// Вы — владелец
// Передача владения из функции

void consume(gsl::owner i) // Прием права владения
{
delete i;
// Теперь это ваше, вы можете уничтожить его
}
void p_and_c()
{
auto i = produce();
consume(i);
}

// создать...
// ...и уничтожить

166  Часть III. Прекратите это использовать

Как видите, заключение указателя в owner сигнализирует о праве собственности. Давайте немного изменим ситуацию:
int* produce()
{
gsl::owner i = new int;
return i;
}

// Простой указатель

Что произойдет в этом случае?
Не следует ждать, что компилятор предупредит вас о преобразовании объекта с информацией о владении в простой указатель. К сожалению, это
не тот случай. Взгляните, как определен тип owner:
template
using owner = T;

Как видите, здесь нет никакой магии. Тип gsl::owner определяется просто:
если T является указателем, то gsl::owner становится псевдонимом T,
иначе определение аннулируется.
Цель этого типа не в том, чтобы явно передать право владения, а в том,
чтобы намекнуть пользователю о смене владельца. Вместо внедрения этой
информации в имя функции она встраивается в тип. Вполне возможно
создать тип с именем owner, который выполняет все необходимые действия
для правильного отслеживания и передачи права владения, но в этом нет
необходимости: с такой задачей прекрасно справляются std::shared_ptr
и std::unique_ptr. Тип gsl::owner — это просто синтаксический сахар, который можно добавить в существующий код, не оказывая влияния на него,
ABI или особенности выполнения, но влияя на удобочитаемость и простоту
восприятия, а также на эффективность работы статических анализаторов
и обзоров кода.
По мере роста популярности библиотеки GSL можно ожидать, что IDE
будут распознавать ее типы и предупреждать о злоупотреблениях правами
владения с помощью визуальных сигналов в редакторе, таких как подчеркивание красной линией или всплывающие подсказки в виде лампочки.
Но до тех пор тип gsl::owner следует использовать не как средство принудительной передачи права владения, а как описательный тип. В конце концов,
относитесь к gsl::owner как к последнему средству, когда нет возможности
использовать абстракции владения более высокого уровня.

I.11. Не передавайте владение через простой указатель или ссылку  167

ПОДВЕДЕМ ИТОГ
zz

Владеть чем-то означает нести ответственность за что-то.

zz

В C++ имеются интеллектуальные указатели, однозначно определяющие принадлежность и владение.

zz

Для обозначения владения используйте интеллектуальные указатели
или gsl::owner.

zz

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

ГЛАВА 3.2

I.3. Избегайте синглтонов

ГЛОБАЛЬНЫЕ ОБЪЕКТЫ — ЭТО ПЛОХО
«Глобальные объекты — это плохо. Понятно?» Вы будете постоянно слышать эту фразу и от начинающих, и от опытных программистов. Но давайте
разберемся, почему это плохо.
Глобальный объект находится в глобальном пространстве имен. Существует только одно такое пространство, отсюда и название «глобальное».
Глобальное пространство имен — это самая внешняя декларативная область
единицы трансляции. Имена в глобальном пространстве имен называются глобальными именами. Любой объект с глобальным именем является
глобальным объектом.
Такой объект не всегда видим во всех единицах трансляции программы.
Правило единственного определения означает, что он может быть определен только в одной единице трансляции, но объявление может повторяться
в любом их количестве.
Глобальные объекты не имеют ограничений доступа. Если они видимы, то
вы можете взаимодействовать с ними. У глобальных объектов нет иного
владельца, кроме самой программы, то есть за них не отвечает ни один
другой объект. Глобальные объекты имеют статический класс хранения,
поэтому они инициализируются при запуске (на этапе статической инициа­
лизации) и уничтожаются при завершении работы (на этапе статической
деинициализации).
Это порождает проблемы. Владение имеет фундаментальное значение для
рассуждений об объектах. Поскольку у глобального объекта нет владельца,
то как можно рассуждать о его состоянии в любой конкретный момент
времени? Вы можете вызывать некоторые функции этого объекта, а затем

I.3. Избегайте синглтонов  169

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

ШАБЛОН ПРОЕКТИРОВАНИЯ «СИНГЛТОН»
Убедившись во вреде, который наносят глобальные объекты нашему коду,
обратим внимание на синглтоны. Впервые члены сообщества C++ столк­
нулись с этим термином в 1994 году, когда вышла книга Design Patterns1.
Она была чрезвычайно захватывающим чтением в то время и остается
очень полезной до сих пор. Каждый разработчик должен иметь ее на
своей книжной полке или в электронной библиотеке. В ней описываются
шаблоны проектирования, повторяющиеся в программной инженерии
почти так же, как шаблоны в традиционной архитектуре, такие как купол, портик или галерея. Самое замечательное в этой книге то, что в ней
определены общие шаблоны программирования и даны имена. Выбрать
хорошее имя — непростая задача, и то, что кто-то взял на себя труд сделать
это, стало большим благом.
В книге шаблоны делятся на три основные категории: порождающие, структурные и поведенческие. Именно в категории порождающих шаблонов
находится шаблон «Синглтон» (Singleton), ограничивающий возможность
создания объектов класса единственным экземпляром. Конечно, описание
шаблона в такой потрясающей книге подразумевало, что его использование — это хорошо и правильно. В конце концов, мы все использовали
синглтоны в течение многих лет, просто не давали им имя, которое было бы
принято всеми.
Популярным примером синглтона является главное окно приложения.
В главном окне происходят все действия, прием вводимой пользователем
1

Gamma E., Helm R., Johnson R., Vlissides J. Design Patterns. — Reading, MA: AddisonWesley, 1994 (Гамма Э., Влиссидес Дж., Хелм Р., Джонсон Р. Приемы объектно-ориентированного проектирования. Паттерны проектирования. — Питер, 2016).

170  Часть III. Прекратите это использовать

информации и отображение результатов работы. В программе может существовать только одно главное окно, поэтому имеет смысл предотвратить
создание другого такого окна. Другой пример синглтона — класс менеджера.
Подобные классы характеризуются включением в идентификатор слова
manager. Это явный признак, что на самом деле создан синглтон и имеются
проблемы с определением прав владения тем, чем он управляет.

ФИАСКО ПОРЯДКА СТАТИЧЕСКОЙ
ИНИЦИАЛИЗАЦИИ
Синглтоны подвержены проблеме фиаско порядка статической инициализации1. Этот термин был введен Маршаллом Клайном (Marshall Cline)
в его сборнике вопросов и ответов по C++ и характеризует проблему
создания зависимых объектов не по порядку. Рассмотрим два глобальных объекта, A и B, где конструктор B использует некоторые функции,
предоставляемые объектом A, поэтому A должен быть создан первым.
Во время компоновки редактор связей идентифицирует набор объектов со
статическим классом хранения, выделяет область памяти для них и создает список конструкторов, которые должны быть вызваны до вызова main.
Вызов этих конструкторов во время выполнения называется статической
инициализацией.
Можно определить, что B зависит от A, и поэтому A должен быть создан
первым, но нет стандартного способа сообщить компоновщику об этом.
Можно ли что-то предпринять? В таком случае нужно найти способ обозначить зависимость в единице трансляции. Но компилятор знает только
о той единице трансляции, которую он компилирует.
Мы уже видим, как вы хмурите брови: «А если я скажу компоновщику,
в каком порядке их создавать? Можно ли изменить компоновщик, чтобы
он соответствовал этой потребности?» На самом деле такая попытка уже
была предпринята. Давным-давно используется IDE под названием Code
Warrior от компании Metrowerks. Версия, которой пользовался я (Гай
1

Возможно, слово «фиаско» является несправедливой характеристикой. Статическая
инициализация никогда и не должна была поддерживать топологический порядок
инициализации. Это невозможно при раздельной компиляции, инкрементном связывании и с компоновщиками 1980-х годов. C++ пришлось смириться с существующими
операционными системами. Это было время, когда системные программисты еще
только учились пользоваться острыми инструментами.

I.3. Избегайте синглтонов  171

Дэвидсон. — Примеч. ред.), предлагала свойство, позволявшее программисту
диктовать порядок создания статических объектов. И все было хорошо,
пока я случайно не создал малозаметную циклическую зависимость, на
трассировку которой уходило почти 20 часов.
Вы можете возразить: «Циклические зависимости — неизбежный спутник
разработки. Факт их получения из-за неправильного определения отношений не должен исключать возможности диктовать порядок создания
объектов на этапе статической инициализации». Все верно, та проблема
была решена, и я продолжил работу. Но не забывайте, что, если понадобится перенести код на другой набор инструментов, не поддерживавший
такой возможности, код потеряет работоспособность. Программист может
дорого заплатить, если попытается, используя такие конструкции, сделать
свой код переносимым.
«Тем не менее, — можете продолжить вы, — эту возможность комитет
мог бы стандартизировать. Спецификации компоновки уже включены
в стандарт. Почему бы не добавить возможность определения порядка инициализации?» Что ж, признаемся: есть еще одна проблема со статическим
порядком инициализации. Она заключается в том, что ничто не мешает
вам запустить несколько потоков выполнения во время статической инициализации и обратиться к объекту до его создания. А уж при этом точно
очень легко выстрелить себе в ногу из-за зависимостей между глобальными
статическими объектами.
Комитет не имеет привычки стандартизировать потенциальные мины
замедленного действия. Зависимость от порядка инициализации чревата опасностями, как было показано в предыдущих абзацах, и позволять
программистам управлять этой возможностью как минимум неразумно.
Кроме того, такая стратегия противоречит самой концепции модульной
организации. Статический порядок инициализации задается для каждой
единицы трансляции в порядке объявления. Задание порядка несколькими
единицами трансляции сразу — вот где все рушится. Определяя зависимые
объекты в одной единице трансляции, вы избегаете всех этих проблем, сохраняя при этом ясность цели и разделение задач.
Слово «компоновщик» (linker) встречается в стандарте только один раз1.
Компоновщики не уникальны для C++; они связывают любой объектный код, который имеет соответствующий формат, независимо от того,
какой компилятор его сгенерировал, будь то C, C++, Pascal или другие
1

https://eel.is/c++draft/lex.name

172  Часть III. Прекратите это использовать

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

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

Теперь, после всего сказанного, рассмотрим способ обойти фиаско порядка
статической инициализации. А выход в том, чтобы вывести объекты из
глобальной области видимости и тем самым дать возможность запланировать их инициализацию. Самое простое решение — создать функцию,
содержащую статический объект требуемого типа, который возвращается
функцией по ссылке. Его иногда называют синглтоном Мейерса в честь
Скотта Мейерса (Scott Meyers), который описал этот подход в своей книге
Effective C++1.
Например:
Manager& manager() {
static Manager m;
return m;
}

Теперь глобальной является функция, а не объект. Объект Manager не будет
создан до вызова функции: на статические данные в области видимости
функции распространяются другие правила инициализации. «Но, — спросите вы, — а как же ситуация конкурентного выполнения? Ведь проблема доступа к объекту из нескольких потоков до его создания никуда не исчезла?»
К счастью, начиная с C++11, это решение стало также потокобезопасным.
Если заглянуть в раздел [stmt.dcl]2 стандарта, то можно увидеть следующее:
«Если поток управления входит в объявление конкурентно, пока инициализация переменной еще не завершилась, то этот поток будет приостановлен
до завершения инициализации».
Однако на этом проблемы не заканчиваются: по-прежнему сохраняется
риск одновременного обращения к единственному изменяемому объекту
без гарантии потокобезопасного доступа к нему.
1

Meyers S. Effective C++. — Reading, MA: Addison-Wesley, 1998 (Мейерс С. Эффективный
и современный С++. 42 рекомендации по использованию C++11 и C++14).

2

https://eel.is/c++draft/stmt.dcl

I.3. Избегайте синглтонов  173

КАК СКРЫТЬ СИНГЛТОН
Взглянув на предложенный выше способ, вы можете решить, что мы просто спрятали синглтон за функцией. Действительно, скрыть синглтон
несложно, но в Core Guidelines отмечается, что заставить не использовать
его в целом очень трудно. Первая идея выявления синглтонов, предлагаемая рекомендацией «I.3. Избегайте синглтонов», гласит: «Ищите классы
с именами, включающими слово singleton». Этот совет может показаться
вполне действенным, но можно нарваться на другие синглтоны: поскольку
синглтон является одним из шаблонов проектирования, инженеры довольно
часто добавляют слово singleton в имена своих классов, чтобы показать:
«Я считаю, что это синглтон» или «Я прочитал книгу Design Patterns».
Конечно, при этом реализация встраи­вается в интерфейс, что само по себе
очень плохо, но это уже совсем другая история.
Вторая идея, предлагаемая Руководством: «Искать классы, для которых
создается только один объект (путем подсчета объектов или изучения
конструкторов)». Для этого требуется полный ручной аудит кода по классам. Иногда синглтоны создаются случайно. Можно ввести абстракцию
и сформировать из нее класс, а также создать все средства, необходимые
для управления жизненным циклом и взаимодействиями с этим классом, такие как специальные функции, общедоступный интерфейс и т. д.
Но в конечном счете окажется, что только один экземпляр объекта
может существовать в каждый конкретный момент времени. Возможно,
в намерения инженера не входило создание синглтона, но именно это
и произошло. Подсчет экземпляров показывает, что их количество равно
единице.
Последняя идея из Руководства, касающаяся обсуждаемого вопроса: «Если
класс X имеет общедоступную статическую функцию, содержащую статическую локальную переменную типа класса X и возвращающую указатель
или ссылку на нее, запретите это». Это тот самый метод решения проблемы
фиаско порядка статической инициализации, который был описан выше.
Класс может иметь надмножество следующего интерфейса:
class Manager
{
public:
static Manager& instance();
private:
Manager();
};

174  Часть III. Прекратите это использовать

Демаскирующим признаком здесь является приватный конструктор.
Объект этого класса может создать только статический член или дружественный класс, но здесь нет объявления дружественных классов. От этого
класса нельзя создать производный класс, если не добавить в него другой
общедоступный конструктор. Приватный конструктор прямо говорит:
«Создание моих экземпляров жестко контролируется другими функциями
в моем интерфейсе». И — о чудо! В общедоступном интерфейсе имеется
статическая функция, которая возвращает ссылку на экземпляр. Вы, без
сомнения, догадаетесь, что именно содержит эта функция-член, взглянув
на пример функции manager(), приведенный выше.
Вариация этого шаблона — синглтон с подсчетом ссылок. Рассмотрим класс,
являющийся жадным пожирателем ресурсов. Из-за этой его особенности
желательно не только разрешить существование его единственного экземпляра, но и гарантировать немедленное уничтожение этого экземпляра,
как только он станет ненужным. Организовать такое поведение довольно
сложно, потому что требуются общий (разделяемый) указатель, мьютекс
и счетчик ссылок. Однако вспомните, что это все тот же синглтон, подпадающий под правило «Избегайте синглтонов».
Возможно, сейчас вы смотрите на эту общедоступную статическую
функцию-член и говорите себе: «Определенно, в Руководстве должно быть
сказано: “Избегайте объектов со статическим классом хранения”. В конце
концов, это тоже синглтоны». Запомните эту мысль.

ТОЛЬКО ОДИН ИЗ НИХ ДОЛЖЕН
СУЩЕСТВОВАТЬ В КАЖДЫЙ МОМЕНТ
РАБОТЫ КОДА
При обучении программированию на C++ приводится несколько популярных примеров, описывающих объектно-ориентированные свойства
языка. На заправочных станциях, например, есть автомобили, насосы,
касса, цистерны для топлива, цены и т. д., это все в совокупности составляет
достаточно богатую экосистему для описания многих видов отношений.
Точно так же в ресторанах есть столы, клиенты, меню, окно выдачи блюд,
официанты, повара, доставщики еды, уборщики и др. (В современных учебниках, вероятно, в качестве подобных моделей могут также упоминаться
сайт или аккаунт в Twitter.)

I.3. Избегайте синглтонов  175

Оба приведенных примера имеют одну общую черту: это абстракция чего-то,
существующего в единственном экземпляре. На АЗС имеется одна касса.
В ресторане имеется одно окно выдачи блюд. Это точно синглтоны? Если
нет, то как тогда быть с созданием объекта?
Одним из возможных разрешений противоречия, которое мы наблюдали,
является создание класса с полностью статическим интерфейсом. Все общедоступные функции-члены и приватные данные являются статическими.
Теперь немного отвлечемся и расскажем вам об Уильяме Хите Робинсоне
(W. Heath Robinson). Этот английский художник-карикатурист, родившийся в 1872 году в Финсбери-парк в Лондоне, особенно известен своими
рисунками нелепо сложных машин, в которых применяется множество
ухищрений для решения простых задач. Одна из автоматических аналитических машин, построенных для Блетчли-парк во время Второй мировой
войны для помощи в расшифровке немецких сообщений, была названа «Хит
Робинсон» в его честь. У него был американский коллега, Руб Голдберг
(Rube Goldberg), родившийся в июле 1883 года в Сан-Франциско, который
тоже рисовал чересчур сложные устройства и изобрел настольную игру
«Мышеловка». Имена этих художников вошли в обиход как синонимы
чрезмерной инженерной усложненности.
Примером такой чрезмерной сложности является класс с полностью статическим интерфейсом. Определяя класс, вы создаете общедоступный
интерфейс для наблюдения за абстракцией и управления ею, а также
множество данных и приватных функций для моделирования поведения
абстракции. Однако если данные существуют только в одном экземпляре,
то зачем привязывать их к классу? Можно просто реализовать все общедоступные функции-члены в одном исходном файле и поместить данные
с приватными функциями в анонимное пространство имен.
Нет, правда, зачем вообще возиться с классом?
Вот оно! Нам пришлось пройти длинный и извилистый путь к этому правильному решению проблемы синглтонов (с маленькой буквы «с»). Они
должны быть реализованы как пространства имен, а не классы. Вместо:
class Manager
{
public:
static int blimp_count();
static void add_more_blimps(int);
static void destroy_blimp(int);

176  Часть III. Прекратите это использовать
private:
static std::vector blimps;
static void deploy_blimp();
};

вы должны объявить:
namespace Manager
{
int blimp_count();
void add_more_blimps(int);
void destroy_blimp(int);
}

Реализация не должна быть видна клиенту как какой-нибудь рисунок Хита
Робинсона, изумительный и очаровательный в своей сложности. Ее можно
спрятать в темных тайниках файла реализации. Такой подход дает дополнительные преимущества: высокую стабильность файла, в котором объявлено
пространство имен, и низкую вероятность крупномасштабной зависимой
перекомпиляции. Конечно, данные, используемые для моделирования
абстракции, не будут принадлежать объекту, а значит, они будут статическими. Поэтому остерегайтесь получить фиаско порядка статической
инициализации, как описано выше.

ПОДОЖДИТЕ МИНУТКУ...
Возможно, рассматривая это решение на основе пространства имен, вы
замечаете про себя: «Но это все еще “Синглтон”».
Нет, это не «Синглтон». Это синглтон. Проблема, о которой предупреждает
Руководство, связана с шаблоном проектирования «Синглтон» (Singleton),
а не с абстракциями существования чего-то в единственном экземпляре.
На самом деле в интервью издательству InformIT в 2009 году Эрих Гамма
(Erich Gamma), один из четырех авторов Design Patterns, заметил, что у него
есть желание удалить шаблон «Синглтон» (Singleton) из каталога1.
Рекомендации, касающиеся языка C++, имеют две проблемы. Первая:
данный ранее умный совет не обязательно останется таким же разумным
советом с течением времени.
1

https://www.informit.com/articles/article.aspx?p=1404056

I.3. Избегайте синглтонов  177

На данный момент каждые три года выходит новая редакция стандарта
C++. Так, появление std::unique_ptr и std::shared_ptr в 2011 году изменило ранее звучавший совет о соблюдении парности вызовов new и delete
(«Удаляйте объект только в том модуле, в котором он был создан»).
Оно сделало возможным отказ от низкоуровневых операций new и delete,
как рекомендуется в «R.11. Избегайте явных вызовов new и delete ».
Не всегда бывает достаточно выучить комплекс советов, чтобы затем идти
по жизни: по мере развития и изменения языка советы должны постоянно
пересматриваться.
Непосредственным проявлением этой проблемы может быть использование
привычного фреймворка, предполагающего широкое применение некогда
идиоматичных, но ныне устаревших приемов программирования на C++.
Возможно, он включает «Синглтон» для захвата и управления переменными среды или настройками, передаваемыми через параметры командной
строки, которые могут измениться. Вы можете думать, что ваш любимый
фреймворк не ошибется, но это не тот случай. Подобно тому как научное
мнение меняется с появлением новой информации, меняются и передовые
приемы программирования на C++. Эта книга, которую вы читаете сегодня,
может содержать несколько вечных советов, но мы, авторы, считаем, что
было бы в высшей степени высокомерно и глупо предполагать, что весь ее
текст — это вековечная мудрость с заповедями, высеченными в граните,
о том, как следует писать на C++.
Вторая проблема заключается в том, что рекомендации часто являются
квинтэссенцией нескольких причин, часто полностью скрытых за выразительной и запоминающейся фразой, закрепляющейся в нашем сознании.
Нужно изучить эти причины или хотя бы ознакомиться с ними. «Избегайте синглтонов» гораздо легче запомнить, чем «избегайте чрезмерной
разработки абстракций с одним экземпляром в классе и злоупотребления
уровнями доступа для предотвращения создания множественных экземпляров». Выучить совет недостаточно. Нужно изучить подоплеку, чтобы
знать и понимать, почему предлагается использовать тот или иной подход
и когда можно без опаски этого не делать.
Core Guidelines — это живой документ в репозитории GitHub, куда вы
можете направлять запросы на извлечение (pull request). В нем содержатся сотни советов, обусловленных различными причинами, и цель этой
книги — выделить первопричины возникновения 30 наиболее ценных
из них.

178  Часть III. Прекратите это использовать

Выше мы отмечали, что вы можете подумать, будто все статические объекты
Данный ранее умный совет
не обязательно останется
являются «Синглтонами» и поэтому
таким же разумным с течеследует избегать любых статических
нием времени.
объектов. Теперь вы должны понимать,
что статические объекты не являются
«Синглтонами» и не обязательно являются синглтонами. Они являются
экземпляром объекта, продолжительность существования которого совпадает с продолжительностью выполнения программы. И при этом они могут
не быть глобальными: область видимости статических переменных-членов
ограничена не глобальной областью видимости, а лишь класса.
Точно так же утверждение «Глобальные объекты — это плохо. Понятно?»
не всегда верно. Вам может навредить именно изменяемое глобальное состояние, как описывается в рекомендации «I.2. Избегайте неконстантных
глобальных переменных». Если глобальный объект неизменяемый, то он
является всего лишь свойством программы. Например, разрабатывая физический симулятор для космической игры, мы не без оснований могли бы
объявить в глобальном пространстве имен объект типа float с именем G,
представляющий гравитационную постоянную:
constexpr float G = 6.674e-11; // Гравитационная постоянная

Ведь это универсальная константа, и никто не должен ее менять. Конечно, вы
можете решить, что глобальное пространство имен не подходит для таких
вещей, и объявить для этих целей пространство имен universe:
namespace universe {
constexpr float G = 6.674e-11; // Гравитационная постоянная
}

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

I.3. Избегайте синглтонов  179

ПОДВЕДЕМ ИТОГ
zz

Избегайте синглтонов: шаблона, а не абстракции с одним экземпляром.

zz

Для моделирования абстракции этого типа вместо класса лучше использовать пространство имен.

zz

С осторожностью используйте статические данные при реализации
синглтона.

zz

Изучайте причины и подоплеку появления рекомендаций в Core
Guidelines.

zz

Пересматривайте советы в Core Guidelines по мере развития языка C++.

ГЛАВА 3.3

C.90. Полагайтесь
на конструкторы
и операторы присваивания
вместо memset и memcpy

В ПОГОНЕ ЗА МАКСИМАЛЬНОЙ
ПРОИЗВОДИТЕЛЬНОСТЬЮ
C++ славится своей производительностью, сопоставимой с производительностью «голого железа». Другие языки приходили и уходили, пытаясь
оспорить у C++ титул и корону высокопроизводительного языка, но он
по-прежнему остается популярным языком для абстракций с нулевыми
накладными расходами. C++ унаследовал эту черту от языка C, предлагающего несколько очень эффективных библиотечных функций. Некоторые
из них могут быть реализованы как однопроцессорные инструкции.
Рассмотрим для примера функцию double floor(double arg). Она находится
в заголовке и возвращает наибольшее целое значение, не превышающее arg. Процессоры x86 имеют единственную инструкцию, способную
сделать это, она называется ROUNDSD. Интеллектуальный оптимизирующий
компилятор может заменить вызов floor этой инструкцией и при этом наполнить восторгом душу типичного инженера, жаждущего скорости.
Для этого процессора CISC доступно несколько удивительных инструкций.
Если вдруг вам понадобится узнать количество ведущих нулей в значении, чтобы определить ближайшую степень числа 2, для этой цели воспользуйтесь инструкцией LZCNT. Или, например, вам потребуется узнать
количество установленных битов в значении при вычислении расстояния
Хэмминга (Hamming). В этом вам поможет POPCNT. Это настолько полезная

C.90.Полагайтесь на конструкторы и операторы присваивания  181

инструкция, что Clang и GCC заметят вашу попытку написать ее и заменят
ваш код вызовом POPCNT. Отличный сервис. Не забудьте оставить чаевые
разработчику вашего компилятора.
Когда я впервые начал программировать1, то быстро перешел с BASIC на
язык ассемблера, сначала Z80, затем 68000. Начав изучать C, я смог обращаться с ним как с языком программирования макросов, что и сделало
мой переход на C довольно гладким. Я рассуждал о своем коде так, будто
он написан на языке ассемблера, только на C его было проще писать, тестировать и отлаживать. Я писал отличный код гораздо быстрее, чем когда
использовал ассемблер 68000.
Начиная переходить на C++, я с подозрительностью относился к некоторым его аспектам. Но непродолжительный анализ, опыт и наблюдения
обычно развеивали мои подозрения. Например, виртуальные функции
выглядели для меня как черная магия, пока я не понял, что это всего
лишь указатели на функции, располагающиеся в верхней части моей
структуры, хотя они и создавали дополнительный уровень косвенности
вдали от ожидаемого места. Перегрузки и шаблоны функций помогли
мне избавиться от множества символов и познакомили с возможностью
исключения реализации из интерфейса, что позволило писать гораздо
более читабельный код.
Но больше всего мне понравился в этом языке синтаксический сахар,
позволяющий писать более ясный код. Вещи, замедляющие мою работу,
я отбросил: определенно они не нужны мне в путешествии.
Но вот конструкторы... Они оказались худшими из худших.

УЖАСНЫЕ НАКЛАДНЫЕ РАСХОДЫ
КОНСТРУКТОРОВ
К моменту, когда я освоил ассемблер, я научился выделять область памяти
для работы и заполнять ее нулями одной ассемблерной инструкцией. Если
я чувствовал себя более уверенно, то даже не обнулял память, а просто
инициализировал ее значениями в соответствии с контекстом, хотя это
усложняло отладку, потому что было трудно выяснять, какие ячейки я уже
инициализировал, а какие — еще нет.
1

Экскурс в историю с Гаем Дэвидсоном. Языковые экзерсисы.

182  Часть III. Прекратите это использовать

Перейдя на C, я быстро научился объявлять целые числа, числа с плавающей
точкой и структуры в начале функции, а в отладочных сборках вызывать
библиотечную функцию memset, объявленную в , для заполнения
памяти нулями одним вызовом. Я просто увеличивал (или уменьшал)
указатель стека и заполнял пространство нулями.
После перехода на C++ я был вынужден отучаться от этой привычки и привыкать к конструкторам по умолчанию. Мне пришлось усвоить, что эти
конструкторы вызываются несмотря ни на что и нет никакой возможности
предотвратить эти вызовы. Я был вынужден смотреть на ассемблерный код
и вздрагивать. Ничто не могло сравниться в скорости со «старым добрым
способом». Лучшее решение, которое я смог придумать, — вызвать memset
в теле конструктора. Списки инициализации просто не могли обнулить все
одной ассемблерной инструкцией.
Можете представить, как я относился к операторам присваивания и конструкторам копирования. Почему они не вызывали memcpy? Почему не было
выбрано более деликатное, изящное и утонченное поэлементное действо?
Я мог бы понять такую избирательность, когда действительно нужно что-то
сделать в теле конструктора, но зачем такие большие накладные расходы,
если я просто выделял область памяти?
Я боролся с языком, проклиная эту неэффективность, получаемую в обмен
на более понятный код. Чтобы написать код, для которого производительность была критически важной, иногда я переходил на C, используя тот
факт, что оба языка были понятны компоновщику.
Химера — мифический огнедышащий зверь с головой льва, телом козла
и хвостом дракона. Я писал эти жуткие химеры в 1990-х. Мне потребовалось много времени, чтобы понять, что главная моя ошибка заключается
в слишком раннем объявлении объектов в функциях. Кроме того, это было
до стандартизации и введения правила «как если бы». Еще больше времени мне потребовалось, чтобы осознать истинную его ценность. Жизненно
важно рассмотреть подробнее правила конструирования объектов.
Стандарт описывает инициализацию классов на 12 страницах, начиная
с [class.init]1 и ссылаясь еще на восемь страниц в [dcl.init]2 для случаев,
когда нет конструктора. Мы не будем разбирать все эти хитросплетения,
а поступим проще и сразу обобщим, начав с агрегатов.
1

https://eel.is/c++draft/class.init

2

https://eel.is/c++draft/dcl.init

C.90. Полагайтесь на конструкторы и операторы присваивания  183

САМЫЙ ПРОСТОЙ КЛАСС
Агрегаты — это классы:
zz

без объявленных пользователем или унаследованных конструкторов;

zz

без необщедоступных нестатических переменных-членов;

zz

без виртуальных функций;

zz

без необщедоступных или виртуальных базовых классов.

Например:
struct Agg {
int a;
int b;
int c;
};

Агрегаты — хорошая штука. Их можно инициализировать, представив
значения в фигурных скобках:
Agg t = {1, 2, 3};

Согласно правилам инициализации, каждый элемент инициализируется
копией соответствующего элемента. В примере выше это выглядит так:
t.a={1};
t.b={2};
t.c={3};

Если нет явно инициализированных элементов, каждый элемент инициализируется инициализатором по умолчанию или копией, инициализированной от пустого инициализатора в порядке объявления. Это становится
невозможным, если один из членов является ссылкой, потому что ссылки
должны связываться при создании экземпляра. Например:
auto t = Agg{};

При таком объявлении t сначала будет инициализирован член t.a с по­
мощью {}, затем t.b и, наконец, t.c. Однако, поскольку все они имеют
тип int, инициализация превратится в пустую операцию: для встроенных типов нет конструктора. «Ах! — можете воскликнуть вы. Именно
здесь я могу вызвать memset, это же очевидно. Содержимое структуры
не определено, и это плохо, поэтому я могу просто обнулить ее. Это будет
совершенно правильно».

184  Часть III. Прекратите это использовать

Нет. Правильно было бы добавить конструктор, выполняющий эту инициализацию, например, так:
struct Agg {
Agg() : a{0}, b{0}, c{0} {};
int a;
int b;
int c;
};

«Но теперь это уже не агрегат, — верно подметите вы, — а мне так нужна
эта возможность инициализации фигурными скобками с вызовом memset,
ну пожалуйста».
Что ж, тогда вы можете использовать инициализаторы членов, например,
вот так:
struct Agg {
int a = 0;
int b = 0;
int c = 0;
};

Если теперь объявить:
auto t = Agg{};

t.a будет инициализирован операцией = 0, так же как t.b и t.c. Более того,
можно использовать назначенные инициализаторы (designated initializers),
появившиеся в C++20, которые позволяют инициализировать члены объекта разными значениями, например:
auto t = Agg{.c = 21};

Теперь t.a и t.b по-прежнему будут инициализироваться 0, а t.c получит
значение 21.
«Да, назначенные инициализаторы хороши, и мы снова получили агрегат»
(мы так и чувствуем, как в вашем сознании формируется «но»), «но члены
по-прежнему инициализируются по одному! Я хочу использовать memset
для их инициализации одной инструкцией».
В действительности это плохая идея, потому что инициализация объекта
отделяется от его определения. Что получится, если в агрегат будут добавлены новые элементы? Ваш вызов memset обнулит только часть агрегата. C++ позволяет заключить весь жизненный цикл объекта в единую

C.90. Полагайтесь на конструкторы и операторы присваивания  185

абстракцию — класс, что весьма ценно. Не нужно пытаться идти против
течения.
Возможная ваша реплика: «Я буду использовать sizeof, чтобы гарантировать независимость от любых изменений в классе».
И все равно это плохая идея. Что, если вы добавите элемент, который по
умолчанию инициализируется ненулевым значением? В таком случае вам
придется гарантировать, что вызов memset учитывает значение этого члена,
возможно, путем разделения его на два. Этот несчастный случай затаится
и будет ждать момента, чтобы произойти.
«Неубедительно! Я владею агрегатом, он определен в частном файле
реализации, а не в заголовке, он не будет изменяться без моего ведома,
вызов memset совершенно безопасен! Что за дела? Почему мне нельзя вызвать memset?»
В том-то и дело, что на самом деле вызывать memset не нужно. Давайте поговорим об абстрактной машине.

О ЧЕМ ГОВОРИТ СТАНДАРТ
«P.2. Придерживайтесь стандарта ISO C++» — одно из первых основных
правил в Core Guidelines. Стандарт определяет поведение соответствующей ему реализации C++. Любое отклонение считается нестандартным.
Существует множество реализаций C++ для разных платформ, все они
действуют по-разному, в зависимости, например, от размера машинного
слова и других особенностей, характерных для конкретной цели. Некоторые платформы не имеют энергонезависимого хранилища в виде дисков.
Другие не имеют устройства стандартного ввода. Как стандарт учитывает
все эти вариации?
Первые три раздела стандарта: «Область применения»1, «Нормативные
ссылки»2 и «Термины и определения»3, а также страницы с 10-й по 12-ю
четвертого раздела «Общие принципы»4 точно объясняют эту проблему.
1

https://eel.is/c++draft/intro.scope

2

https://eel.is/c++draft/intro.refs

3

https://eel.is/c++draft/intro.defs

4

https://eel.is/c++draft/intro

186  Часть III. Прекратите это использовать

Вот вам одно из доказательств важности принципа RTFM (Read The Front
Matter — «чтение вступительных разделов»).
Первые четыре раздела, составляющие вводную часть, описывают структуру документа, принятые соглашения, значение термина «неопределенное
поведение», понятие «плохо сформированная программа» и фактически
определяют всю систему отсчета. В разделе «Общие принципы», в частности, в секции [intro.abstract]1, вы найдете следующий текст: «Семантические описания в этом документе определяют параметризованную
недетерминированную абстрактную машину. Этот документ не предъявляет никаких требований к структуре соответствующих реализаций.
В частности, они не обязаны копировать или эмулировать структуру
абстрактной машины. Скорее, соответствующие реализации должны
подражать (и только) наблюдаемому поведению абстрактной машины,
как описано ниже».
К этому абзацу прилагается сноска, в которой говорится: «Это положение
иногда называют правилом “как если бы”, потому что реализация может
игнорировать любое требование этого документа, пока результат получается таким же, как если бы требование было соблюдено, насколько это
можно определить по наблюдаемому поведению программы. Например,
фактическая реализация не обязана оценивать выражение, если может
сделать вывод, что его значение не используется и что при его вычислении
не возникают побочные эффекты, влияющие на наблюдаемое поведение
программы».
Это изумительная оговорка. Согласно ей, реализация должна лишь эмулировать наблюдаемое поведение. Это означает, что она может просмотреть
ваш код, оценить результат его выполнения и сделать все необходимое для
соответствия этому результату. Вот как работает оптимизация: берется
результат и составляется оптимальный набор инструкций, необходимый
для его достижения.
Что это означает для нашего примера агрегатного класса?
Поскольку все инициализаторы членов равны нулю, компилятор заметит,
что при создании экземпляра Agg требуется обнулить три целочисленных
члена. А так как такая инициализация идентична вызову memset, он, вполне
вероятно, вызовет memset. Вызывать memset вручную не потребуется.
1

https://eel.is/c++draft/intro.abstract

C.90. Полагайтесь на конструкторы и операторы присваивания  187

Но постойте-ка! Класс содержит всего три целых числа. На типичной
64-битной платформе с 32-битными целыми числами это означает, что
обнулить нужно только 12 байт. На платформе x64 это можно сделать
двумя инструкциями. С чего бы вдруг вы захотели вызвать memset? Чтобы
убедиться в нашей правоте, посетите веб-сайт Compiler Explorer и попробуйте скомпилировать такой код:
struct Agg {
int a = 0;
int b = 0;
int c = 0;
};
void fn(Agg&);
int main() {
auto t = Agg{}; //
fn(t);
//
}




Вызов функции в строке  не позволит компилятору оптимизировать
объект t, отбросив его.
Компилятор gcc для платформы x86-64 с флагом оптимизации -O3 дает
следующий код:
main:

sub
mov
mov
mov
call
xor
add
ret

rsp, 24
rdi, rsp
QWORD PTR [rsp], 0
DWORD PTR [rsp+8], 0
fn(Agg&)
eax, eax
rsp, 24

//



//



Как видите, обнуление трех целочисленных членов выполняется двумя
инструкциями mov. Автор компилятора знает, что это самый быстрый способ обнуления трех целых чисел. Если бы понадобилось обнулить гораздо
больше элементов, в игру вступили бы инструкции MMX. Вся прелесть
веб-сайта Compiler Explorer в том, что вы легко сможете проверить этот
результат сами.
Мы надеемся, что этот пример убедил вас отказаться от использования memset.

188  Часть III. Прекратите это использовать

А КАК ЖЕ MEMCPY?
По аналогии с применением memset в программах на C для обнуления
структуры логично использовать memcpy для присваивания ее другому
экземпляру. Присваивание в C++ очень похоже на инициализацию: по
умолчанию эта операция копирует данные поэлементно в порядке объявления, используя операции присваивания, соответствующие типам
членов. Вы можете написать и свой оператор присваивания, в отличие от
конструктора не начинающийся с неявного поэлементного копирования.
Можно подумать, что в этой ситуации аргументы в пользу вызова memcpy
стали сильнее. Но на самом деле по тем же причинам, что были описаны
выше, это по-прежнему не является ни хорошей идеей, ни необходимостью. Вернемся на сайт Compiler Explorer и внесем небольшое изменение
в исходный код:
struct Agg {
int a = 0;
int b = 0;
int c = 0;
};
void fn(Agg&);
int main() {
auto t = Agg{};
fn(t);
auto u = Agg{};
fn(u);
t = u;
fn(t);
}

//
//
//
//
//
//








На этот раз компилятор сгенерировал следующий код:
main:

sub
mov
mov
mov
call
lea
mov
mov
call

rsp, 40
rdi, rsp
QWORD PTR [rsp], 0
DWORD PTR [rsp+8], 0
fn(Agg&)
rdi, [rsp+16]
QWORD PTR [rsp+16], 0
DWORD PTR [rsp+24], 0
fn(Agg&)

//



//



//



//



C.90. Полагайтесь на конструкторы и операторы присваивания  189
mov
mov
mov
mov
mov
call
xor
add
ret

rax, QWORD PTR [rsp+16]
rdi, rsp
QWORD PTR [rsp], rax
eax, DWORD PTR [rsp+24]
DWORD PTR [rsp+8], eax
fn(Agg&)
eax, eax
rsp, 40

//
//
//





//



Как видите, компилятор использовал тот же трюк QWORD/DWORD и сгенерировал четыре инструкции, напрямую копирующие содержимое памяти из
исходного объекта. И снова: зачем вам вызывать memcpy?
Обратите внимание, что если отключить оптимизацию, то сгенерированный
код будет вести себя в более точном соответствии со стандартом и в меньшей
степени будет следовать правилу «как если бы». Этот код генерируется
быстрее и в общем случае проще. Коль скоро вы подумываете о возможности использования memset и memcpy, то мы предполагаем, что вас как раз
больше заботит оптимизация. В таком случае вам будет достаточно только
сгенерировать более оптимизированный код.
В приведенном выше ассемблерном коде можно заметить неожиданное
изменение порядка. Автор компилятора знает все о характеристиках выполнения этих инструкций и соответствующим образом переупорядочил
код, потому что от него требуется только эмулировать наблюдаемое поведение.

НИКОГДА НЕ ПОЗВОЛЯЙТЕ СЕБЕ
НЕДООЦЕНИВАТЬ КОМПИЛЯТОР
Чтобы получить максимальную отдачу от компилятора, нужно сообщить
ему, чего именно вы добиваетесь на самом высоком доступном уровне
абстракции. Как мы видели, для memset и memcpy доступны более высокие
уровни абстракции: конструирование и присваивание.
В качестве последнего примера рассмотрим std::fill. Вместо заполнения
блока памяти одним значением или копирования объекта, состоящего из
нескольких слов, в один блок памяти инструкция std::fill решает задачу
дублирования объекта из нескольких слов в блок памяти.

190  Часть III. Прекратите это использовать

Простейшей ее реализацией был бы простой цикл и итеративное конструи­
рование на месте или присваивание существующему объекту:
#include
struct Agg {
int a = 0;
int b = 0;
int c = 0;
};
std::array a;
void fn(Agg&);
int main() {
auto t = Agg{};
fn(t);
for (int i = 0; i < 1000; ++i) { // Заполнить массив
a[i] = t;
}
}

Однако все это может сделать std::fill, вам придется читать меньше кода,
и вы с меньшей вероятностью допустите ошибку, как это произошло выше.
(Заметили? Сравните размер массива и количество итераций в цикле for.)
int main() {
auto t = Agg{};
fn(t);
std::fill(std::begin(a), std::end(a), t); // Заполнить массив
}

Разработчики компиляторов прилагают все силы, чтобы сгенерировать
наилучший код. Типичная реализация std::fill будет включать в себя
механизм SFINAE (или, что более вероятно в настоящее время, ограничение предложением requires), чтобы использовать простую функцию
memcpy для тривиально конструируемых и тривиально копируемых типов,
где безопасно можно использовать memset
и вызов конструктора не требуется.
Чтобы получить максимальную отдачу от компиЦель этой рекомендации не только в том,
лятора, нужно сообщить
чтобы отговорить вас от использования
ему, чего именно вы доmemset и memcpy , а также и убедить исбиваетесь на самом выпользовать возможности языка, чтобы
соком доступном уровне
дать компилятору всю информацию и тот
абстракции.
мог сгенерировать оптимальный код.

C.90. Полагайтесь на конструкторы и операторы присваивания  191

Не заставляйте компилятор гадать. Он спрашивает вас: «Что вы хотите,
чтобы я сделал?» — и действительно постарается сделать все возможное,
получив от вас правильный и максимально полный ответ.

ПОДВЕДЕМ ИТОГ
zz

Используйте конструкторы и присваивание, а не memset и memcpy.

zz

Используйте максимально высокий из доступных уровень абстракции для связи с компилятором.

zz

Помогите компилятору сделать для вас все возможное.

ГЛАВА 3.4

ES.50. Не приводите
переменные
с квалификатором const
к неконстантному типу

Когда меня1 спрашивают о моих любимых особенностях C++, помимо детерминированного уничтожения, среди прочих я называю квалификатор const.
Он позволяет разделить интерфейс между представлением и управлением
и дать пользователям возможность изучить способы работы с ним. Это разделение не раз выручало меня, но никогда так сильно, как в моем первом
крупном игровом проекте.
На рубеже веков мои работодатели затеяли грандиозный проект — компьютерную игру под названием Rome: Total War. Это была стратегия реального
времени, действия в которой происходили в эпоху Римской империи. В игре
были представлены сражения с сотнями солдат, действующих в составе
кавалерийских, пехотных и артиллерийских частей, и предоставлялись
широкие возможности управления для перемещения войск по полю боя.
Игрок мог объединять отдельные части в формирования, создавать группы
из отдельных солдат, отправлять их в бой против вражеских сил, контролируемых хитрым ИИ, и наблюдать, как разворачивается драма в прекрасно
оформленном трехмерном мире.
Работы по созданию игры оказалось довольно много, даже больше, чем
мы ожидали. Одной из причин этого было желание сделать именно многопользовательскую игру. Мы хотели дать возможность сражаться не только
с искусственным интеллектом компьютера, но также с другими игроками1

Экскурс в историю. Говорит автор Дж. Гай Дэвидсон, и говорит на протяжении целой
главы.

ES.50. Не приводите переменные с квалификатором const  193

людьми. Это породило множество проблем, не последней из которых была
поддержка одинакового состояния мира на машинах разных игроков.
Подобная проблема характерна для всех многопользовательских игр. Если
вы играете в автогонки, то важно, чтобы на компьютерах всех игроков все
автомобили располагались одинаково и двигались синхронно. Если складывается такая ситуация, что на разных игровых компьютерах первыми
пересечь финишную черту могут разные автомобили, то такая игра никуда
не годится. Одно из возможных решений — назначить одну игровую станцию авторитетным сервером игрового мира, гарантировать регулярную
отправку всеми клиентскими компьютерами обновлений их экземпляров
игры на сервер, заставить сервер преобразовывать эти обновления в новое
состояние мира и рассылать новое состояние клиентам.
В случае с автогонками это будет означать отправку на клиентские машины, скажем, 20 новых положений и ускорений автомобилей. Нагрузка
небольшая: нужны только компоненты x и y (если на гоночной трассе нет
мостов), то есть по четыре 4-байтных компонента с плавающей точкой для
20 автомобилей, всего получается 320 байт на обновление мира.
В то время частота обновления 30 кадров в секунду считалась вполне приемлемой, но при этом для каждого кадра требовалось обновлять не весь мир,
а только его представление. Модель мира включает координаты и ускорения каждого автомобиля, поэтому с помощью уравнений Ньютона можно
точно оценить, где будет находиться каждый из них. Нужно лишь, чтобы
обновления мира выполнялись быстрее, чем может распознать человеческий
мозг. Десяти герц для этого вполне достаточно, то есть каждую секунду
сервер должен отправлять 3200 байт каждому клиенту. Обеспечить такую
скорость в последнее десятилетие 1990-х не было проблемой. У всех были
модемы на 56 Кбит/с, так что требуемая пропускная способность 26 Кбит/с
выглядела приемлемой.

РАБОТА С БОЛЬШИМ КОЛИЧЕСТВОМ ДАННЫХ
К несчастью, такой подход был неосуществим в нашей игре. Автомобиль
выполняет одно действие — движется. Солдаты в игре выполняют самые
разные действия. Ходят, бегают трусцой, бегают в полную силу, метают
копья, размахивают мечами и буксируют осадные машины к городским
стенам — это лишь небольшой перечень действий, которые они могут выполнять, причем каждый в отдельности со своей уникальной анимацией.

194  Часть III. Прекратите это использовать

Это означало, что каждый солдат характеризовался шестью компонентами,
а не четырьмя, потому что требовалось включить не только выполняемые
ими действия, но и как далеко они продвинулись в этих действиях. Получилось по 18 байт на солдата.
Хуже того, солдат было больше 20. На самом деле, чтобы придать осмысленность игре, нужно было разместить 1000 солдат. Получалась удручающая
арифметика:
10 Герц *
1,000 солдат *
18 байт *
8 бит =
1,440,000 бит в секунду

В начале этого века такая пропускная способность была невозможной.
В домохозяйствах только начали появляться сети ADSL, и пропускная
способность исходящего канала выше 1 Мбит/с была большой редкостью.
Оставалось единственное решение — отправлять каждому клиенту список команд для применения к солдатам и гарантировать их выполнение
каждым клиентом. Поскольку команды отдавались только отрядам из
нескольких солдат раз в несколько секунд, передавать такие обновления
было проще. Конечно, при этом требовалось, чтобы каждый клиент поддерживал идентичную копию игрового мира, известную как синхронное
состояние.
Задача была чрезвычайно сложной. Чтобы добиться максимальной определенности, все объекты в игре должны были инициализироваться одинаково. Если в структуру добавлялся новый элемент данных, его нужно
было инициализировать строго определенным значением. И даже случайные числа должны были быть определенными. Мы ничего не должны
были упустить из виду. Нам пришлось решать очень необычные задачи,
например изменять режимы арифметики с плавающей точкой в графических драйверах, что приводило к разным результатам одних и тех же
вычислений на разных машинах. Но самая большая проблема заключалась
в предотвращении рассинхронизации между представлением и моделью
мира.
Каждый клиент имеет свое окно на мир. Предполагалось, что механизм
визуализации будет смотреть на мир и вызывать константные функциичлены каждого объекта, чтобы получить необходимую информацию.
Использование константных функций давало уверенность, что механизм визуализации не будет мешать механизму моделирования мира.

ES.50. Не приводите переменные с квалификатором const  195

К сожалению, константные функции работают только до определенного
предела. Взгляните на следующий класс:
class unit {
public:
animation* current_animation() const;
private:
animation* m_animation;
};

Механизм визуализации может удерживать объект unit const*, вызвать
функцию current_animation() и, если получен объект animation, внести
в него изменения. Объект unit (представляющий отряд солдат) не обязательно должен иметь свою анимацию: иногда все солдаты в отряде будут
иметь общую анимацию, иногда у каждого может быть своя анимация,
например, когда солдат поражается копьем во время марша в строю. Константная функция-член возвращает константный объект указателя по
значению, а не константный объект animation по указателю.

БРАНДМАУЭР CONST
Эта проблема имеет несколько решений, например, можно реализовать
пару функций, в которой константная функция возвращает animation
const*, а неконстантная — animation*, но дело в том, что злоупотребление
квалификатором const может незаметно приводить к катастрофическим
последствиям. Одно маленькое изменение может вдруг просочиться
в остальной мир и оставаться незамеченным, пока не станет слишком
поздно, чтобы исправить его. Это как эффект бабочки, только более ярко
выраженный.
Интерфейс const, или, как мы его называли, брандмауэр const, был в высшей степени важен. В коде этот квалификатор нес большую уникальную
нагрузку: подсказывал, какие функции являются частью представления,
а какие — частью контроллера. Злоупотребление брандмауэром const
оказало большую медвежью услугу остальным членам команды. По мере
продвижения проекта вперед требовалось все больше и больше времени
для выяснения причин рассинхронизации мира.
Как вы понимаете, появление const_cast в любом месте в коде сродни тревожному звоночку. Налицо был соблазн разрешить различные причудливые

196  Часть III. Прекратите это использовать

взаимодействия между объектами с помощью const_cast, и программистов
приходилось удерживать от этого, постоянно напоминая об ужасной судьбе,
ожидающей их. Объявление чего-то незыблемым, а затем его изменение —
это самый худший обман ваших клиентов.
Например, представьте такую функцию:
float distance_from_primary_marker(soldier const& s);

Все что угодно должно иметь возможность безопасно вызывать эту функцию для каждого солдата, не опасаясь вмешаться в модель мира. Согласно
объявлению, эта функция вообще не меняет состояние солдата. А теперь
вообразите, что где-то в теле функции имеется следующий код:
float distance_from_primary_marker(soldier const& s) {
...
const_cast(s).snap_to_meter_grid(); // О боже...
...
}

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

РЕАЛИЗАЦИЯ ДВОЙНОГО ИНТЕРФЕЙСА
Повсюду оказался разбросан код с двойным интерфейсом, причем один из
интерфейсов был константным, а другой — неконстантным. Функции-члены
дублировались уже с квалификатором const, например:
class soldier {
public:
commander& get_commander();
commander const& get_commander() const;
};

У каждого солдата есть командир, но, чтобы отыскать его, требуется
проверить немало объектов и выполнить несколько запросов. Чтобы

ES.50. Не приводите переменные с квалификатором const  197

не дублировать код и не увеличивать бремя сопровождения, можно использовать перегруженную версию с квалификатором const, вот так:
commander& soldier::get_commander() {
return const_cast(
static_cast(*this).get_commander());
}

Здесь const_cast применяется к возвращаемому типу, поэтому разумно
предположить, что, поскольку это неконстантная функция, в таком преобразовании нет ничего опасного. Однако это противоречит духу Руководства. К счастью, с тех пор, как в C++11 появились завершающие типы
возвращаемого значения, имеется лучшее решение:
class soldier {
public:
commander& get_commander();
commander const& get_commander() const;
private:
template
static auto get_commander_impl(T& t)
-> decltype(t.get_commander) {
// реализация функции
}
};

Общедоступные функции get_commander просто перенаправляют вызов
шаблону статической функции. Преимущество этого решения в том, что
шаблон функции знает, когда он работает с объектом soldier const. Квалификатор const является частью типа T. Если реализация нарушит const, то
компилятор сообщит об ошибке. Никакого приведения не требуется, и это
хорошо, так как приведение смотрится уродливо.
Однако это решение не всегда пригодно. Рассмотрим пример с current_
animation:
class unit {
public:
animation* current_animation();
animation const* current_animation() const;
private:
animation* m_animation;
};

198  Часть III. Прекратите это использовать

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

КЭШИРОВАНИЕ И ОТЛОЖЕННЫЕ ВЫЧИСЛЕНИЯ
Другой пример, который может сподвигнуть на использование ключевого
слова const_cast, — кэширование результатов дорогостоящих вычислений.
Их много в области моделирования мира, даже в подмножестве размером
2 на 2 километра. Как мы уже отмечали, выяснение личности командира —
нетривиальная задача, поэтому рассмотрим попытку сохранить значение
для ускорения его получения в будущем.
class soldier {
public:
commander& get_commander();
commander const& get_commander() const;
private:
commander* m_commander;
template
static auto get_commander_impl(T& t)
-> decltype(t.get_commander);

};

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

ES.50. Не приводите переменные с квалификатором const  199

кто является командиром, и запишет значение обратно в член m_commander,
прежде чем разыменовать указатель и вернуть ссылку. Это удивительно
дорогостоящая процедура. Мы намеренно использовали словосочетание
«продерется через дебри»: с таким количеством объектов, составляющих
мир, и с таким количеством различных отношений, которые нужно поддерживать и контролировать, поиск командира можно сравнить с ситуацией,
когда вы продираетесь через праздничную толпу на улице, выкрикивая имя
своего друга в надежде быстро отыскать его. Так что слежение за командиром — идеальный кандидат для кэширования.
Беда в том, что этот шаблон функции должен работать как с объектами
типа soldier const, так и soldier — в первом случае изменение указателя
будет запрещено. Единственное решение — const_cast t:
template
auto soldier::get_commander_impl(T& t) -> decltype(t.get_commander) {
if (!t.m_commander->is_alive()) {
... // Поиск нового командира
const_cast(t).m_commander = new_commander;
}
return *t.m_commander;
}

Очень некрасиво.

ДВА ВИДА CONST
Есть две интерпретации «константности». Преимущество, которое дает
const, заключается в отсутствии заметной разницы в природе объекта,
возвращаемого вызовом константной функции-члена. Предположим, что
объект является полностью автономным и не ссылается на другие объекты, подобно чистой функции, которая не ссылается на данные, внешние
по отношению к ее области видимости. В этом случае последовательные
вызовы константных функций-членов всегда будут давать одни и те же
результаты.
Это не означает, что представление объекта останется неизменным. Такой
уровень абстракции был бы неверным. Реализация не ваша забота сейчас,
видимый интерфейс — вот что должно вас волновать.
На самом деле рассматриваемая сейчас нами проблема затрагивает
важный аспект проектирования классов. Чем владеет ваш класс, с чем

200  Часть III. Прекратите это использовать

разделяет права владения и в чем просто заинтересован? В примере
с классом unit некоторые переменные-члены хранят указатели на другие
объекты, принадлежащие кому-то другому. Что означает квалификатор
const в этом случае?
В этом случае он может означать лишь, что функция не изменяет связанный с ней объект каким-либо наблюдаемым образом. Такая константность
известна как логическая.
Другой вид константности известен как побитовая константность. Она накладывается автором функции-члена с квалификатором const и означает,
что никакая часть представления объекта не может быть изменена во время
выполнения функции. Это требование гарантируется компилятором.
Присутствие const_cast внутри функции-члена с квалификатором const
должно вызвать у вас муки совести. Вы обманываете своих клиентов.
К сожалению, пример с кэшированием не единственное место, где может понадобиться использовать const_cast. Представьте, что вы решили
гарантировать потокобезопасность вашего класса. Одно из возможных
решений — добавить переменную-член с мьютексом и блокировать его
при выполнении функций-членов. Мы не можем оставить это последнее
предложение без внимания и хотели бы отметить, что вы должны свести
к минимуму досягаемость мьютекса: он должен охватывать как можно
меньше данных и быть частью наименьшей возможной абстракции.
Это перекликается с рекомендациями «ES.5. Минимизируйте области
видимости» и «CP.43. Минимизируйте время выполнения критической
секции».
Но использование мьютекса-члена влечет за собой еще одну проблему: как
заблокировать его в функции-члене с квалификатором const, если для этого
требуется изменить состояние мьютекса?
Теперь, определив разницу между логической и побитовой константностью, можно ввести ключевое слово mutable. Цель этого ключевого слова —
учитывать логическую константность объекта, не учитывая побитовую
константность. Это давнишнее ключевое слово, его можно найти в довольно
старых программных проектах. Оно декорирует переменные-члены, показывая, что они могут изменяться во время выполнения константных
функций-членов.
Но вернемся к примеру с классом unit и посмотрим, как можно его использовать:

ES.50. Не приводите переменные с квалификатором const  201
class soldier {
public:
commander& get_commander();
commander const& get_commander() const;
private:
mutable commander* m_commander; // Иммунитет от ограничений const
template
static auto get_commander_impl(T& t)
-> decltype(t.get_commander) {
if (!t.m_commander->is_alive()) {
... // Поиск нового командира
t.m_commander = new_commander; // Всегда доступно для изменения
}
return *t.m_commander;
}

};

Теперь член m_commander можно изменять даже по константной ссылке.

СЮРПРИЗЫ CONST
Очевидно, что не следует произвольно разбрасывать mutable по всему классу.
Это ключевое слово должно применяться только к данным, используемым
для подсчетов, а не для моделирования абстракции. В отношении члена
мьютекса это разумная политика. Однако в примере выше это не так очевидно. Простые указатели сбивают с толку и мутят воду в процессе определения
владения, как обсуждалось в рекомендации «I.11. Никогда не передавайте
владение через необработанный (простой) указатель (T*) или ссылку (T&)».
Они также вносят путаницу в дизайн API, когда дело доходит до отделения
представления от контроллера.
Например, следующий фрагмент кода является вполне законным:
class int_holder {
public:
void increment() const { ++ *m_i; }
private:
std::unique_ptr m_i;
};

Он вызовет удивление у типичного пользователя, предполагающего невозможность изменения объекта std::unique_ptr и его содержимого.

202  Часть III. Прекратите это использовать

На самом деле const распространяется
только до объекта std::unique_ptr. РаНе следует произвольно
зыменование объекта — это константная
разбрасывать mutable по
всему классу. Это ключеоперация, потому что она не изменявое слово должно приет std::unique_ptr. Это подчеркивает
меняться только к данным,
важность понимания разницы между
используемым для подсчеконстантным указателем и указателем
тов, а не для моделирована константу. Константный указатель
ния абстракции.
нельзя изменить, но объект, на который он указывает, можно. Это чем-то
напоминает поведение ссылок: их нельзя переустановить — они не могут
ссылаться на другой объект в течение всего времени своего существования,
но объект, на который они ссылаются, изменить можно.
Кстати говоря, ссылки не могут преподнести такой сюрприз:
class int_holder {
public:
void increment() const { ++ m_i; }
private:
int& m_i;
};

Этот код не будет компилироваться. Однако хранить объекты по ссылке
в классах редко бывает хорошей идеей: это полностью исключает при­
сваивание значением по умолчанию, потому что ссылку нельзя переустановить.
Что действительно необходимо, так это форма идиомы pImpl, объединяющая указатель с объектом внутри одного класса и распространяющая
константность на него. Такой объект был предложен для стандартизации
и на момент написания книги был доступен в библиотеке fundamentals v2
под названием std::experimental::propagate_const. Ожидаются и другие
решения, следите за событиями.

ПОДВЕДЕМ ИТОГ
zz

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

ES.50. Не приводите переменные с квалификатором const  203
zz

Не вводите никого в заблуждение, говоря о константности и используя при этом const_cast.

zz

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

zz

При необходимости используйте mutable, желательно только для
переменных-членов, служащих для подсчетов, а не для моделирования абстракции.

zz

Помните о том, как далеко распространяется действие const, и о разнице между константным указателем и указателем на константу.

ГЛАВА 3.5

E.28. При обработке ошибок
избегайте глобальных
состояний (например, errno)

ОБРАБАТЫВАТЬ ОШИБКИ СЛОЖНО
Наверное, где-то есть люди, которые пишут совершенный и безошибочный
код. К сожалению, у нас, простых смертных, при разработке кода иногда
все идет не по плану. Причины этого могут быть разные: задача была определена неверно, переполнился буфер ввода или сломалось железо. Обычно
программы не могут просто метафорически пожать плечами, пробормотать
«ну ладно» и прекратить работу или просто попытаться продолжить выполнение в меру своих возможностей. Программы не люди, это машины,
не имеющие сознания. Они точно и беспрекословно выполняют все, что
им приказывают, хотя иногда в это трудно поверить.
Обработка ошибок имеет долгую, многообразную и пеструю историю. Слово «многообразный» здесь подчеркивает факт существования множества
способов обработки ошибок и отсутствие универсального средства.

ЯЗЫК C И ERRNO
Цель этой рекомендации — показать, как избежать проблем, связанных
c передачей ошибки через глобальное состояние, поэтому начнем с него.
Когда речь заходит о глобальном состоянии, на ум сразу же приходят две
проблемы:
zz

принадлежность к потоку;

zz

ссылка на ошибку.

E.28. При обработке ошибок избегайте глобальных состояний  205

Проблема принадлежности к потоку достаточно проста: в современном
многопоточном мире глобальный объект ошибки должен содержать информацию о потоке, сообщившем об ошибке. Можно попробовать использовать
объект с классом хранения в локальном потоке, но это будет означать,
что ошибки, возникшие за пределами конкретного потока, будут скрыты.
Другой вариант: создать отдельный объект с информацией об ошибках для
каждого потока, что требует тщательной синхронизации. Синхронизацию
вполне можно осуществить, но все-таки это требование снижает привлекательность такого подхода для многих разработчиков в их личном рейтинге
предпочтительных решений для обработки ошибок.
Конечно, так было не всегда, и унаследованный старый код действительно
будет содержать ссылки на объект с именем errno. Этот объект — часть
стандарта C и имеет простой тип int. Начиная с С++11, объект errno получил класс хранения в локальном потоке, а до этого он имел статический
класс. Это не самое плохое решение: если что-то пойдет не так, появится
ошибка, вы наверняка захотите вызвать функцию обработки ошибок, чтобы
исправить ее. Одиночный код ошибки означает, что совершенно неважно,
где произошла ошибка, функция обработки ошибок сможет определить,
что именно пошло не так. Код ошибки можно разделить на группы битов:
одни биты определяют область ошибки, другие — ее характер, а третьи биты
добавляют дополнительный контекст.
Но представьте, что получится, если во время перехода к коду обработки
ошибки произойдет другая ошибка. Ваш код обработки может обрабатывать только самую последнюю ошибку, что затрудняет восстановление
программы до работоспособного состояния. Кроме того, подход с единственным значением просто плохо масштабируется. Любой код должен
проверять состояние после вызова функции в случае, если она сообщила
об ошибке. Даже если функция не сообщает об ошибках, это не значит, что
она не изменится и не начнет сообщать о них в будущем. Наконец, клиенты
могут и будут игнорировать или забывать коды ошибок, независимо от того,
насколько настойчиво вы в своей документации советуете им сохранять
бдительность и не делать этого.
Вся ситуация здорово напоминает «Глобальные объекты — это плохо.
Понятно?». Поскольку объект errno не имеет владельца, невозможно
контролировать, как им распоряжаются в необычных обстоятельствах.
Что будет, если в процессе обработки ошибки произойдет еще одна
ошибка? Как отмечает Руководство, глобальным состоянием трудно
управлять.

206  Часть III. Прекратите это использовать

К сожалению, как уже говорилось выше, errno является частью стандарта,
и некоторые разделы стандартной библиотеки языка C продолжают использовать errno, чтобы сообщать об ошибках. Например, если попытаться
вычислить квадратный корень из отрицательного числа вызовом sqrt,
в переменную errno будет записано значением EDOM.
Проблемы с передачей ошибок через errno осознаны давно, еще до появления C++. С тех пор было рассмотрено множество альтернатив, успешных
и не очень, просочившихся в программные проекты по всему миру, и часть
изних была принята в качестве стандартной практики. Давайте рассмотрим
некоторые из подходов к обработке ошибок.

КОДЫ ВОЗВРАТА
Самый распространенный способ сигнализировать об ошибке — просто
сообщить о ней вызывающему коду напрямую. Организовать это можно
с помощью возвращаемого значения. Если функция имеет ограниченный
диапазон значений результата, то значения не из этого диапазона можно использовать, чтобы сигнализировать об ошибке. Например, scanf возвращает
количество аргументов, которым было успешно присвоено значение, то есть
число, которое больше нуля или равное нулю, а об ошибке сигнализирует,
возвращая EOF, то есть отрицательное значение.
Этот метод имеет очевидные недостатки. Первый: возвращаемый тип
должен соответствовать диапазону значений результата и кодов ошибок.
Рассмотрим функцию из стандартной библиотеки, вычисляющую натуральный логарифм числа:
double log(double);

Концептуально область результатов полностью охватывает возвращаемый
тип, и нет возможности вернуть признак ошибки. Однако стандарт предусматривает возврат специальных значений, сигнализирующих о том, что
что-то пошло не так; для этого используется несколько значений типа double,
выделенных для представления ошибок. Это может стать неожиданностью
для начинающего программиста.
Другой недостаток: вызывающие программы могут игнорировать коды
возврата, если только функция не отмечена атрибутом [[no_discard]].
Но даже в этом случае вызывающий код может игнорировать объект с кодом

E.28. При обработке ошибок избегайте глобальных состояний  207

возврата. Хотя в этом случае ответственность незаметно перекладывается
на вызывающий код, ведь никто не заинтересован в том, чтобы допускать
отбрасывание ошибок.
Другой способ вернуть код ошибки — передать функции ссылку на переменную, через которую можно сообщить об ошибке. Вот как выглядит
такой API:
double log(double, int&);

Вызывающий код предоставляет место для сообщения об ошибках и после
вызова проверяет наличие сообщения. В этом решении тоже не все хорошо.
Во-первых, оно некрасивое. Читаемые API просто рассказывают о происходящем: если вам нужен логарифм, то вы предполагаете, что есть функция,
принимающая и возвращающая число. Присутствие дополнительного
параметра создает дополнительную когнитивную нагрузку.
Во-вторых, такое решение несет дополнительные накладные расходы, даже
когда вы абсолютно уверены в диапазоне чисел, передаваемых функции.
Такой подход часто приводит к API с двойными функциями, где каждая
функция имеет соответствующую перегруженную версию, принимающую
дополнительный параметр для возврата ошибки. Это по меньшей мере
не очень элегантно.

ИСКЛЮЧЕНИЯ
Вместе с C++ появились конструкторы — специальные функции, которые
ничего не возвращают и в некоторых случаях не принимают аргументов.
Здесь не помогут никакие манипуляции с сигнатурами: вы не сможете
сигнализировать об ошибке, не пожертвовав конструктором по умолчанию.
Хуже того, все деструкторы имеют одну сигнатуру, не предусматривающую
аргументов и ничего не возвращающую. Аналогично ведут себя перегруженные операторы.
Для таких случаев и структур были введены исключения. Они создают
дополнительный путь возврата, по которому можно проследовать в обход
обычного пути и выполнить все необходимые заключительные операции,
обычно производимые в конце функции. Кроме того, исключения можно
перехватывать в любом месте в стеке вызовов, а не только в месте вызова функции, что позволяет программисту обрабатывать ошибки там,

208  Часть III. Прекратите это использовать

где это возможно, или просто игнорировать их, позволяя им подниматься
выше по стеку вызовов.
Обработка исключений — дорогое удовольствие. Конец функции очевиден, это оператор return или закрывающая фигурная скобка. Исключение
может возникнуть во время любого вызова и требует много вспомогательного кода для управления им. Всякий раз, когда вызывается функция, она может сгенерировать исключение, поэтому компилятор должен
вставить в это событие весь код, необходимый для очистки стека. Такой
дополнительный код не только влияет на скорость выполнения, но и занимает место. Накладные расходы при этой реализации оказываются
настолько велики, что все компиляторы предоставляют возможность отключить раскрутку исключений в обмен на невозможность использовать
блоки try/catch.
Это действительно плохое решение. Результаты опроса, проведенного C++
Foundation в 2019 году1, показали, что сообщество C++ разделено на две
части: около половины всех проектов на C++ полностью или частично
запрещают исключения. И еще один важный нюанс: запрет исключений
лишает проекты возможности обращаться к частям стандартной библиотеки, использующим исключения для сигнализации об ошибках.


Второй стандарт C++, C++11, представил еще одно решение: типы std::
error_code и std::error_condition , которые можно найти в заголовке
. Этот механизм сигнализации об ошибках позволяет стандартизировать отчеты об ошибках, сгенерированных операционной системой или низкоуровневыми интерфейсами в случае автономных систем.
Он включает не только код, но и указатель на категорию ошибки. Такое
нововведение позволяет программистам создавать и анализировать новые
семейства ошибок, производные от базового класса error_category.
Упомянутые типы решают проблему подобия ошибок друг другу. Перечисление errc определяет большой набор кодов ошибок, импортированных из
POSIX, и дает им более понятные имена. Например, ENOENT преобразуется
в no_such_file_or_directory. Предопределенные категории ошибок включают
generic_category, system_category, iostream_category и future_category.
1

https://isocpp.org/files/papers/CppDevSurvey-2019-04-summary.pdf

E.28. При обработке ошибок избегайте глобальных состояний  209

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

BOOST.OUTCOME
Естественно, были и другие попытки исправить ситуацию с обработкой
ошибок, и предлагались альтернативные решения. Одно из них можно найти
на сайте boost.org1 — в богатом источнике классов, помогающих улучшить
код. В Boost есть два класса для решения проблемы обработки ошибок.
Один из них — result. Первый параметр здесь представляет тип возвращаемого объекта. Второй — тип объекта с информацией
о причине сбоя. Экземпляр result будет содержать либо экземпляр Т,
либо экземпляр Е . В этом отношении он очень похож на вариантный
тип. О третьем параметре можно прочитать в документации Boost. Хотя,
признаемся, для большинства целей достаточно значения по умолчанию,
а документация довольно сложная.
Создав объект для возврата чего-то, можно дополнительно предложить
преобразование в значение типа bool, сообщающее об успехе вызова или
о наличии ошибки, а также функции для выяснения характера ошибки.
Поскольку это либо T, либо E, то в случае нормального выполнения это
ничего не будет стоить. Вот как можно использовать этот тип:
outcome::result log(double); // прототип функции
r = log(1.385);
if (r)
{
// Нормальное продолжение выполнения
}
else
{
// Обработка ошибки
}
1

https://www.boost.org/doc/libs/develop/libs/outcome/doc/html/index.html

210  Часть III. Прекратите это использовать

Это замечательное решение: оно предлагает согласованный способ проверки
наличия ошибок, при этом информация об ошибке в случае ее возникновения помещается в объект. Все локально. Единственный недостаток — можно
забыть проверить наличие ошибки.
Второй интересный класс — outcome. Если класс
result сигнализирует об успешном или неуспешном выполнении в сочетании с контекстом, то класс outcome представляет ошибки двумя способами:
как ожидаемые и как неожиданные, которые задаются вторым и третьим
параметрами. Ожидаемую ошибку можно исправить, а неожиданную — нет.
Появление неустранимой ошибки — это ситуация, когда обычно принято
генерировать исключение.
Этот класс — способ предохранения кода от исключений. Он может
передавать исключения между уровнями программы, которые изначально
не предусматривали обработку исключений.
Несмотря на удобство, тип boost::outcome, являющийся частью библиотеки
Boost, не стандартизирован (подробнее об этом — чуть ниже) и поэтому
доступен только в коде, где разрешено использовать Boost.
Существует поразительное количество окружений, где ситуация иная, из-за
чего остаются доступными только три варианта обработки ошибок: исключения, передача кодов ошибок через стек вызовов или развертывание своих
механизмов обработки ошибок, усовершенствованных и приспособленных
для нужд предметной области.
Такое положение вещей не кажется удовлетворительным.

ПОЧЕМУ ОБРАБАТЫВАТЬ ОШИБКИ
ТАК СЛОЖНО
Первая проблема в том, что ошибки разных типов должны обрабатываться
по-разному. Выделим три типа ошибок.
Представьте функцию преобразования строки в число. Преобразование
потерпит неудачу, если строка содержит еще что-то, кроме цифр. Это исправимая ошибка, о которой следует сообщить вызывающей стороне:
в вызывающем коде имеет место логическая ошибка, из-за чего входные

E.28. При обработке ошибок избегайте глобальных состояний  211

данные не соответствуют ограничениям, указанным автором функции.
То есть здесь самый простой вид ошибок: «Я ожидал это, вы передали мне
неправильное значение, поэтому пойдите и подумайте, что вы сделали».
Налицо явное нарушение предварительного условия, и вызывающий код
всегда должен извещаться о подобных ошибках.
Следующий вид ошибок — ошибка программирования. Здесь неправильное действие совершил не вызывающий, а вызываемый код, например попытавшись разыменовать область памяти, которую не должен был. Беда
таких ошибок в том, что они ставят программу в ситуацию, когда та должна
остановиться. Из-за ошибки невозможно рассуждать о происходящем,
и нет смысла сообщать о ней вызывающему коду. Разве они смогут как-то
исправить ситуацию? Программа оказывается в поврежденном состоянии,
восстановить которое невозможно.
Теперь об ошибках последнего типа, которые не так однозначны. Они происходят при появлении нарушений в программном окружении. Во вступительной части стандарта вместе с правилом «как если бы» описывается
«абстрактная машина» [intro.abstract]1. Есть несколько способов нарушить
работу абстрактной машины, но наиболее часто встречается исчерпание
свободного пространства в хранилище или в стеке. В обоих случаях возникает нехватка памяти. Исчерпать свободное пространство в хранилище
можно, запросив выделить место для вектора с миллиардом элементов
типа double на 32-битной машине. Чтобы исчерпать стек, можно запустить
бесконечную рекурсию.
Только в первом случае, при ошибке, которая считается исправимой, следует уведомить вызывающий код, сгенерировав исключение или вернув
код ошибки. Во всех других случаях нужно просто остановить программу
и сообщить об этом программисту через утверждение assert, запись в файл
журнала или стандартный вывод. Но помните, что в случае нарушения
абстрактной машины ваши возможности сообщить об этом программисту
могут быть довольно ограниченными.
Вторая проблема в том, что вы не знаете, как вызывающий код среагирует
на ошибки. Нельзя полагаться на то, что он обработает ошибку. Он может
намеренно отбросить код ошибки и продолжать свою веселую жизнь, а может просто забыть о ней.
1

https://eel.is/c++draft/intro.abstract

212  Часть III. Прекратите это использовать

Например, функция преобразования
строк, упоминавшаяся выше, может возВы не знаете, как вызывающий код среагирует на
вращать значение типа double и вместе
ошибки. Нельзя полагаться
со строкой для преобразования принина то, что он обработает
мать ссылку для возврата кода ошибки.
ошибку.
Этот код ошибки может быть просто
проигнорирован. Проблема игнорирования возвращаемых кодов частично решается добавлением атрибута
[[no_discard]], но только если возвращаемое значение используется исключительно для возврата ошибок. И даже в этом случае вызывающий код
может принять возвращаемое значение в локальную переменную и забыть
об этом. В любом случае заставлять пользователей проверять наличие
ошибок — не лучшая идея, ведь тогда решение о том, как поступать в случае
сбоя, будет приниматься ими, а не вами.
Проблема в том, что вариантов неправильного выполнения кода намного
больше, чем правильного. По аналогии с человеческим телом: в наших силах точно определить, находится ли оно в хорошем рабочем состоянии, но
разобраться в бесчисленном множестве причин, по которым его состояние
может ухудшиться, — сложная задача.

СВЕТ В КОНЦЕ ТУННЕЛЯ
Мы уже отметили, что решения Boost не стандартизированы и доступны
только там, где разрешено использовать эту библиотеку. Комитет уже рассматривает несколько документов с целью улучшить ситуацию и с ошибками, и с Boost.
Первый из них предлагает добавить в стандарт тип std::expected,
который может содержать значение типа T или E, причем Е представляет
причину отсутствия значения типа T. Этот тип больше похож на специализированную версию std::variant. Впервые он был предложен вскоре после
публикации стандарта C++14, то есть его рассматривают уже довольно
долго. Проверить, как движется дело, можно, изучив документ P03231.
Второй документ определяет детерминированные исключения с нулевыми издержками, которые генерируют значения, а не типы. Этот документ
направлен на объединение разделившегося на почве обработки ошибок
1

www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0323r10.html

E.28. При обработке ошибок избегайте глобальных состояний  213

сообщества C++. Он старается сделать обработку исключений намного
более приемлемой. В частности, генерируемые исключения размещаются
в стеке и типизируются статически, благодаря чему нет необходимости
выделять место в куче и не используется механизм RTTI. Детерминированные исключения похожи на особый возвращаемый тип. Узнать,
как движется дело с рассмотрением этих предложений, можно, изучив
документ P07091.
Обратите внимание, что Boost.Outcome является частичной, библиотечной
реализацией этой идеи. Для полноценной реализации необходимо внести
изменения в язык.
Третий документ предлагает в дополнение к детерминированным исключениям с нулевыми издержками ввести новый объект status_code
и стандартный объект ошибки, а также некоторые улучшения по сравнению с заголовком . К ним относится, например, отказ
от включения заголовка , который подразумевает присутствие
большого количества дополнительных механизмов, таких как аллокаторы и алгоритмы. Он значительно увеличивает тем самым время сборки
и компоновки.
Еще один из аспектов обработки ошибок, который пока не упоминался, —
использование макроса assert для обнаружения экземпляров ошибок второго типа, описанных в предыдущем разделе. С его помощью программист
задает ожидаемое состояние абстрактной машины в определенный момент,
а макрос проверяет фактическое состояние и останавливает программу, если
фактическое состояние окажется отличным от заданного. Ошибки этого
вида отличаются от ошибок нарушения состояния абстрактной машины:
они считаются ошибками программиста.
В развитие этой идеи рассматривалось еще одно предложение, которое чуть
не вошло в C++20. Фактически дело дошло до стадии рабочего проекта, но
в последнюю минуту продвижение было остановлено. Мы говорим о контрактах. Они определяют предварительные и заключительные условия на
уровне языка, а не на уровне библиотечного макроса. Тем самым программист получает возможность снабжать определение функции атрибутами,
определяющими также и ожидания.
К сожалению, на момент написания этих строк такое множество разно­
образных обновлений языка и библиотеки еще не было включено в рабочий
1

www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0709r4.pdf

214  Часть III. Прекратите это использовать

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

ПОДВЕДЕМ ИТОГ
zz

Глобальное состояние не годится для обработки ошибок. Сохраняйте
состояние ошибки локально и всегда обращайте на него внимание.

zz

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

zz

Различайте типы ошибок: неверный ввод, неверная логика, неверное
окружение и т. д.

zz

Если обработка исключений вам недоступна, подумайте о возможности использования Boost.Outcome, но не забывайте мониторить
предстоящие изменения в стандарте. Core Guidelines не охватывает
этот подход, потому что он выходит за рамки стандарта.

ГЛАВА 3.6

SF.7. Не используйте using
namespace в глобальной
области видимости
в заголовочном файле

НЕ ДЕЛАЙТЕ ЭТОГО
Пожалуйста, не делайте этого. Никогда.
Никогда. Ни за что. Ни разу.
Пожалуйста.
В Core Guidelines приводится следующий пример:
// bad.h
#include
using namespace std; // плохо