1053_60_cpp_antipatterns_ru/image2.png


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


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


Вредный совет N46. Тренируйся на работе для IOCCC


Пишите код так, как будто его будет читать председатель жюри IOCCC и он знает, где вы живёте (чтоб приехать и вручить вам приз).


Это отсылка к фразе "Пишите код так, как будто поддерживать его будет склонный к насилию психопат, который знает, где вы живёте". Фраза сказана Джоном Ф. Вудсом, но ошибочно приписывается Стивену Макконнеллу, так как он упоминает её в своей книге "Совершенный код".


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


IOCCC (International Obfuscated C Code Contest) — конкурс программирования, в котором задачей участников является написание максимально запутанного кода на языке C с соблюдением ограничений на размер исходного кода.


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


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


Вредный совет N47. Развлекайся, оформляя код


Если в C++ переносы строк и отступы незначимые, то почему бы не оформить код в виде зайчика или белочки?


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


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


Главное не вредить самому коду, не перебарщивать, не оскорблять кого-то.


Встретив в коде дракона, я отнесусь к этому нормально. И буду благодарен, что автор обратил моё внимание на что-то таким необычным способом.


//        .==.        .==.          
//       //`^\\      //^`\\         
//      // ^ ^\(\__/)/^ ^^\\        
//     //^ ^^ ^/6  6\ ^^ ^ \\       
//    //^ ^^ ^/( .. )\^ ^ ^ \\      
//   // ^^ ^/\| v""v |/\^ ^ ^\\     
//  // ^^/\/ /  `~~`  \ \/\^ ^\\    
//  -----------------------------

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


Вредный совет N48. У каждого свой стиль


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


Ладно-ладно, два предыдущих совета были слишком эксцентричны. Что по поводу адекватного самовыражения в именовании сущностей и оформлении кода?


Этого не стоит делать по следующим причинам:


  1. Собственный код, написанный в привычном индивидуальном стиле проще читать и понимать. Вот только над проектами трудятся команды. В результате всем будет постоянно тяжело, так как придётся работать с кодом, написанным по-разному. Пустая трата времени и умственных усилий. Если выбрать один стиль, то вначале все тоже будут испытывать дискомфорт. Но постепенно к стилю привыкнут, и он станет удобным для чтения и написания. Повысится общая производительность команды.
  2. Конфликт стилей. Часто нужно поправить только какой-то фрагмент кода. Если каждый делает это на свой лад, то код будет выглядеть как лоскутное одеяло. Получается, что вообще нет никакого стиля, сплошная анархия. Если же переделывать чужой код, с которым приходится работать, под себя, то это будет отнимать много времени. С точки зрения автора первоначального кода, это вообще вредное действие. В общем, это ненужная почва для личностных конфликтов. Конечно, правя чужой код, можно подстраиваться под стиль, в котором он написан. Но, во-первых, это сложно, а во-вторых, ты всё равно точно не знаешь, какой именно стиль у твоего коллеги.
  3. То, что свой стиль нравится человеку, вовсе не значит, что он хороший. Есть множество приёмов, когда написание кода определённым образом помогает избежать ошибок. Некоторые из таких приёмов я описывал в мини-книге "Главный вопрос программирования, рефакторинга и всего такого". Поэтому полезно опытными членами команды совместно разработать и внедрить единый для всех стандарт кодирования.

Надеюсь, я был убедителен. Очень полезно в команде иметь стандарт кодирования. За основу можно взять, например Google C++ Style Guide или любой другой. Затем адаптировать для себя. Дополнительным источником для вдохновения могут стать:


  • уже не раз упомянутая здесь книга "Совершенный код" С. Макконнелла (ISBN 978-5-7502-0064-1);
  • Ален И. Голуб. "Веревка достаточной длины, чтобы… выстрелить себе в ногу." Мне лично она не очень понравилась, но я не могу про неё не вспомнить. Это классика;
  • Standard C++ Foundation. FAQ: Coding Standards.

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


  1. clang-format;
  2. Best C++ Code Formatter/Beautifier;
  3. Are there any lint tools for C and C++ that check formatting?
  4. Is there any tool to standardize format of C++ code?

Вредный совет N49. Перегрузи всё


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


Перегрузка операторов — это более изящный вызов функций. Например, она позволяет использовать операторы + и = для строк. Это помогает писать изящный код конкатенации строк:


result = str1 + str2;

Согласитесь, это более красиво, чем:


result.assign(str1.strcat(str2));

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


Пусть A, B, C — это экземпляры класса матрицы. Тогда следующий код очевиден без пояснений:


A = B * C;

Другое дело:


A = B && C;

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


P.S. Кстати, однажды Бьёрн Страуструп в качестве шутки предложил разрешить перегружать пробел: Generalizing Overloading for C++2000.


Вредный совет N50. Не верь в эффективность std::string


Универсальный std::string – это неэффективно. Можно ловчее и эффективнее использовать realloc, strlen, strncat и так далее.


То, что производительность программы можно существенно увеличить, отказавшись от класса std::string, является мифом. Но этот миф имеет под собой основание.


Дело в том, что раньше распространённые реализации std::string действительно оставляли желать лучшего. Так что, пожалуй, мы имеем дело даже не с мифом, а с устаревшей информацией.


Поделюсь собственным опытом. Начиная с 2006 года, мы разрабатываем статический анализатор PVS-Studio. В 2006 году он назывался Viva64, но это не имеет значения. Изначально в анализаторе мы широко использовали как раз стандартный класс std::string.


Шло время. Анализатор развивался, в нём появлялось всё больше диагностик, и с каждой версией он работал всё медленнее :). Пришло время задуматься над оптимизацией кода. Одним из узких мест, на которое указал профилировщик, оказалась работа со строками. И тогда настало время цитаты "в любом проекте рано или поздно появляется свой собственный класс строки". К сожалению, я не помню, ни откуда эта цитата, ни момент, когда именно это произошло. Кажется, это был 2008 или 2009 год.


Анализатор в процессе своей работы создаёт большое количество пустых или очень коротких строк. Мы написали свой собственный класс строки vstring, эффективно выделяющий память для таких строк. С точки зрения публичного интерфейса наш класс повторял std::string. Собственный класс строки повысил скорость работы анализатора приблизительно на 10%. Крутое достижение!


Этот класс строки служил нам долгие годы, пока на конференции C++ Russia 2017 я не побывал на докладе Антона Полухина "Как делать не надо: C++ велосипедостроение для профессионалов". В нём он рассказал, что уже много лет класс std::string хорошо оптимизирован. А те, кто использует свой собственный класс строки, являются ретроградами и динозаврами :).



Антон разобрал в своём докладе, какие оптимизации сейчас используются в классе std::string. Например, из самого простого – про конструктор перемещения. Меня же больше всего заинтересовала Small String Optimization.


Оставаться динозавром не хотелось. Мы провели эксперимент по обратному переходу с самодельного класса vstring к std::string. Для начала мы просто закомментировали класс vstring и написали using vstring = std::string;. Благо после этого потребовались незначительные правки кода в других местах, так как интерфейсы класса всё ещё почти полностью совпадали.


И как изменилось время работы? Никак! Это значит, что для нашего проекта универсальный std::string стал точно так же эффективен, как наш собственный специализированный класс, спроектированный нами с десяток лет назад. Здорово! Можно убрать один велосипед из кодовой базы проекта.


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


Во-первых, обработка С-строк (нуль-терминированных строк) провоцирует частое вычисление их длины. Не храня отдельно размер строк, сложно написать высокопроизводительный код. А если длина хранится, то мы вновь приближаемся к аналогу строкового класса.


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


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


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


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


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



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

Комментарии (10)


  1. vadimr
    26.06.2023 10:12
    +1

    Вообще не въезжаю, почему A=B*C с матрицами понятно, а A=B&&C с матрицами непонятно. Если на то пошло, то как раз A=B*C методом перегрузки вызывает вопросы, так как это может быть матричное умножение, а может – поэлементное. А с конъюнкцией что неясно? Такого в языках, допускающих элементарные операции с массивами, в программах полно. Просто конвейеризованная логическая операция.

    Конечно, можно криво перегрузить, но это другой вопрос. Умножение тоже можно криво перегрузить.


    1. Gay_Lussak
      26.06.2023 10:12

      Как раз операция A=B*C в контексте работы с матрицами должна четко ассоциироваться с матричным умножением. Если человеку кажется иное, то тут вопрос скорее к человеку, чем к коду.


      1. vadimr
        26.06.2023 10:12
        +2

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

        Матричное умножение так записывается обычно в математических пакетах вроде Матлаба.


        1. Gay_Lussak
          26.06.2023 10:12

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


  1. tandzan
    26.06.2023 10:12

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


    1. vadimr
      26.06.2023 10:12
      +1

      Кросплатформенно невозможно работать с путями, так как сами пути не кроссплатформенны.

      Необходимо уточнить задачу.


    1. Sklott
      26.06.2023 10:12
      +2

      Начиная с C++17 есть std::filesystem::path. Если этого не достаточно, то надо или искать подходящую библиотеку или писать самому.

      Трудно рекомендовать что-то конкретное, не зная конкретных требований.


  1. kchhay
    26.06.2023 10:12

    По поводу std::string можно было бы добавить про std::string_view. Штука, которая немного повышает читаемость и очень сильно снижает вероятность ошибки при использовании, в сравнении с const std::string&.


    1. sv_911
      26.06.2023 10:12

      А как его использовать? Обычно, протащив string_view через 15 вложенных функций замечаеш, что у 16 функции параметр const std::string&, и приходится конвертировать это в строку, выполняя лишнюю аллокацию памяти и копирование.

      Хотя, может на данный момент большинство кода написано со string_view, и этой проблемы больше не возникает, но раньше было именно так


    1. Sklott
      26.06.2023 10:12

      К сожалени, из опыта попыток использования std::string_view, не очень оно и подходит. В большинстве случаев оказывается что либо чего-то не хватает (например конкатенации двух sv) без предварительной конвертации в std::string, либо в какой-то очередной API надо передать именно string. Единственное где sv сейчас применять удобно, это всякого рода парсинг, когда можно сделать remove_prefix() и/или remove_suffix() без реаллокации.