Как стать автором
Обновить
323.22
PVS-Studio
Статический анализ кода для C, C++, C# и Java

60 антипаттернов для С++ программиста, часть 2 (совет 6 — 10)

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров4.7K

1053_60_cpp_antipatterns_ru/image2.png


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


Я буду публиковать советы по 5 штук, чтобы не утомить вас, так как мини-книга содержит много интересных отсылок на другие статьи, видео и т. д. Однако, если вам не терпится, здесь вы можете сразу перейти к её полному варианту: "60 антипаттернов для С++ программиста". В любом случае желаю приятного чтения.


Вредный совет N6. Невидимые символы


Используйте при написании кода невидимые символы. Пусть ваш код работает магическим образом. Это прикольно.


Существуют Unicode-символы, которые не отображаются или изменяют видимое представление кода в среде разработки. Комбинации таких символов могут привести к тому, что человек и компилятор будут интерпретировать код по-разному. Это может быть сделано специально. Такой вид атаки называется Trojan Source.


1053_60_cpp_antipatterns_ru/image4.png


Подробнее ознакомиться с этой темой вы можете в статье "Атака Trojan Source для внедрения в код изменений, незаметных для разработчика". Настоящее хоррор-чтиво для программистов :). Рекомендую.


Более детальный разбор здесь. К счастью, анализатор PVS-Studio уже умеет обнаруживать подозрительные невидимые символы.


И заодно ещё один вредный совет. Может пригодиться для розыгрыша на 1 апреля. Оказывается, существует греческий знак вопроса U+037E, который выглядит, как точка с запятой (;).


1053_60_cpp_antipatterns_ru/image5.png


Когда коллега отвлечётся, поменяйте в его коде какую-нибудь точку с запятой на этот символ. И сидите, наблюдайте, наслаждайтесь :). Код не будет компилироваться, хотя вроде всё хорошо.


1053_60_cpp_antipatterns_ru/image6.png


Вредный совет N7. Магические числа


Используйте странные числа. Так ваша программа будет выглядеть умнее и солиднее. Согласитесь, что такие строки смотрятся хардкорно: qw = ty / 65 — 29 * s;


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


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


К сожалению, невозможно в одной главе описать множество подходов, позволяющих писать понятный красивый код. Поэтому я отправляю читателя к такому обстоятельному труду, как "Совершенный код" С. Макконнелла (ISBN 978-5-7502-0064-1).


Плюс есть отличная дискуссия на сайте Stack Overflow: What is a magic number, and why is it bad?


Вредный совет N8. Везде int


Во всех старых книгах для хранения размеров массивов и для организации циклов использовались переменные типа int. Так и делайте. Не стоит нарушать традиции.


Долгое время на распространённых платформах, где использовался язык C++, массив не мог на практике содержать более INT_MAX элементов.


Например, 32-битной программе на Windows доступно максимум 2 GB памяти (на самом деле ещё меньше). Поэтому 32-битного типа int было более чем достаточно для хранения размера массивов или для их индексации.


Раньше программисты и авторы книг не заморачивались — смело использовали в циклах счётчики типа int. И всё было хорошо.


Однако на самом деле размер таких типов, как int, unsigned и даже long, может быть недостаточен. В этот момент Linux-программисты могут удивиться: почему long недостаточно? А дело в том, что, например, компилятор MSVC при сборке приложений для платформы Windows x64 использует модель данных LLP64, в которой тип long остался 32-битным.


1053_60_cpp_antipatterns_ru/image8.png


А какие же тогда типы использовать? Безопасными для хранения размеров массивов или индексов являются memsize-типы, такие как ptrdiff_t, size_t, intptr_t, uintptr_t.


Рассмотрим простейший пример, когда использование 32-битного счётчика приведёт к ошибке при обработке большого массива в 64-битной программе:


std::vector<char> &bigArray = get();
size_t n = bigArray.size();
for (int i = 0; i < n; i++)
  bigArray[i] = 0;

Если контейнер содержит более INT_MAX элементов, то произойдёт переполнение знаковой переменной int, а это неопределённое поведение. Причём, как оно себя проявит, предсказать не так просто, как может показаться. Вот здесь я разбирал один интересный случай: "Undefined behavior ближе, чем вы думаете".


Правильным вариантом будет написать, например, так:


size_t n = bigArray.size();
for (size_t i = 0; i < n; i++)
  bigArray[i] = 0;

Ещё более правильным будет такой вариант:


std::vector<char>::size_type n = bigArray.size();
for (std::vector<char>::size_type i = 0; i < n; i++)
  bigArray[i] = 0;

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


auto n = bigArray.size();
for (auto i = 0; i < n; i++)    // :-(
  bigArray[i] = 0;

Переменная n будет иметь правильный тип, а вот счётчик i – нет. Константа 0 имеет тип int, а значит, переменная i тоже будет иметь тип int. И мы возвращаемся к тому, с чего начали.


Так как же правильно перебрать элементы и при этом написать короткий код? Во-первых, можно использовать итераторы:


for (auto it = bigArray.begin(); it != bigArray.end(); ++it)
  *it = 0;

Во-вторых, можно использовать range-based for loop:


for (auto &a : bigArray)
  a = 0;

Читатель может сказать, что всё правильно, но неприменимо к его программам. Все массивы, которые создаются в его коде, в принципе не могут быть большими, и поэтому можно по-прежнему использовать переменные int и unsigned. Рассуждение неверно по двум причинам.


Первая причина. Такой подход потенциально опасен для будущего. То, что сейчас программа не работает с большими массивами, не означает, что так будет всегда. Ещё один сценарий — код может быть заимствован в другое приложение, где обработка больших массивов – обычное дело. В конце концов, одной из причин падения ракеты Ariane 5 стало как раз использование старого кода, не рассчитанного на новые величины "горизонтальной скорости". См. статью "Космическая ошибка: 370.000.000 $ за Integer overflow".


Вторая причина. При использовании смешанной арифметики можно получить проблемы, работая даже с маленькими массивами. Рассмотрим пример кода, который работоспособен в 32-битном варианте и неработоспособен в 64-битном:


int A = -2;
unsigned B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B);   // Invalid pointer value on 64-bit platform
printf("%i\n", *ptr);  // Access violation on 64-bit platform

Давайте проследим, как происходит вычисление выражения ptr + (A + B):


  1. Согласно правилам языка C++, переменная A типа int приводится к типу unsigned;
  2. Происходит сложение A и B. В результате мы получаем значение 0xFFFFFFFF типа unsigned;
  3. Вычисляется выражение ptr + 0xFFFFFFFFu.

Что из этого выйдет, будет зависеть от размера указателя на данной архитектуре. Если сложение будет происходить в 32-битной программе, то данное выражение будет эквивалентно ptr — 1, и мы успешно распечатаем число "3". В 64-битной программе к указателю честным образом прибавится значение 0xFFFFFFFFu. Указатель окажется далеко за пределами массива, и при доступе к элементу по данному указателю нас ждут неприятности.


Если вас заинтересовала эта тема и вы хотите лучше разобраться в ней, то рекомендую следующие материалы:


  1. 64-битные уроки. Урок 13. Паттерн 5. Адресная арифметика;
  2. 64-битные уроки. Урок 17. Паттерн 9. Смешанная арифметика;
  3. Что такое size_t и ptrdiff_t.

Вредный совет N9. Глобальные переменные


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


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


Глобальные константные переменные не в счёт. Собственно, они никакие не "переменные", а просто константы :).


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


  1. Stack Overflow. Are global variables bad?
  2. Global Variables Are Bad.
  3. Глобальные состояния: зачем и как их избегать.
  4. Why (non-const) global variables are evil.
  5. The Problems with Global Variables.

Ну и для того, чтобы было понятно, что всё это серьезно, предлагаю познакомиться со статьёй "Toyota: 81 514 нарушений в коде". Одна из причин, что код получился запутанным и забагованным, — это использование 9000 глобальных переменных.


Вредный совет N10. abort в библиотеках


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


Иногда в программах можно встретить очень простую обработку ошибок: завершение работы программы. Чуть что-то не получилось, например открыть файл или выделить память, как тут же вызывается функция abort, exit или terminate. Для некоторых утилит и простых программ это вполне приемлемое поведение. Да и вообще, автор программы сам вправе решить, что делать в случае сбоя в работе приложения.


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


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


А что если библиотекой захочет воспользоваться embedded-разработчик? Такие руководства для разработчиков встраиваемых систем, как MISRA и AUTOSAR, вообще запрещают вызывать функции abort и exit (MISRA-C-21.8, MISRA-CPP-18.0.3, AUTOSAR-M18.0.3).


Об этой мини-книге


Автор: Карпов Андрей Николаевич. E-Mail: karpov [@] viva64.com.


Более 15 лет занимается темой статического анализа кода и качества программного обеспечения. Автор большого количества статей, посвящённых написанию качественного кода на языке C++. С 2011 по 2021 год удостаивался награды Microsoft MVP в номинации Developer Technologies. Один из основателей проекта PVS-Studio. Долгое время являлся CTO компании и занимался разработкой С++ ядра анализатора. Основная деятельность на данный момент — управление командами, обучение сотрудников и DevRel активность.


Ссылки на полный текст:



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

Теги:
Хабы:
Всего голосов 13: ↑12 и ↓1+11
Комментарии16

Публикации

Информация

Сайт
pvs-studio.com
Дата регистрации
Дата основания
2008
Численность
31–50 человек
Местоположение
Россия