
Ценность квалифицированного программиста смещается в сторону умения проводить обзоры кода. Генерировать код становится проще, но всё так же важно проверять его с точки зрения качества декомпозиции, корректности реализации, эффективности, безопасности. Посмотрим на примере маленького проекта markus, созданного с помощью Claude Opus, почему важно понимать сгенерированный код и уметь видеть, что скрывает красивый текст программы.
Проект markus
Я искал какой-то проект, сгенерированный с помощью ИИ, и мне попался markus.
Это single-header библиотека для парсинга текста и конвертации его в HTML. Создана с помощью Claude Opus 4.5.
Мне понравилось, что библиотека крошечная. Можно её просмотреть и даже погрузиться в изучение отдельных фрагментов кода. Если перед вами сразу много вайб-кода, то как-то даже не хочется начинать его смотреть.
Библиотека состоит из одного файла markus.h, содержащего 6484 строки на C++20. По существу кода ещё меньше. Некоторые алгоритмы в целях оптимизации используют табличные методы. Четыре таблицы представлены в коде бессмысленно вытянуто. Пример:
static constexpr uint8_t kWhitespaceTable[256] = { 0, 0, 0, 0, .... // и так 256 строк
Если таблицы не считать, то, собственно, кода ещё на 1000 строк меньше. Анализатор PVS-Studio ничего особенного в проекте не нашёл, но проверка стала поводом посмотреть некоторые фрагменты поподробнее. И вот уже специалист, то есть я, могу выделить моменты, которые стоит обсудить.
Проблематика вайб-кодинга
Генерация кода с помощью ИИ может очаровать, особенно неискушённого программиста, который не работал с большими старыми проектами.
Кто сталкивался, тот понимает, что главная задача — не написать код, а сделать так, чтобы его можно было сопровождать и развивать. Об этом хорошо написал Роберт Мартин:
Без присмотра AI будет нагромождать код поверх кода. У него нет чувства большой картины. Архитектура, вероятно, за пределами его возможностей.
Так что изучение принципов декомпозиции, умение правильной постановки задач и т. д. становятся только более актуальной компетенцией программиста.
Но я хочу посвятить статью другой особенности вайб-кодинга, про которую мало говорят: это очарование процессом генерации кода и, как следствие, излишнее доверие к нему. Редкое исключение — заметка “ИИ пишет код быстрее. Но не понимает, что пишет”.
Убедившись, что простые задачи решаются хорошо или даже лучше, чем это бы сделали вы сами, теряется критичность взгляда. Кажется, что и в больших задачах качество будет высоким, код оптимизированным и т. д.
За примером далеко ходить не надо. Я ради интереса просил ИИ написать код для быстрого подсчёта количества единичных бит в больших массивах. Я был очарован предложенными алгоритмами. Про многие интересные решения я даже не знал, хотя в своё время прочитал в книге разбор целого ряда алгоритмов на эту тему.
Смотришь на предложенный код подсчёта бит и думаешь: “А не пора ли податься в фермеры?” Сам бы я такой код не написал и не догадался бы его поискать, т. к. просто не знаю про такие алгоритмы.
Это восхищение проецируется на весь код, созданный ИИ. Когда я только заглянул в код markus, то подумал, что анализировать его смысла нет: всё красиво, оптимизированные алгоритмы и т. д:
Тонкая настройка оптимизации компилятора с помощью подсказок наиболее вероятных веток исполнения [[unlikely]];
Использование условной компиляции на основе
if constexpr;Использование оптимизированных табличных алгоритмов;
Генерация SIMD-Friendly Helper Functions;
Использование контейнеров из пространства имён
std::pmrдля задания собственной стратегии выделения памяти.
Код солиден, что там смотреть? Оказывается, есть что. Ощущение, что код оптимизирован, — ложное. Вернее, местами он действительно оптимизирован, а местами всё плохо.
К чести автора проекта хочу сказать, что он молодец и взвешенно подходит к описанию проекта. Райан явно предупреждает:
DO NOT USE IN PRODUCTION. This is completely vibe coded and has not undergone any reviews for memory safety. Its performance is also 2-3x slower than cmark.
Какую проблему я хочу подсветить статьёй? Код может выглядит педантичным, оптимизированным и безопасным, но таковым не является.
Хорошо, что работу проекта markus можно сравнить с другим решением и понять, что он получился не очень быстрым. А если сравнивать не с чем? Нужно экспертное мнение человека. Беда в том, что многим кажется, что теперь эксперт и обзор кода не нужны.
Нужен ли эксперт?
А может быть, оценка кода экспертом действительно не нужна?
Вы создали код. Он выглядит красиво и опрятно. Он выглядит оптимизированным и работает. Может, этого достаточно?
На мой взгляд, без оценки можно обойтись, если выполняются все четыре условия:
Никто не пострадает и не понесёт убытки, если код работает не так, как задумывалось, из-за багов или неправильной интерпретации ТЗ.
Код безответственный в плане безопасности. Никто не будет его ломать, так как это никому не нужно.
Скорость работы кода неважна.
Не предполагается, что у проекта будет долгий жизненный цикл. Его не потребуется развивать десятилетиями, сопровождать, адаптировать для других платформ и т. д.
Во всех остальных случаях, извините, все требования к проверке качества кода в силе и ещё более актуальны. К вайб-коду должны применяться все те же процессы, что описываются в SSDLC / стандарте РБПО.
Почему я строг к ИИ
Я не строг, а требователен. Для реализации проекта взят язык С++. Суть использования C++ — построение быстрых, эффективных приложений, экономно использующих память. Если вам не нужна быстрая программа, не надо использовать C++.
Раз уж выбран C++ для создания быстрого приложения, то задача именно написать быстрый код. Неважно, пишет его человек или это делает ИИ. Если быстрый код не получился — задача не решена.
Мой личный интерес — изучать, что такое вайб-код и как статические анализаторы могут помочь контролировать его надёжность. Вайб-коду, как и людям, свойственно антипаттерны, но другие. Есть смысл развивать анализатор PVS-Studio с учётом этих особенностей.
Попутно, раз я всё равно смотрю проекты, есть повод написать статью. У меня нет задачи ругать вайб-код и в целом вайб-кодинг. Это глупо, это просто инструмент. Но в отличие от евангелистов ИИ у меня нет задачи продвигать эти технологии, поэтому если я вижу, что код — хрень, я так и напишу.
SIMD-friendly код
Комментарий к функции IsSpanBlank обещает, что код составлен из расчёта облегчить компилятору задачу его векторизации, например, с помощью SSE-инструкций.
Суть оптимизации — несколько элементов обрабатываются одновременно, чтобы определить, хранят ли они пробельные символы.
// Check if a span contains only blank characters (space, tab, \r, \n) // SIMD-friendly: processes 8 bytes at a time inline bool IsSpanBlank(const char* data, size_t len) { size_t i = 0; // Process 8 bytes at a time for (; i + 8 <= len; i += 8) { // For each byte, check if it's NOT a blank character // Blank chars: space(0x20), tab(0x09), \n(0x0A), \r(0x0D) bool all_blank = true; for (size_t j = 0; j < 8; ++j) { unsigned char c = static_cast<unsigned char>(data[i + j]); bool is_blank = (c == ' ' || c == '\t' || c == '\n' || c == '\r'); all_blank = all_blank && is_blank; } if (!all_blank) return false; } // Handle remaining bytes for (; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true; }
Код создаёт впечатление, что ИИ знает, что делает. Внутренний цикл всегда обрабатывает по 8 символов, и предполагается, что эта часть может быть оптимизирована с помощь SIMD-инструкций. Оставшийся хвост символов обрабатывается отдельным простым циклом.
Мне этот код показался подозрительным тем, что на самом деле байты обрабатываются всё так же последовательно. Вложенный цикл не несёт чего-то полезного. На мой взгляд, этот код не лучше и не хуже простейшего варианта его реализации.
inline bool IsSpanBlank_Simple(const char* data, size_t len) { for (size_t i = 0; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true; }
Я бы написал именно такой код. Если бы я понимал, что это узкое место и нужно непременно сделать код как можно быстрее, я бы пошёл искать решение, построенное на интринсиках.
Какой-то другой вариант, например, с вложенным циклом, я бы делать не решился. Я хоть что-то и понимаю в оптимизации, но не настолько, чтобы отважиться писать хитрый код с целью подсказать компилятору.
Можно сказать: “Вот! Ты слаб! А Claude Opus взял и сделал. Взял и написал более умный вариант”.
Давайте попробуем сравнить скорость. Мой коллега Филипп провёл небольшой эксперимент с GCC и Clang:
Формируем одинаковый набор данных для обоих бенчмарков: генерируем 1 000 000 строк случайной длины от 1 до 127 символов с произвольным содержимым. Генерация наборов не учитывается в замерах.
Подаём на вход алгоритму набор данных и для каждой строки вычисляем, содержит ли она пробельные символы. Замеряем общее время.
В обоих вариантах код компилируется с агрессивными оптимизациями -O3. Можно заметить, что для обоих компиляторов простой вариант функции лучше, чем с вложенным циклом.

Быть может, просто надо какой-то волшебный ключ оптимизации указать, чтобы вайб-код оптимизировался лучше? Нет такого ключа. А если будет, компилятор и без подсказки сам сделает столь же эффективный вариант из простого кода с одним циклом.
Параллельно с коллегой тоже поэкспериментировал с двумя рассмотренными и тремя новыми вариантами алгоритма. Я использовал компилятор MSVC. Давайте посмотрим, какие результаты замеров получились.
Во-первых, мне подсказали, что ситуацию можно улучшить, заменив в изначальном варианте || на |. Short-circuit evaluation в нашем случае не помогает, а только вредит. Улучшенная проверка:
bool is_blank = (c == ' ' | c == '\t' | c == '\n' | c == '\r');
Полный код функции
inline bool IsSpanBlank_BinOR(const char* data, size_t len) { size_t i = 0; // Process 8 bytes at a time for (; i + 8 <= len; i += 8) { // For each byte, check if it's NOT a blank character // Blank chars: space(0x20), tab(0x09), \n(0x0A), \r(0x0D) bool all_blank = true; for (size_t j = 0; j < 8; ++j) { unsigned char c = static_cast<unsigned char>(data[i + j]); bool is_blank = (c == ' ' | c == '\t' | c == '\n' | c == '\r'); all_blank = all_blank && is_blank; } if (!all_blank) return false; } // Handle remaining bytes for (; i < len; ++i) { unsigned char c = static_cast<unsigned char>(data[i]); if (c != ' ' && c != '\t' && c != '\n' && c != '\r') { return false; } } return true; }
Во-вторых, я спросил DeepSeek. Он предложил вариант оптимизации, построенный на битовых операциях:
inline bool IsSpanBlank_Deepseek(const char* data, size_t len) noexcept { // Создаём битовую маску для допустимых символов // Используем 128-битную маску для ASCII static constexpr uint64_t blank_mask_low = []() { uint64_t mask = 0; mask |= 1ULL << ' '; mask |= 1ULL << '\t'; mask |= 1ULL << '\n'; mask |= 1ULL << '\r'; return mask; }(); for (size_t i = 0; i < len; ++i) { unsigned char uc = static_cast<unsigned char>(data[i]); if (uc >= 64) return false; // Все blank-символы в ASCII < 64 if (!((blank_mask_low >> uc) & 1)) return false; } return true; }
И напоследок я попросил у DeepSeek оптимизированную функцию, по-настоящему использующую векторизацию. Вот что получилось:
inline bool IsSpanBlank_Deepseek_SSE(const char* data, size_t len) noexcept { // Создаём векторы с допустимыми символами const __m128i space = _mm_set1_epi8(' '); const __m128i tab = _mm_set1_epi8('\t'); const __m128i newline = _mm_set1_epi8('\n'); const __m128i carriage = _mm_set1_epi8('\r'); size_t i = 0; // Обрабатываем по 16 байт for (; i + 15 < len; i += 16) { __m128i chunk = _mm_loadu_si128(reinterpret_cast<const __m128i*>(data + i)); // Проверяем, что каждый байт является одним из допустимых __m128i eq_space = _mm_cmpeq_epi8(chunk, space); __m128i eq_tab = _mm_cmpeq_epi8(chunk, tab); __m128i eq_nl = _mm_cmpeq_epi8(chunk, newline); __m128i eq_cr = _mm_cmpeq_epi8(chunk, carriage); __m128i is_blank = _mm_or_si128(_mm_or_si128(eq_space, eq_tab), _mm_or_si128(eq_nl, eq_cr)); // Получаем маску и проверяем int mask = _mm_movemask_epi8(is_blank); if (mask != 0xFFFF) return false; } // Обрабатываем остаток for (; i < len; ++i) { char c = data[i]; if (c != ' ' && c != '\t' && c != '\n' && c != '\r') return false; } return true; }
Теперь результаты. Хотя 32-битные сборки сейчас неактуальны, ради интереса я и её запустил. Замеры выполнялись на массиве размером 212 мегабайт, заполненном случайными пробельными символами. Выполнение потока привязывается к одному предварительно прогретому ядру. Если непонятно, о чём идёт речь, загляните в статью “Вечный вопрос измерения времени”.
Win32: 1. Самый простой вариант с одним циклом: 1.39 сек. 2. Изначальный "оптимизированный код" с вложенным циклом: 1.86 сек. 3. В изначальном варианте операторы || заменены на | : 0.351 сек. 4. Вариант от Deepseek с оптимизацией на битовых масках: 0.817 сек. 5. Вариант от Deepseek с оптимизацией на SSE2 интристиках: 0.0449 сек. Win64: 1. Самый простой вариант с одним циклом: 0.175 сек. 2. Изначальный "оптимизированный код" с вложенным циклом: 0.309 сек. 3. В изначальном варианте операторы || заменены на | : 0.253 сек. 4. Вариант от Deepseek с оптимизацией на битовых масках: 0.207 сек. 5. Вариант от Deepseek с оптимизацией на SSE2 интристиках: 0.0396 сек.
Хорошо заметно, что 64-битный код получился намного эффективнее, кроме случая, где в ход идёт SSE2, но и там в любом случае всё быстро.
Код, сгенерированный Claude Opus для проекта markus, худший из всех вариантов.
Даже самый простой вариант с обыкновенным циклом не только быстрее, но ещё и короче. Дополнительные строки кода пошли только во вред.
В лидерах код на интристиках, но платой тому — ограничение переносимости.
Кто-то скажет, что я зря переживаю. Да, Claude Opus выбрал неудачно решение, но ведь DeepSeek предложил вполне хорошее, быстрое и переносимое решение.
Согласен. Вот только в проекте мы видим неудачный код. Код выглядит оптимизированным, но таковым не является. Задача не решена. Нужен эксперт, который это поймёт и выберет другое решение.
В будущем проблема будет нарастать. Сгенерированного кода будет всё больше, а людей, которые могут делать обзор кода, останется столько же. Затем системы начнут учиться на таком же “оптимизированном” коде... Ох, интересные времена нас ждут.
Сравнение строк: цветочки
Моё внимание привлекло предупреждение PVS-Studio: V590 [CWE-571] Consider inspecting this expression. The expression is excessive or contains a misprint. markus.h 5987
Действительно, код содержит избыточную проверку:
BlockNode ParseParagraph() { .... auto trimmed = detail::TrimLeft(line); // trimmed имеет тип std::string_view .... if (trimmed.size() >= 2 && trimmed[1] == '!' && trimmed.size() >= 9) { .... }
Тут, на самом деле, ничего страшного. Ну лишняя проверка, которую и так выкинет компилятор. Для красоты код, конечно, стоит поправить, но не более того:
if (trimmed.size() >= 9 && trimmed[1] == '!') // вот теперь хорошо
Однако глаза скользнули на код ниже if. И вот там избыточность, которая достойна вашего внимания:
std::pmr::string upper; for (size_t j = 0; j < 9; ++j) { upper += static_cast<char>( std::toupper(static_cast<unsigned char>(trimmed[j]))); } if (upper.starts_with("<!DOCTYPE")) { goto html_interrupt; }
Здесь выполняется проверка, что строка начинается с "<!doctype", причём без разницы, используются строчные буквы или заглавные. Чтобы игнорировать верхний/нижний регистр, создаётся временная строчка в верхнем регистре и уже она сравнивается с шаблоном.
Так себе решение с точки зрения эффективности. Создание временной строки — это тяжёлая операция, несмотря на то, что используется std::pmr::string. Всё равно ведь надо символы копировать в память, да ещё по одному. Кстати, здесь простой std::string будет не хуже, так как строка не более 15 символов и сработает Small String Optimization.
Лучше написать функцию, выполняющую сравнение без учёта регистра. Например, такую:
bool starts_with_insensitive(std::string_view str, std::string_view prefix_lower_case) { if (str.size() < prefix.size()) return false; const auto pred = [](unsigned char lhs, unsigned char rhs) { return lhs == std::tolower(rhs); }; return std::equal(prefix_lower_case.begin(), prefix_lower_case.end(), str.begin(), pred); }
Это просто пример. Можно реализовать алгоритм по-другому или сделать istring_view, написав свои char_traits, и так далее. Главное — мы избавляемся от создания временной строки и упрощаем код, выполняющий проверку:
if (starts_with_insensitive(trimmed, "<!doctype")) { goto html_interrupt; }
Ещё ярче необходимость создания функции сравнения станет видна в следующей главе. Как говорится, это только цветочки, а ягодки будут впереди.
Сравнение строк: ягодки
Просматривая другие предупреждения PVS-Studio, я обратил внимание на этот вектор строк:
std::optional<HtmlBlock> TryParseHtmlBlock() { .... static const std::pmr::vector<std::pmr::string> type1_tags = { "script", "pre", "style", "textarea" }; .... }
Анализатору он не нравится: V1096 The ‘type1_tags’ variable with static storage duration is declared inside the inline function with external linkage. This may lead to ODR violation. markus.h 4781
Нет смысла рассматривать это предупреждение в статье. Куда серьёзнее то, как работают с этим вектором. Анализатор не может оперировать высокоуровневыми понятиями и заметить неладное, зато это может заметить человек и сказать всё, что думает про эту дичь.
Код ниже проверяет наличие этих тегов в строке. Но ведь в теге ещё есть открывающая угловая скобка <, поэтому ИИ, не стыдясь, создаёт эти временные строки, содержащие тег со скобкой:
for (const auto& tag : type1_tags) { std::pmr::string open_tag = "<" + tag;
Далее нужно проверить наличие этого тега в строке без учёта регистра. Собственно, почему бы для этого не создать ещё одну временную строчку. Вайб-код генерировать — не мешки ворочать.
std::pmr::string lower; for (size_t j = 0; j < open_tag.size(); ++j) { lower += static_cast<char>( std::tolower(static_cast<unsigned char>(trimmed[j]))); }
Весь этот кринж выглядит следующим образом:
static const std::pmr::vector<std::pmr::string> type1_tags = { "script", "pre", "style", "textarea" }; for (const auto& tag : type1_tags) { std::pmr::string open_tag = "<" + tag; if (trimmed.size() >= open_tag.size()) { std::pmr::string lower; for (size_t j = 0; j < open_tag.size(); ++j) { lower += static_cast<char>( std::tolower(static_cast<unsigned char>(trimmed[j]))); } if (lower == open_tag) { // .... } } }
В худшем случае (если тега в тексте нет), создаётся восемь временных строк! Ситуацию лишь немного спасает то, что формируемые строки небольшие по размеру, и Small String Optimization внутри std::pmr::string устранит динамические аллокации.
И ведь можно достаточно легко сделать код быстрее. Во-первых, можно сразу сформировать нужные префиксы и записать их в вектор. Во-вторых, вектор можно смело заменить на массив std::string_view, что вовсе уберёт динамические аллокации.
using namespace std::literals; static constexpr std::array type1_tags = { "<script"sv, "<pre"sv, "<style"sv, "<textarea"sv };
В-третьих, теперь можно найти префикс с помощью написанной ранее функции starts_with_insensitive. В результате весь говнокод, приведённый выше, сокращается до:
using namespace std::literals; static constexpr std::array type1_tags = { "<script"sv, "<pre"sv, "<style"sv, "<textarea"sv }; for (auto tag : type1_tags) { if (starts_with_insensitive(trimmed, tag.c_str(), tag.size()) {
Код стал в два раза короче и в N раз быстрее.
Можно и дальше улучшения делать. Например, не создавать строку с тегом в скобах end_condition = "</" + tag + ">";, а брать уже готовую из массива. Но хватит уже это место мучить.
Цена красоты
Когда я первый раз пролистывал код, моё уважение вызвал код сериализации массива объектов в строку. Всё по фэншую: разные функции обходят с помощью паттерна “визитор” массивы вариантов и формируют нужные строки. Код типобезопасный и короткий.
Функций сериализации четыре: GetAltTextFromIds, GetAltText, RenderInlines, DebugAst, но для примера приведу только одну, которая покороче:
using InlineNode = std::variant<....>; std::pmr::string GetAltText(const std::pmr::vector<InlineNode>& nodes) { std::pmr::string result; for (const auto& node : nodes) { std::visit( [&result, this](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, Text>) { result += arg.content; } else if constexpr (std::is_same_v<T, Code>) { result += arg.content; } else if constexpr (std::is_same_v<T, SoftBreak>) { result += ' '; } else if constexpr (std::is_same_v<T, HardBreak>) { result += ' '; } else if constexpr (std::is_same_v<T, Emphasis>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Strong>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Link>) { result += GetAltTextFromIds(arg.children); } else if constexpr (std::is_same_v<T, Image>) { result += arg.alt_text; } }, node); } return result; }
Выглядит элегантно. Изначально из-за этого кода я думал назвать статью как-то менее вызывающе. После рассмотрения недостатков я планировал привести этот код как положительный пример. Мол, вот здесь ИИ написал лучше, чем это сделает не очень начитанный C++ программист. Итого: местами не очень получилось, а где-то прям здорово.
Перед написанием хвалебной части мне, естественно, понадобилось посмотреть на код внимательнее и попробовать разобраться, что к чему. Зачем я только это сделал... Остался бы и дальше очарованным паттернами, ключевыми словами constexpr и кодом, красиво оформленным в столбик.
Оказывается, и здесь плохо. Сейчас и вам расскажу почему.

Итак, стоит задача хранить массив неких элементов, которые по-разному сериализуются в строки.
Простым подходом будет создать базовый класс и наследовать от него разные сущности. В контейнере тогда будут храниться указатели на него. А для сериализации понадобится в разных случаях вызывать разные виртуальные функции. Получилась бы приблизительно следующая иерархия классов:
using namespace std::literals; struct Node { virtual ~Node() = default; virtual std::string StrAltText() = 0; virtual std::string StrDebug() = 0; }; struct SoftBreak : public Node { std::string StrAltText() { return " "s; } std::string StrDebug() { return " "s; } }; struct HtmlBlock : public Node { std::string content; std::string StrAltText() { return content; } std::string StrDebug() { return std::format("HtmlBlock = {}", content); } };
Массив таких объектов превращался бы в строку как-то так:
std::string GetAltText(const std::vector<Node *>& nodes) { std::string result; for (const auto& node : nodes) result += node->StrAltText(); return result; }
Приведённый код требует ручного управления памятью, да и вообще небезопасен. Ситуацию можно улучшить, храня в векторе умные указатели, например std::unique_ptr. Другой вариант — использовать std::any.
Однако остаётся вторая проблема: код сериализации размазан по множеству классов. Можно собрать обработку в одном месте, узнавая тип объекта в массиве с помощью dynamic_cast. Это медленно.
Поэтому классическим решением будет хранить идентификатор объекта в базовом классе, чтобы быстро определять его тип:
enum NodeType { kSoftBreak, kHtmlBlock }; struct Node { const NodeType type; Node(NodeType t) : type(t) {} virtual ~Node() = default; }; struct SoftBreak : public Node { SoftBreak() : Node(kSoftBreak) {} }; struct HtmlBlock : public Node { std::string content; HtmlBlock() : Node(kHtmlBlock) {} };
Теперь обработку, хоть и не очень изящно, можно собрать в одном месте:
std::string GetAltText(const std::vector<std::unique_ptr<Node>>& nodes) { std::string result; for (const auto& node : nodes) switch(node->type) { case kSoftBreak: { // Здесь даже не требуется получать сам объект. // SoftBreak &n = *static_cast<SoftBreak *>(node); result += ' '; break; } case kHtmlBlock: { const HtmlBlock &n = static_cast<const HtmlBlock &>(*node); result += n.content; break; } } return result; }
Если хочется большего изящества, можно воспользоваться типом данных std::variant, что и сделал Claude Opus. Тип std::variant — это такой умный вариант union, безопасный с точки зрения работы типов. Вот только тут есть нюанс. Жирный такой нюанс.
Но начнём мы с того, то ИИ вообще создал странную избыточную сущность. Он завёл перечисление NodeType, которое бывает нужно для схемы, которую мы рассмотрели выше:
enum class NodeType { .... kHtmlBlock, kBlockQuote, kList, kListItem, kText, kSoftBreak, .... };
И вставил бессмысленный числовой идентификатор kType в структуры:
struct SoftBreak { static constexpr NodeType kType = NodeType::kSoftBreak; }; struct HtmlBlock { static constexpr NodeType kType = NodeType::kHtmlBlock; std::pmr::string content; int block_type = 0; // CommonMark HTML block type (1-7) };
Это перечисление и идентификатор в структурах не нужны. kType нигде не используется, благо объявлен он как static и не занимает память в каждом объекте.
Само перечисление используется в единственном месте — в функции NodeTypeToString:
// Convert NodeType to string for debugging inline std::string_view NodeTypeToString(NodeType type) { switch (type) { case NodeType::kDocument: return "Document"; case NodeType::kParagraph: return "Paragraph"; case NodeType::kHeading: return "Heading"; case NodeType::kThematicBreak: return "ThematicBreak"; .... case NodeType::kHtmlInline: return "HtmlInline"; } return "Unknown"; }
Вот только непонятно, зачем и как этой функцией пользоваться на практике. Зачем она? Чем она поможет? Кажется, никому и ничем. В общем, всё это можно удалить и сократить размер кода проекта ещё строк на 100.
Ну лишний код, и ладно — не привыкать. Дальше нас ждёт самое толстое. В прямом смысле слова.
Особенность переменной типа std::variant в том, что она должна иметь достаточный размер для размещения внутри себя наибольшей альтернативы. В нашем случае вариант должен вмещать в себя следующие классы:
using InlineNode = std::variant<Text, SoftBreak, HardBreak, Code, Emphasis, Strong, Link, Image, HtmlInline>;
Размер этих классов весьма разный, начиная с самых маленьких, таких как SoftBreak:
struct SoftBreak { static constexpr NodeType kType = NodeType::kSoftBreak; };
Размер этого класса 1 байт. Хотя класс пустой, он всё равно должен занимать хотя бы один байт.
Самый большой класс — Image:
struct Image { static constexpr NodeType kType = NodeType::kImage; std::pmr::string destination; std::pmr::string title; std::pmr::string alt_text; };
Его размер в 64-битной программе может составлять 120 байтов. Дело в том, что std::pmr::string, например, в реализации STL занимает 40 байтов. Вы уже поняли, что это означает?
Вариант ещё дополнительно хранит информацию, какой тип он содержит в текущий момент. Итого размер одного объекта типа InlineNode составляет 128 байтов! Можете здесь сами посмотреть.
Итого, программа работает с массивами, где размер каждого элемента 128 байтов. Офигительная “эффективность” расхода памяти.
Столько памяти нужно даже для того, чтобы хранить пустые классы, обозначающие разделители и превращающиеся обычно в пробелы:
} else if constexpr (std::is_same_v<T, SoftBreak>) { result += ' ';
Чтобы знать, где напечатать пробел, требуется хранить 128 байтов.
Перерасход памяти сам по себе, может, не такая уж большая проблема, но здесь ещё возникает и проблема производительности. При работе с таким растянутым массивом менее эффективно будет использоваться и кэш процессора.
Можно возразить, что вариант с умными указателями добавляет уровень косвенности при доступе к объекту, что тоже замедлит работу. И ещё нужно самому тип объекта проверять (см. выше пример со switch).
Не принимается. Косвенный доступ по указателю не существенный, учитывая, что потом всё равно будет куча обращений к памяти для чтения строк и их конкатенации. Перерасход памяти выглядит куда более существенной проблемой. Что по поводу проверки типа объекта, так она всё равно есть, просто скрыта внутри std::variant.
Ещё представьте, как болезненно происходит расширение вектора, если очередной элемент уже не влезает в зарезервированный буфер. Например, здесь:
std::pmr::vector<InlineNode> link_content; for (size_t i = opener->pos + 1; i < result.size(); ++i) { link_content.push_back(std::move(result[i])); }
При расширении вектора происходит перемещение объектов на новые места. Брр... Версия с умными указателями на базовый класс std::vector<std::unique_ptr<Node>> куда легковеснее.
Итог: перед нами вновь говнокод, поданный в красивой обёртке.
Я не говорю, что умные указатели или std::any здесь является лучшим решением. Можно что-то и другое придумать. Если задача — писать эффективный код, то это хорошее место, чтобы подумать, как хранить объекты и обходить их для сериализации.
Что ещё?
Не знаю. Я и так уже вышел за лимит времени, который собирался посвятить просмотру кода и статье.
Это ещё раз говорит о росте ценности ревьюеров. Нагенерировать код всё проще. Вопрос: где взять силы на его просмотр и вынесение вердикта, получилось ли то, что нужно? Кто возьмёт ответственность за результат? Мы всё-таки в мире C++, суть использования которого — эффективность. Если кому-то скорость вообще не нужна, что он тут забыл?
Внимательный разбор завершаю, но напоследок пройдусь по некоторым предупреждениям PVS-Studio.
Склероз
Предупреждение: V560 [CWE-571] A part of conditional expression is always true: !input.empty(). markus.h 2228
explicit LineBuffer(std::string_view input) { if (input.empty()) [[unlikely]] { return; } .... // input не модифицируется .... if (input.size() > line_start || (!input.empty() && (input.back() == '\n' || input.back() == '\r'))) { line_offsets_.push_back({line_start, input.size() - line_start}); } .... }
ИИ педантично вставляет [[unlikely]], чтобы подсказать компилятору, что это маловероятное событие. Но при этом через некоторое время забывает, что далее строка всегда непустая, и влепляет бессмысленную повторную проверку !input.empty().
С кем поведёшься, от того и наберёшься
Предупреждение: V1048 [CWE-1164] The ‘prev_line_had_content’ variable was assigned the same value. markus.h 4090
void ExtractLinkReferences(Document& doc) { .... bool prev_line_had_content = false; .... if (prev_line_had_content) { prev_line_had_content = true; // This line also has content Advance(); continue; } .... }
Переменной prev_line_had_content присваивается значение true, хотя из проверки и без того следует, что там лежит это значение. Анализатор прав — перед нами бессмысленная строчка кода. Но предупреждение неинтересное: я бы не стал его рассматривать, если бы не комментарий.
Слышали байки, что программисты иногда пишут верные, но бесполезные комментарии? Видимо, у ИИ были “хорошие” учителя. Это как раз тот случай, когда комментарий фактически пересказывает выполняемое действие, не добавляя ничего нового.
См. также "Вредный совет N53 — Отвечай в комментарии на вопрос “что?” в книге “60 антипаттернов для С++ программиста”.
Создание временных прокси объектов
Предупреждение: V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2597
std::pmr::vector<InlineNodeId> ParseInlines() { std::pmr::vector<InlineNode> result; .... result.push_back(Text(text_.substr(text_start, pos_ - text_start))); .... }
В начале создаётся временный объект типа Text, затем он помещается в контейнер. Первая мысль: использовать emplace_back:
result.emplace_back(Text(text_.substr(text_start, pos_ - text_start)));
Но изменение на emplace_back в этом случае даст примерно 0% ускорения, так ещё и нагрузит дополнительно компилятор — ему придётся делать инстанцирование шаблона функции emplace_back. Внутри него уже будет идеально передаваться созданный объект, который будет перемещён конструктором перемещения.
Здесь придётся написать более хитрый код, чтобы действительно получилось размещение сразу в контейнере. Это контейнер вариантов, значит, мы должны подсказать ему, какую именно альтернативу собираемся создавать, и передать её std::string_view.
Вот как это будет выглядеть:
result.emplace_back(std::in_place_type<Text>, text.substr(....));
В таком случае мы идеально передадим подсказку о конструируемом типе и аргументы конструктора этого типа. Нас интересует 5-я перегрузка конструктора std::variant.
При таком изменении мы избавимся от одного вызова конструктора перемещения.
По отдельности мелочь, конечно, но если таких вставок много, то почему бы и не написать сразу хорошо
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2610
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2632
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2650
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2733
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2751
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2754
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2855
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2931
V823 Decreased performance. Object may be created in-place in the ‘quote_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5239
V823 Decreased performance. Object may be created in-place in the ‘quote_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5241
V823 Decreased performance. Object may be created in-place in the ‘item_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5641
V823 Decreased performance. Object may be created in-place in the ‘item_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 5643
V823 Decreased performance. Object may be created in-place in the ‘para_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 6045
V823 Decreased performance. Object may be created in-place in the ‘para_lines’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 6047
V823 Decreased performance. Object may be created in-place in the ‘delimiter_stack’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2729
V823 Decreased performance. Object may be created in-place in the ‘delimiter_stack’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2746
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2193
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2196
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2208
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2218
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2221
V823 Decreased performance. Object may be created in-place in the ‘line_offsets_’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2229
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2637
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2947
V823 Decreased performance. Object may be created in-place in the ‘result’ container. Consider replacing methods: ‘push_back’ -> ‘emplace_back’. markus.h 2958
Копия вместо перемещения
Предупреждение: V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4670
struct Heading { static constexpr NodeType kType = NodeType::kHeading; int level = 1; // 1-6 std::pmr::vector<InlineNodeId> children; std::pmr::string raw_content; // Temporary storage for inline parsing }; std::optional<Heading> TryParseAtxHeading() { .... std::pmr::string content(trimmed.substr(i)); .... Heading heading; heading.level = level; heading.raw_content = content; // <= return heading; }
Строка в переменной content копируется, хотя её можно переместить, так как дальше она не нужна. Можно написать:
heading.raw_content = std::move(content);
А можно обойтись вообще без временного объекта heading и написать что-то типа:
return Heading { .level = level, .raw_content = std::move(content) };
Опять-таки, рассмотренное место — мелочь, но постепенно накапливаются лишние действия
V820 The ‘info_string’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4761
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 4762
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 5101
V820 The ‘content’ variable is not used after copying. Copying can be replaced with move/swap for optimization. markus.h 5821
Закат солнца вручную
Предупреждение: V837 The ‘insert’ function does not guarantee that arguments will not be copied or moved if there is no insertion. Consider using the ‘try_emplace’ function. markus.h 5274
using LinkRefMap = std::pmr::unordered_map<std::pmr::string, std::pair<std::pmr::string, std::pmr::string>>; std::optional<BlockQuote> TryParseBlockQuote() { .... LinkRefMap* parent_link_refs_ = nullptr; .... for (const auto& [label, dest_title] : nested_doc.link_references) { if (parent_link_refs_->find(label) == parent_link_refs_->end()) { parent_link_refs_->insert({label, dest_title}); } } .... }
PVS-Studio предупреждает о неэффективности, если в контейнере уже есть элемент с таким же ключом. В нашем случае защита от этого сделана вручную, но она неэффективна. В коде произойдут два поиска:
Сначала ищем элемент по ключу для проверки условия.
При вставке производим эту же операцию снова, пытаясь найти нужную позицию вставки в хэш-таблицу.
Вызов try_emplace в этой ситуации сделает ровно один поиск и вставит элемент, если его там не было:
for (const auto& [label, dest_title] : nested_doc.link_references) { parent_link_refs_->try_emplace(label, dest_title); }
Заключение
Выводы всё те же, что были в предыдущей статье “Давайте заглянем в этот самый вайб-код”. Независимо от того, как вы создаёте программный код, для обеспечения качества и надёжности разрабатываемого ПО следует использовать комплекс мер.
Цените архитекторов и разработчиков, которые умеют декомпозировать и строить модульную архитектуру.
Помните, что безопасность закладывается в систему с момента её проектирования (КИБ).
Цените разработчиков, которые могут отличить эффективный код от медленного, безопасный от опасного. Тех, кто может разобраться и принять решение, имеет ли право созданный код стать частью проекта.
Используйте статические анализаторы кода.
Используйте динамические анализаторы кода и FAST анализаторы для frontend-приложений.
Используйте инструменты композиционного анализа. В случае использования ИИ это особенно актуально, так как их галлюцинации начали применять для атак на цепочки поставок.
Используйте стандарты кодирования. Нет ничего хуже, когда код написан вразнобой.
Примените по необходимости другие практики, обобщённо называемые разработкой безопасного ПО.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Andrey Karpov. Let’s check vibe code that acts like optimized C++ one but is actually a mess.
Комментарии (15)

german25avenue
21.04.2026 13:12Проблема агентов и тех, кто с помощью них плохо кодит, в том что порой чтобы получить хорошо читаемое, вылизанное и быстрое решение, нужно отдельно разработать подробный и дотошный промт, а лучше написать его с помощью другой нейронки, прося еще одну нейронку добавить что-то от себя. Иначе ты будешь днями копаться в проекте и на каждый непонятный и медленный метод его же и просить улучшить или сделать рефакторинг.

FlyGst
21.04.2026 13:12На теме вайбкодинга, я думаю, must have использовать тесты где можно и где не можно - пусть также сгенерированный ИИ, но такой код уже (как мне кажется) не будет поддерживаться человеком. А значит итерация на улучшение будет выполняться на более умной LLM. И здесь только всесторонние тесты могут показать что в новом коде что-то работает не так, как в старом (что в принципе не исключает старой ошибки).

Andrey2008 Автор
21.04.2026 13:12Нет. Тесты это только часть подхода к качеству. Они не заменят построение архитектуры, контроль заимствованных компонент (SCA), проверку оптимальности выбранных алгоритмов и т.д. Не цепляйтесь за одну лучшую методологию. Тесты хорошо, но недостаточно.

makssof
21.04.2026 13:12А тесты кто будет ревьюрить? А есть уверенность что тесты не будут такого же качества, как и сам код? У меня был опыт работы в проекте, где используется ии-автоген тесты, и я могу сказать что в таких ни пасс, ни фейлур - не являются показателями работоспособности

Faositb
21.04.2026 13:12Да, какое-никакое ревью тестов необходимо: в моем рабочем проекте на Python тесты в основном пишем при помощи локальной нейронки компании (MiniMax M2.7): опыт показывает, если есть какие-то схожие с требуемым к написанию тесты, на которые можно сослаться в промпте, то LLM вполне качественно отрабатывает, но с нуля генерировать тесты - это очень подробное словесное описание нужно давать, иначе привет фактически низкому уровню покрытия (при большом количестве самих тестов) и галлюцинациям. Да и проблема потери контекста и последующей генерации правдоподобного бреда даже на 200к токенов актуальна.

DingosWill
21.04.2026 13:12Не хватает в таких статьях сравнения в конце.
Вот есть критика от разработчика определённой компетенции, а вот критика, которую мы получили, попросив нейронку оценить дизайн. Было бы интересно посмотреть, насколько совпадают или расходятся тезисы
Andrey2008 Автор
21.04.2026 13:12Вы можете сами это легко сделать. Проект состоит из одного файла. Скормите его нейронке и попросите её подробно написать, что не так. Потом расскажите :) Я делал такое, глянул на выхлоп и закрыл. Мне такой фигнёй (сравнением) заниматься не интересно.

Cheater
21.04.2026 13:12Не принимается. Косвенный доступ по указателю не существенный, учитывая, что потом всё равно будет куча обращений к памяти для чтения строк и их конкатенации. Перерасход памяти выглядит куда более существенной проблемой.
Экономия на спичках. В реальности скорее всего потеряете в производительности в несколько раз. Массив смартпоинтеров - это куча лишних аллокаций и структура данных с непредсказуемым и нелокальным расположением элементов, крайне враждебная к оптимизациям.

Andrey2008 Автор
21.04.2026 13:12Забыл в статье ещё вот на этот пост сослаться, который мне понравился и который транслирует схожие мысли:
...
Главный риск ИИ-кода — не в том, что он плохой, содержит баги, уязвимости и т.п. В том-то и беда, что он выглядит убедительно хорошим. Агент выдаёт что-то, что компилируется, проходит тесты, рендерит красивый UI. И возникает непреодолимый соблазн пустить это в прод, не заглядывая внутрь.
...
j4niwzis
C++ вполне приятный язык. Его можно использовать и для когда, для которого не важны оптимизации. Его метапрограммирование многого стоит, да ещё и со статической типизацией. Много ещё языков есть, которые позволяют такое?
Не выглядит оно элегантно. Если человек не способен распознать тут проблемы прямо сразу, то это не начитанный C++ программист. Тут сразу видна проблема в том, что создают промежуточные строки, а потом сразу же копируются в общую. Для сериализации в строки есть прекрасный апи через std::formatter. Внутри же можно было бы вызывать std::format_to и не использовать промежуточные строки.
Ещё уродливый if-chain с кучей if constexpr. Зачем? Ведь существует std::visit и перегрузки (можно было бы через концепты сократить код).
Далеко не лучшее решение. Намного удобнее было бы взять std::variant из std::unique_ptr. Не будет проблемы перерасхода памяти, будет элегантный апи std::variant, можно будет использовать std::visit и перегрузки с концептами. Так ещё и std::variant работает быстрее виртуальных функций (у меня в одной статье освещалось то, что компиляторы их лучше оптимизируют. Да и бенчмарки самого visit найти можно). Промежуточные std::string тем временем никуда не пропали.
Там, где речь шла про SIMD, не помешал бы ассемблер в статье. Не помешали бы и бенчмарки при сравнении варианта с std::unique_ptr<Node> и оригинального std::variant.
AndreyDT
Я когда ифы сплошником увидел, сразу испугался) может и где-то это используется, но я такое только на си видел
Andrey2008 Автор
Ну там не просто if-ы, а этапа компиляции (
if constexpr). Я к тому, что это работает не так, как вы возможно подумали :)j4niwzis
ИИ часто такое выдаёт. Если выдаёт, то должно использоваться и очень много. Даже когда ему явно прописано, что так делать нельзя. Но пугаться тут нечего - тут просто не самый лучший код.
Нечто подобное было бы получше:
Или через overloaded делать перегрузки.
Чтобы не было лишнего дублирования кода там, где можно было бы обойтись и без этого. Но и минусы у такого подхода имеются. Если обновляется перечень типов, то нужно внимательно следить за тем, чтобы посреди этого не вызвалась какая неправильная перегрузка.
Есть и средний вариант - выражать всё через функции и перегрузки, а не if-chain. Тогда неправильным перегрузкам будет сложно вызваться (остаются только неявные преобразования), а код будет более элегантным уж точно.