Привет, Хабр. С гордостью, триумфом и трепетом хотим рассказать вам об одной из наших флагманских новинок, вышедшей в пылающем июле — книге «Экскурс в неопределённое поведение C++».

Cегодня книжные полки изобилуют нестареющими пособиями по C++. Этот язык чрезвычайно важен не только в разработке игр, финансового софта и встраиваемого ПО, но и как основной материал для изучения алгоритмов. Именно поэтому мы даже выпустили две книги-билингвы по алгоритмам, в которых код на C++ соседствует с идентичным ему кодом на Python. Это наш многолетний бестселлер «Алгоритмический тренинг. Решения практических задач на Python и C++» Максима Иванова и недавняя новинка «Базовые алгоритмы. Реализации на Python и C++ на примере классических игр» Павла Довгалюка. Но язык C++ не только очень полезен, но и опасен, так как на этапе преобразования исходного кода в машинный многие решения отдаются на откуп компилятору. Поскольку компилятор в большинстве режимов изначально заточен на оптимизацию кода, он регулярно привносит в код C++ непредсказуемые и порой необъяснимые варианты неопределённого поведения (UB, Undefined Behavior). Титаническую работу по систематизации неопределённого поведения в C++ проделал уважаемый Дмитрий Свиридкин @Nekrolm. В настоящее время он работает инженером по программированию встраиваемых систем в отделе Cloudfront Compute компании AWS. Дмитрий преподавал курсы по Linux и C++ в Санкт-Петербургском государственном университете и Высшей школе экономики, а также имеет богатейший послужной список, в котором есть и олимпиады по информатике, и машинное обучение, и программирование прошивок и, конечно же, выжимание последних капель производительности из самого неукротимого облачного железа. Некоторое время его заметки публиковались на сайте компании PVS-Studio, разрабатывающей известный российский статический анализатор кода.

Предисловие Андрея Карпова

Чтобы был понятен мой интерес к теме неопределённого поведения (UB), начнём с того, чем я занимаюсь.

Меня зовут Андрей Карпов и я... C++ программист. По крайней мере, точно им был. Сейчас моя деятельность сместилась в сторону DevRel активностей и обучения сотрудников. Однако я успел вдоволь попрограммировать системы обработки больших массивов данных, пописать специализированные CAD-системы для медицины. И, самое главное, я стал одним из основателей проекта PVS-Studio — статического анализатора для поиска ошибок в коде. На момент написания книги инструмент поддерживает анализ программ на C, C++, C# и Java, а начиналось всё именно с C и C++.

Если на предыдущих местах работы я успел походить по различным граблям написания С++ кода, то, начав работать над PVS-Studio, я познал всю красоту кошмаров, которые можно создать на этом языке. Можно сказать, я узрел бездну, пучину... воплощение вселенского ужаса! И думаю, не открою ничего удивительного, если скажу, что существенная часть этого ужаса связана как раз с неопределённым поведением.

Большинство неалгоритмиче��ких ошибок в C++ коде связаны с неопределённым поведением. Выход за границу массива/переполнение буфера — UB. Неправильное использование printf — UB. Деление на ноль — UB. Разыменование нулевого указателя — UB. Использование уничтоженного объекта/освобождённой памяти — UB. Переполнение переменной типа int — UB. Применили оператор delete к указателю типа void *? UB. Добавили отсебятину в пространства имён std? UB. Продолжать можно долго, доказательством чему является эта книга.

Разрабатывая статический анализ, нет смысла заваливать пользователя предупреждениями с упоминанием неопределённого поведения. Собственно, тогда половину предупреждений можно сформулировать как "у вас тут UB". Толку от такой формальности нет. Поэтому анализаторы сообщают о разыменовании нулевых указателей: что индекс вышел за границу массива, что хорошего не будет от вот этой штуки, которую вы засунули в namespace std. Однако за всеми этими найденными проблемами просматривается многоликое UB. Так что разработчики статических анализаторов обязаны регулярно медитировать над теорией неопределённого поведения.

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

Так я набрёл на электронную подборку Дмитрия Свиридкина на GitHub, посвящённую неопределённому поведению (ubbook). С большим любопытством с ней познакомился, выписал для себя ряд интересных мыслей, которые со временем станут основой новых диагностических правил. В общем, я получил от чтения и удовольствие, и пользу.

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

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

Мы проработали, расширили материал, параллельно публикуя его в блоге PVS-Studio. А теперь предлагаем вашему вниманию печатный вариант книги. Думаю, получилось хорошо, и вы не раз восхититесь различными нюансами языка C++ и ловушками, которые он хранит. Дмитрий обладает большим теоретическим и практическим опытом, и проделал поистине грандиозную работу, оформив свои знания в виде книги. Обещаю, будет интересно. Запасайтесь печеньками и вниманием для приятного и вдумчивого чтения.

С уважением, Андрей Карпов.

Обнаружив россыпь этой ценнейшей информации, мы обратились к автору с предложением собрать из неё книгу для будущих асов низкоуровневого программирования.  Неоценимую помощь в её подготовке оказал уважаемый Андрей Карпов @Andrey2008 директор по развитию бизнеса компании PVS-Studio. Он тщательно вычитал и отредактировал книгу, написал к ней предисловие и ещё в январе анонсировал на Хабре её выход. Господин Свиридкин мечтал назвать свою работу «Ружьё достаточной огневой мощи, чтобы на нём повеситься», но мы, опасаясь травмировать нежную психику отечественных книжных маркетологов, коллегиально остановились на названии «Экскурс в неопределённое поведение C++», непосредственно отсылающем к нетленке Бьёрна Страуструпа «A Tour of C++».   

О чём эта книга

Книга Свиридкина определённо станет классикой российско�� компьютерки, поскольку на всём протяжении этого текста автор пытается ответить на один из вечных вопросов великой русской литературы — «что делать»? Заходите под спойлер, там содержание.

Содержание

Суть неопределённого поведения

Думаю, что читатели, сталкивавшиеся с неопределённым поведением, не нуждаются в рассказе о его симптомах и не ищут страшилок на ночь — их интересуют причины явления, способы противодействия ему или смягчения последствий. Но на всякий случай, готовясь к этому анонсу, я позавчера перевёл в моём редакционном блоге большой текст о неопределённом поведении в С и C++, может кто ещё не видел. Суть неопределённого поведения заключается в том, что результат некоторых операций в стандарте не регламентирован, поэтому компилятор, преобразуя исходный код в машинный, может трактовать их как ему вздумается. Слишком часто такие трактовки оказываются несовместимы с дальнейшим выполнением программы, либо приводят к жутким латентным ошибкам, которые не проявляются в программе до самого критического момента. По этим причинам неопределённое поведение контринтуитивно и очень плохо покрывается тестами, и единственный относительно годный инструмент для его выявления — это статический анализатор кода (либо санитайзер). Неудивительно, что наработки Дмитрия впервые вышли в свет именно на сайте PVS-Studio. Исторически неопределённое поведение возникло в языке С потому, что этот древний язык рассчитан на поддержку всевозможных аппаратных платформ, в том числе, древних, экзотических и чрезвычайно маломощных по нынешним меркам. Кроме того, сыграло свою роль превратное понимание языка С как «просто высокоуровневого ассемблера» — что, конечно же, является недопустимым упрощением.

Уникальность книги Свиридкина заключается в том, что в ней сделан акцент именно на высокопроизводительном C++. Более того, в книге есть отдельные сопоставления C++ и Rust и даже встречается код на Rust. Таким образом, здесь вы прочитаете именно об актуальных рисках, о тех вариантах UB, которые можно считать неизбежными в любом сколь-нибудь серьёзном боевом коде на C++.

Подробнее о наполнении книги

Книга имеет внушительный для столь сложной темы объём — 370+ страниц без учёта оглавления. В первых главах рассматриваются элементарные контексты, в которых возникает неопределённое поведение – операции над целыми числами, выполнение программы, жизненный цикл объекта и его возможные нарушения, синтаксические ошибки. Особое внимание уделено висячим ссылкам, двойному высвобождению и использованию после высвобождения. Автор показывает, что, при всей порочности преждевременной оптимизации, жизнь программиста осложняется и из-за обычной невнимательности, забывчивости и непредусмотренных вариантов, обусловленных, например, необходимостью обеспечить какую-то внезапную совместимость.

Во второй части книги разобраны темы, более специализированные вещи, характерные для языка C++. Настоящим бриллиантом в оправе остальной книги является глава 4, посвящённая стандартной библиотеке. В этой главе есть шесть разделов, название которых начинается с красноречивого слова «Грабли». Объяснена работа со строками, диапазонами, конструкторами, функциями, а также проиллюстрированы разные (в том числе, опасные) варианты перегрузки.

Небольшая глава 6 полностью посвящена происхождению указателей. Вы понимаете, почему это важно. Мы даже оставили в этой главе без перевода термины «strict aliasing» и «type punning». Как руководитель проекта отмечу в сторону, что Дмитрий Свиридкин чрезвычайно ответственно и профессионально подошёл к моей просьбе выполоть из книги все ненужные англицизмы и перевести максимум терминов, но местами – как здесь — мы оставили термины без перевода, стремясь к максимальной ясности.

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

Об иллюстрации на обложке

Летом я публиковал на Хабре полюбившийся многим из вас лонгрид «Меховой Интернет: как появляются ваши любимые книжные обложки» об истории происхождения «зверинца» на обложках издательства O’Reilly. В нашем случае вопрос с обложкой был наиболее прост и безальтернативен: её украшает единорог. Вот как обосновал этот художественный выбор Дмитрий Свиридкин:

«Неопределенное поведение в языках C++ и С связано с различными мифами в разработке («может произойти что угодно») и часто не воспринимается всерьез новичками.  Желательно изобразить единорога —мифическое существо, неуловимое и невероятно опасное. В книге рассматриваются различные варианты «укрощения» неопределенного поведения, так что стоит изобразить единорога с золотой уздечкой и угрозой/растерянностью во взгляде. Может быть даже лучше изобразить частично сцену укрощения единорога, или охоты на единорога:

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

Приятного чтения и доброй охоты, господа.