60 антипаттернов для С++ программиста, часть 7 (совет 31 — 35)
Перед вами обновлённая коллекция вредных советов для C++ программистов, которая превратилась в целую электронную книгу. Всего их 60, и каждый сопровождается пояснением, почему на самом деле ему не стоит следовать. Всё будет одновременно и в шутку, и серьёзно. Как бы глупо ни смотрелся вредный совет, он не выдуман, а подсмотрен в реальном мире программирования.
Я буду публиковать советы по 5 штук, чтобы не утомить вас, так как мини-книга содержит много интересных отсылок на другие статьи, видео и т. д. Однако, если вам не терпится, здесь вы можете сразу перейти к её полному варианту:»60 антипаттернов для С++ программиста». В любом случае желаю приятного чтения.
Вредный совет N31. Всё в h-файлах
Побольше кода в заголовочных файлах, ведь так гораздо удобнее, а время компиляции возрастает очень незначительно.
В эпоху моды на header-only библиотеки этот совет не кажется таким уж и вредным. В конце концов, существует даже «A curated list of awesome header-only C++ libraries».
Но одно дело — маленькие библиотеки. А другое — большой проект, в который вовлечены десятки людей и который развивается многие годы. В какой-то момент время компиляции вырастет с минут до часов, а сделать что-то с этим уже будет сложно. Не рефакторить же сотни и тысячи файлов, перенося реализацию функций из *.h в cpp-файлы? А если рефакторить, то не проще ли сразу было делать нормально? :)
Самым плохим следствием размещения реализации функций в заголовочных файлах является то, что минимальная правка приводит к необходимости перекомпиляции большого количества файлов в проекте. Есть важное отличие между кодом в header-only библиотеках и кодом вашего проекта. Код в библиотеках вы не трогаете, а свой код вы постоянно правите!
Дополнительная полезная ссылка: Pimpl.
Вредный совет N32. Оператор goto
Злые языки говорят, что goto считается вредным оператором, но это чушь. Этот оператор очень мощен и даже позволяет отказаться от for, while, do. Да здравствует goto и аскетизм!
Использование оператора goto провоцирует усложнение кода для понимания. Код, пронизанный операторами goto, сложно читать сверху вниз. Особенно если присутствуют переходы снизу вверх. Придётся «скакать» по меткам, чтобы понять, как устроена логика программы. Чем больше функция и чем больше в ней используется операторов goto, тем сложнее разобраться.
Есть даже специальный термин: спагетти-код. Цитата из Wikipedia:
Спагетти-код — плохо спроектированная, слабо структурированная, запутанная и трудная для понимания программа, содержащая много операторов GOTO (особенно переходов назад), исключений и других конструкций, ухудшающих структурированность. Самый распространённый антипаттерн программирования.Спагетти-код назван так, потому что ход выполнения программы похож на миску спагетти, то есть извилистый и запутанный. Иногда называется «кенгуру-код» (kangaroo code) из-за множества инструкций «jump».
Спагетти-код может быть отлажен и работать правильно с высокой производительностью, но он крайне сложен в сопровождении и развитии. Правка спагетти для добавления новой функциональности иногда несёт такой огромный потенциал внесения новых ошибок, что рефакторинг становится неизбежным.
Естественно, виноват не сам по себе оператор goto, а его необдуманное использование. Если он не виноват, почему существует рекомендация вообще его не использовать? Ведь в C++ почти любая конструкция опасна :).
Дело в том, что без этого оператора можно вполне обойтись. Более того, без goto код обычно выглядит и читается лучше.
Вообще сложно даже привести какие-то примеры уместного использования goto. В голову приходит только паттерн с одной точкой выхода, который мы рассматривали в главе N19. Все goto осуществляют переход на одну метку, после которой следует освобождение памяти и возврат из функции кода ошибки. Такой код не создаёт сложности для понимания. Однако дальше показано, что ещё лучше использовать умные указатели. Получается, что goto опять-таки не нужен.
Вредный совет N33. enum’ы не нужны
Никогда не используйте enum’ы, они все равно неявно приводятся к int. Используйте int напрямую!
Язык C++ идёт в сторону более сильной типизации. Поэтому, например, появился enum class. См. дискуссию «Why is enum class preferred over plain enum?».
Вредный же совет, наоборот, призывает вернуться к ситуации, когда легко запутаться в типах данных и случайно использовать не ту переменную или не ту константу.
Даже использование обыкновенных enum вместо безликого int позволяет анализатору PVS-Studio выявлять вот такие аномалии в коде.
Вредный совет N34. Везде нужен некий константный экземпляр класса? Для удобства объявите его в заголовочном файле
Бывает, что повсеместно нужен какой-то глобальный константный объект. Например, экземпляр пустой строки. Для удобства разместите его в заголовочном файле, где объявлен ваш класс строки.
В итоге получается что-то такое:
// MySuperString.h
class MySuperString {
char *m_buf;
size_t m_size, m_capacity;
public:
MySuperString(const char *str);
....
};
const MySuperString GlobalEmptyString("");
Беда в том, что GlobalEmptyString вовсе не глобальный константный объект, существующий в одном экземпляре.
При включении такого заголовочного файла через #include произойдёт создание множественных копий объекта. Это приведёт к пустой трате памяти и времени для создания множества пустых строк.
В общем случае, если в классе есть конструктор, его код выполнится при каждом включении заголовочного файла, что может привести к нежелательным побочным эффектам.
Чтобы избежать множественного создания объектов, можно объявить переменную как inline (начиная с C++17) или extern. В этом случае инициализация и вызов конструктора произойдёт один раз. Исправленный вариант:
inline const MySuperString GlobalEmptyString("");
Более подробно данная тема рассмотрена в статье «What Every C++ Developer Should Know to (Correctly) Define Global Constants».
P.S. Анализатор PVS-Studio предостережёт вас от описанной ошибки с помощью диагностики V1043.
Вредный совет N35. Объявление переменных в начале функции
Проявите немного уважения к программистам прошлого — объявляйте все переменные в начале функций. Это традиция!
Переменную лучше всего объявлять как можно ближе к месту её использования. Ещё лучше, когда переменная сразу инициализируется при объявлении. Преимущества:
- Сразу видно, какой тип имеет переменная, что облегчает понимание программы;
- Если переменная «тяжёлая» и используется только при выполнении какого-то условия, то можно улучшить производительность, создавая её только в случае необходимости. См. также V821;
- Сложнее опечататься и использовать не то имя переменной.
Конечно, нужно действовать осмысленно. Например, в случае циклов иногда для производительности лучше создавать и инициализировать переменную вне цикла. Примеры: V814, V819.
Об этой мини-книге
Автор: Карпов Андрей Николаевич. E-Mail: karpov [@] viva64.com.
Более 15 лет занимается темой статического анализа кода и качества программного обеспечения. Автор большого количества статей, посвящённых написанию качественного кода на языке C++. С 2011 по 2021 год удостаивался награды Microsoft MVP в номинации Developer Technologies. Один из основателей проекта PVS-Studio. Долгое время являлся CTO компании и занимался разработкой С++ ядра анализатора. Основная деятельность на данный момент — управление командами, обучение сотрудников и DevRel активность.
Ссылки на полный текст:
Подписывайтесь на ежемесячную рассылку, чтобы не пропустить другие публикации автора и его коллег.