﻿# Ревью вайб\-кода с гнильцой, который притворяется оптимизированным С\+\+ кодом

Ценность квалифицированного программиста смещается в сторону умения проводить обзоры кода\. Генерировать код становится проще, но всё так же важно проверять его с точки зрения качества декомпозиции, корректности реализации, эффективности, безопасности\. Посмотрим на примере маленького проекта markus, созданного с помощью Claude Opus, почему важно понимать сгенерированный код и уметь видеть, что скрывает красивый текст программы\.

![1366_markus_cpp_vibe_ru/image1.png](https://import.viva64.com/docx/blog/1366_markus_cpp_vibe_ru/image1.png)

## Проект markus

Я искал какой\-то проект, сгенерированный с помощью ИИ, и мне попался [markus](https://github.com/ryanmcdermott/markus)\.

Это single\-header библиотека для парсинга текста и конвертации его в HTML\. Создана с помощью Claude Opus 4\.5\.

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

Библиотека состоит из одного файла `markus.h`, содержащего 6484 строки на C\+\+20\. По существу кода ещё меньше\. Некоторые алгоритмы в целях оптимизации используют табличные методы\. Четыре таблицы представлены в коде бессмысленно вытянуто\. [Пример](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L620):

```cpp
static constexpr uint8_t kWhitespaceTable[256] = {
      0,
      0,
      0,
      0,
      .... // и так 256 строк
```

Если таблицы не считать, то, собственно, кода ещё на 1000 строк меньше\. Анализатор PVS\-Studio ничего особенного в проекте не нашёл, но проверка стала поводом посмотреть некоторые фрагменты поподробнее\. И вот уже специалист, то есть я, могу выделить моменты, которые стоит обсудить\.

## Проблематика вайб\-кодинга

Генерация кода с помощью ИИ может очаровать, особенно неискушённого программиста, который не работал с большими старыми проектами\.

Кто сталкивался, тот понимает, что главная задача — не написать код, а сделать так, чтобы его можно было сопровождать и развивать\. Об этом хорошо [написал](https://habr.com/ru/articles/990934/) Роберт Мартин:

> Без присмотра AI будет нагромождать код поверх кода\. У него нет чувства большой картины\. Архитектура, вероятно, за пределами его возможностей\.

Так что изучение принципов декомпозиции, умение правильной постановки задач и т\. д\. становятся только более актуальной компетенцией программиста\.

Но я хочу посвятить статью другой особенности вайб\-кодинга, про которую мало говорят: это очарование процессом генерации кода и, как следствие, **излишнее доверие к нему**\. Редкое исключение — заметка "[ИИ пишет код быстрее\. Но не понимает, что пишет](https://vc.ru/ai/2814435-ii-v-razrabotke-skorost-koda-i-risk-kachestva)"\.

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

За примером далеко ходить не надо\. Я ради интереса просил ИИ написать код для быстрого подсчёта количества единичных бит в больших массивах\. Я был очарован предложенными алгоритмами\. Про многие интересные решения я даже не знал, хотя в своё время прочитал в книге разбор целого ряда алгоритмов на эту тему\.

Смотришь на предложенный код подсчёта бит и думаешь: "А не пора ли податься в фермеры?" Сам бы я такой код не написал и не догадался бы его поискать, т\. к\. просто не знаю про такие алгоритмы\.

Это восхищение проецируется на весь код, созданный ИИ\. Когда я только заглянул в код markus, то подумал, что анализировать его смысла нет: всё красиво, оптимизированные алгоритмы и т\. д:

1. Тонкая настройка оптимизации компилятора с помощью подсказок наиболее вероятных веток исполнения \[\[unlikely\]\];
1. Использование условной компиляции на основе `if constexpr`;
1. Использование оптимизированных табличных алгоритмов;
1. Генерация SIMD\-Friendly Helper Functions;
1. Использование контейнеров из пространства имён `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 можно сравнить с другим решением и понять, что он получился не очень быстрым\. А если сравнивать не с чем? Нужно экспертное мнение человека\. Беда в том, что многим кажется, что теперь эксперт и обзор кода не нужны\.

## Нужен ли эксперт?

А может быть, оценка кода экспертом действительно не нужна?

Вы создали код\. Он выглядит красиво и опрятно\. Он выглядит оптимизированным и работает\. Может, этого достаточно?

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

1. Никто не пострадает и не понесёт убытки, если код работает не так, как задумывалось, из\-за багов или неправильной интерпретации ТЗ\.
1. Код безответственный в плане безопасности\. Никто не будет его ломать, так как это никому не нужно\.
1. Скорость работы кода неважна\.
1. Не предполагается, что у проекта будет долгий жизненный цикл\. Его не потребуется развивать десятилетиями, сопровождать, адаптировать для других платформ и т\. д\.

Во всех остальных случаях, извините, все требования к проверке качества кода в силе и ещё более актуальны\. К вайб\-коду должны применяться все те же процессы, что описываются в SSDLC / стандарте [РБПО](https://pvs-studio.ru/ru/webinar/rbpo/content/)\.

## Почему я строг к ИИ

Я не строг, а требователен\. Для реализации проекта взят язык С\+\+\. **Суть использования C\+\+ — построение быстрых, эффективных приложений, экономно использующих память**\. Если вам не нужна быстрая программа, не надо использовать C\+\+\.

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

Мой личный интерес — изучать, что такое вайб\-код и как статические анализаторы могут помочь контролировать его надёжность\. Вайб\-коду, как и людям, свойственны антипаттерны, но другие\. Есть смысл развивать анализатор PVS\-Studio с учётом этих особенностей\.

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

## SIMD\-friendly код

Комментарий к функции [`IsSpanBlank`](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L377) обещает, что код составлен из расчёта облегчить компилятору задачу его векторизации, например, с помощью SSE\-инструкций\.

Суть оптимизации — несколько элементов обрабатываются одновременно, чтобы определить, хранят ли они пробельные символы\.

```cpp
// 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](https://ru.wikipedia.org/wiki/SIMD)\-инструкций\. Оставшийся хвост символов обрабатывается отдельным простым циклом\.

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

```cpp
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 взял и сделал\. Взял и написал более умный вариант"\.

Давайте попробуем сравнить скорость\. Мой [коллега Филипп](https://pvs-studio.ru/ru/blog/?author=philip-khandeliants) провёл небольшой эксперимент с [GCC](https://quick-bench.com/q/rPfirfciTyG23cnVSLWDV9dEVJk) и [Clang](https://quick-bench.com/q/vfIF7a6mkD3AOzLw7HyDqaq0IyY):

* Формируем одинаковый набор данных для обоих бенчмарков: генерируем 1 000 000 строк случайной длины от 1 до 127 символов с произвольным содержимым\. Генерация наборов не учитывается в замерах\.
* Подаём на вход алгоритму набор данных и для каждой строки вычисляем, содержит ли она пробельные символы\. Замеряем общее время\.

В обоих вариантах код компилируется с агрессивными оптимизациями `-O3`\. Можно заметить, что для обоих компиляторов простой вариант функции лучше, чем с вложенным циклом\.

![1366_markus_cpp_vibe_ru/image2.png](https://import.viva64.com/docx/blog/1366_markus_cpp_vibe_ru/image2.png)

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

Параллельно с коллегой тоже поэкспериментировал с двумя рассмотренными и тремя новыми вариантами алгоритма\. Я использовал компилятор MSVC\. Давайте посмотрим, какие результаты замеров получились\.

Во\-первых, мне подсказали, что ситуацию можно улучшить, заменив в изначальном варианте `||` на `|`\. Short\-circuit evaluation в нашем случае не помогает, а только вредит\. Улучшенная проверка:

```cpp
bool is_blank = (c == ' ' | c == '\t' | c == '\n' | c == '\r');
```

<details>
   <summary>Полный код функции</summary>

```cpp
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;
}
```


</details>
Во\-вторых, я спросил DeepSeek\. Он предложил вариант оптимизации, построенный на битовых операциях:

```cpp
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 оптимизированную функцию, по\-настоящему использующую векторизацию\. Вот что получилось:

```cpp
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 мегабайт, заполненном случайными пробельными символами\. Выполнение потока привязывается к одному предварительно прогретому ядру\. Если непонятно, о чём идёт речь, загляните в статью "[Вечный вопрос измерения времени](https://pvs-studio.ru/ru/blog/posts/cpp/0097/)"\.

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v590/) \[CWE\-571\] Consider inspecting this expression\. The expression is excessive or contains a misprint\. markus\.h 5987

Действительно, код содержит избыточную проверку:

```cpp
BlockNode ParseParagraph() {
  ....
  auto trimmed = detail::TrimLeft(line);  // trimmed имеет тип std::string_view
  ....
  if (trimmed.size() >= 2 && trimmed[1] == '!' && trimmed.size() >= 9) {  
  ....
}
```

Тут, на самом деле, ничего страшного\. Ну лишняя проверка, которую и так выкинет компилятор\. Для красоты код, конечно, стоит поправить, но не более того:

```cpp
if (trimmed.size() >= 9 && trimmed[1] == '!') // вот теперь хорошо
```

Однако глаза скользнули на код ниже `if`\. И вот там избыточность, которая достойна вашего внимания:

```cpp
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](https://pvs-studio.ru/ru/blog/terms/6658/)\.

Лучше написать функцию, выполняющую сравнение без учёта регистра\. Например, такую:

```cpp
bool starts_with_insensitive(std::string_view str,
                             std::string_view prefix_lower_case)
{
  if (str.size() < prefix_lower_case.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](http://www.gotw.ca/gotw/029.htm), и так далее\. Главное — мы избавляемся от создания временной строки и упрощаем код, выполняющий проверку:

```cpp
if (starts_with_insensitive(trimmed, "<!doctype")) {
  goto html_interrupt;
}
```

Ещё ярче необходимость создания функции сравнения станет видна в следующей главе\. Как говорится, это только цветочки, а ягодки будут впереди\.

## Сравнение строк: ягодки

Просматривая другие предупреждения PVS\-Studio, я обратил внимание на этот вектор строк:

```cpp
std::optional<HtmlBlock> TryParseHtmlBlock() {
  ....
  static const std::pmr::vector<std::pmr::string> type1_tags = {
      "script", "pre", "style", "textarea"
  };
  ....
}
```

Анализатору он не нравится: [V1096](https://pvs-studio.ru/ru/docs/warnings/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

Нет смысла рассматривать это предупреждение в статье\. Куда серьёзнее то, как работают с этим вектором\. Анализатор не может оперировать высокоуровневыми понятиями и заметить неладное, зато это может заметить человек и сказать всё, что думает про эту дичь\.

Код ниже проверяет наличие этих тегов в строке\. Но ведь в теге ещё есть открывающая угловая скобка `<`, поэтому ИИ, не стыдясь, создаёт эти временные строки, содержащие тег со скобкой:

```cpp
for (const auto& tag : type1_tags) {
  std::pmr::string open_tag = "<" + tag;
```

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

```cpp
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])));
}
```

Весь этот кринж [выглядит](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L4781) следующим образом:

```cpp
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`, что вовсе уберёт динамические аллокации\.

```cpp
using namespace std::literals;

static constexpr std::array type1_tags = {
  "<script"sv, "<pre"sv, "<style"sv, "<textarea"sv
};
```

В\-третьих, теперь можно найти префикс с помощью написанной ранее функции `starts_with_insensitive`\. В результате весь говнокод, приведённый выше, сокращается до:

```cpp
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](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L3511), [GetAltText](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L3543), [RenderInlines](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L6262), [DebugAst](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L6336), но для примера приведу только одну, которая покороче:

```cpp
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` и кодом, красиво оформленным в столбик\.

Оказывается, и здесь плохо\. Сейчас и вам расскажу почему\.

![1366_markus_cpp_vibe_ru/image3.png](https://import.viva64.com/docx/blog/1366_markus_cpp_vibe_ru/image3.png)

Итак, стоит задача хранить массив неких элементов, которые по\-разному сериализуются в строки\.

Простым подходом будет создать базовый класс и наследовать от него разные сущности\. В контейнере тогда будут храниться указатели на него\. А для сериализации понадобится в разных случаях вызывать разные виртуальные функции\. Получилась бы приблизительно следующая иерархия классов:

```cpp
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); }
};
```

Массив таких объектов превращался бы в строку как\-то так:

```cpp
std::string GetAltText(const std::vector<Node *>& nodes) {
    std::string result;
    for (const auto& node : nodes)
        result += node->StrAltText();
    return result;
}
```

Приведённый код требует ручного управления памятью, да и вообще небезопасен\. Ситуацию можно улучшить, храня в векторе умные указатели, например [`std::unique_ptr`](https://en.cppreference.com/w/cpp/memory/unique_ptr.html)\. Другой вариант — использовать [`std::any`](https://en.cppreference.com/w/cpp/utility/any.html)\.

Однако остаётся вторая проблема: код сериализации размазан по множеству классов\. Можно собрать обработку в одном месте, узнавая тип объекта в массиве с помощью `dynamic_cast`\. Это медленно\.

Поэтому классическим решением будет хранить идентификатор объекта в базовом классе, чтобы быстро определять его тип:

```cpp
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) {}
};
```

Теперь обработку, хоть и не очень изящно, можно собрать в одном месте:

```cpp
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`](https://en.cppreference.com/w/cpp/utility/variant.html), что и сделал Claude Opus\. Тип `std::variant` — это такой умный вариант `union`, безопасный с точки зрения работы типов\. Вот только тут есть нюанс\. Жирный такой нюанс\.

Но начнём мы с того, то ИИ вообще создал странную избыточную сущность\. Он завёл [перечисление `NodeType`](https://github.com/ryanmcdermott/markus/blob/4519680d8331693140aabac5bb5591931a796ad8/markus.h#L70), которое бывает нужно для схемы, которую мы рассмотрели выше:

```cpp
enum class NodeType {
  ....
  kHtmlBlock,
  kBlockQuote,
  kList,
  kListItem,
  kText,
  kSoftBreak,
  ....
};
```

И вставил бессмысленный числовой идентификатор `kType` в структуры:

```cpp
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`:

```cpp
// 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` в том, что она должна иметь достаточный размер для размещения внутри себя наибольшей альтернативы\. В нашем случае вариант должен вмещать в себя следующие классы:

```cpp
using InlineNode = std::variant<Text, SoftBreak, HardBreak, Code, Emphasis,
                                Strong, Link, Image, HtmlInline>;
```

Размер этих классов весьма разный, начиная с самых маленьких, таких как `SoftBreak`:

```cpp
struct SoftBreak {
  static constexpr NodeType kType = NodeType::kSoftBreak;
};
```

Размер этого класса 1 байт\. Хотя класс пустой, он всё равно должен занимать хотя бы один байт\.

Самый большой класс — `Image`:

```cpp
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](https://github.com/microsoft/STL/blob/main/stl/inc/xstring#L589) занимает 40 байтов\. Вы уже поняли, что это означает?

Вариант ещё дополнительно хранит информацию, какой тип он содержит в текущий момент\. Итого размер одного объекта типа `InlineNode` составляет 128 байтов\! Можете [здесь сами посмотреть](https://godbolt.org/z/WsWG6MM6K)\.

Итого, программа работает с массивами, где размер каждого элемента 128 байтов\. Офигительная "эффективность" расхода памяти\.

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

```cpp
} else if constexpr (std::is_same_v<T, SoftBreak>) {
  result += ' ';
```

**Чтобы знать, где напечатать пробел, требуется хранить 128 байтов\.**

Перерасход памяти сам по себе, может, не такая уж большая проблема, но здесь ещё возникает и проблема производительности\. При работе с таким растянутым массивом менее эффективно будет использоваться и кэш процессора\.

Можно возразить, что вариант с умными указателями добавляет уровень косвенности при доступе к объекту, что тоже замедлит работу\. И ещё нужно самому тип объекта проверять \(см\. выше пример со `switch`\)\.

Не принимается\. Косвенный доступ по указателю не существенный, учитывая, что потом всё равно будет куча обращений к памяти для чтения строк и их конкатенации\. Перерасход памяти выглядит куда более существенной проблемой\. Что по поводу проверки типа объекта, так она всё равно есть, просто скрыта внутри `std::variant`\.

Ещё представьте, как болезненно происходит расширение вектора, если очередной элемент уже не влезает в зарезервированный буфер\. Например, здесь:

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v560/) \[CWE\-571\] A part of conditional expression is always true: \!input\.empty\(\)\. markus\.h 2228

```cpp
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](https://pvs-studio.ru/ru/docs/warnings/v1048/) \[CWE\-1164\] The 'prev\_line\_had\_content' variable was assigned the same value\. markus\.h 4090

```cpp
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 антипаттернов для С\+\+ программиста](https://pvs-studio.ru/ru/blog/posts/cpp/1053/)"\.

### Создание временных прокси объектов

Предупреждение: [V823](https://pvs-studio.ru/ru/docs/warnings/v823/) Decreased performance\. Object may be created in\-place in the 'result' container\. Consider replacing methods: 'push\_back' \-\> 'emplace\_back'\. markus\.h 2597

```cpp
std::pmr::vector<InlineNodeId> ParseInlines() {
  std::pmr::vector<InlineNode> result;
  ....
  result.push_back(Text(text_.substr(text_start, pos_ - text_start)));
  ....
}
```

В начале создаётся временный объект типа `Text`, затем он помещается в контейнер\. Первая мысль: использовать `emplace_back`:

```cpp
result.emplace_back(Text(text_.substr(text_start, pos_ - text_start)));
```

Но изменение на `emplace_back` в этом случае даст примерно 0% ускорения, так ещё и нагрузит дополнительно компилятор — ему придётся делать инстанцирование шаблона функции `emplace_back`\. Внутри него уже будет идеально передаваться созданный объект, который будет перемещён конструктором перемещения\.

Здесь придётся написать более хитрый код, чтобы действительно получилось размещение сразу в контейнере\. Это контейнер вариантов, значит, мы должны подсказать ему, какую именно альтернативу собираемся создавать, и передать её `std::string_view`\.

Вот как это будет выглядеть:

```cpp
result.emplace_back(std::in_place_type<Text>, text.substr(....));
```

В таком случае мы идеально передадим подсказку о конструируемом типе и аргументы конструктора этого типа\. Нас интересует [5\-я перегрузка](https://en.cppreference.com/w/cpp/utility/variant/variant.html) конструктора `std::variant`\.

При таком изменении мы избавимся от одного вызова конструктора перемещения\.

<details>
   <summary>По отдельности мелочь, конечно, но если таких вставок много, то почему бы и не написать сразу хорошо</summary>

* 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


</details>
### Копия вместо перемещения

Предупреждение: [V820](https://pvs-studio.ru/ru/docs/warnings/v820/) The 'content' variable is not used after copying\. Copying can be replaced with move/swap for optimization\. markus\.h 4670

```cpp
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` копируется, хотя её можно переместить, так как дальше она не нужна\. Можно написать:

```cpp
heading.raw_content = std::move(content);
```

А можно обойтись вообще без временного объекта `heading` и написать что\-то типа:

```cpp
return Heading { .level = level,
                 .raw_content = std::move(content)
               };
```

<details>
   <summary>Опять\\\-таки, рассмотренное место — мелочь, но постепенно накапливаются лишние действия</summary>

* 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


</details>
### Закат солнца вручную

Предупреждение: [V837](https://pvs-studio.ru/ru/docs/warnings/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

```cpp
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 предупреждает о неэффективности, если в контейнере уже есть элемент с таким же ключом\. В нашем случае защита от этого сделана вручную, но она неэффективна\. В коде произойдут два поиска:

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

Вызов `try_emplace` в этой ситуации сделает ровно один поиск и вставит элемент, если его там не было:

```cpp
for (const auto& [label, dest_title] : nested_doc.link_references) {
    parent_link_refs_->try_emplace(label, dest_title);
}
```

## Заключение

Выводы всё те же, что были в предыдущей статье "[Давайте заглянем в этот самый вайб\-код](https://pvs-studio.ru/ru/blog/posts/cpp/1354/)"\. Независимо от того, как вы создаёте программный код, для обеспечения качества и надёжности разрабатываемого ПО следует использовать комплекс мер\.

1. Цените архитекторов и разработчиков, которые умеют декомпозировать и строить модульную архитектуру\.
1. Помните, что безопасность закладывается в систему с момента её проектирования \([КИБ](https://pvs-studio.ru/ru/blog/video/11596/)\)\.
1. Цените разработчиков, которые могут отличить эффективный код от медленного, безопасный от опасного\. Тех, кто может разобраться и принять решение, имеет ли право созданный код стать частью проекта\.
1. Используйте [статические анализаторы кода](https://pvs-studio.ru/ru/blog/video/11446/)\.
1. Используйте [динамические анализаторы кода](https://pvs-studio.ru/ru/blog/video/11452/) и [FAST анализаторы](https://pvs-studio.ru/ru/blog/video/11574/) для frontend\-приложений\.
1. Используйте [инструменты композиционного анализа](https://pvs-studio.ru/ru/blog/video/11494/)\. В случае использования ИИ это особенно актуально, так как их [галлюцинации начали применять для атак на цепочки поставок](https://vkvideo.ru/video-229013285_456239040?sh=4)\.
1. Используйте [стандарты кодирования](https://pvs-studio.ru/ru/blog/video/11433/)\. Нет ничего хуже, когда код написан вразнобой\.
1. Примените по необходимости другие практики, обобщённо называемые [разработкой безопасного ПО](https://pvs-studio.ru/ru/webinar/rbpo/content/)\.