Если почитать комментарии на хабре по тематике языковых моделей, то наблюдалась совсем недавно тенденция от резкого перехода “языковая модель только для справки” до “используем агентов”. В первом случае помощь от языковых моделей минимальна. Во втором случае есть вопросы с доступностью подписок из России и/или большим расходом токенов. Надо конечно понимать что агент – это нечто большее чем просто языковая модель + обвязка, так как языковые модели специально дообучают агентным возможностям. То есть используя алгоритмическое составление контекста можно применять сравнительно бюджетные языковые модели, которые уже умеют писать код, но недостаточно хороши в агентных возможностях. Я бы отметил что агентные возможности и написание кода – это не одно и то же, но исторически сложилось что вначале прокачивали возможности кодинга в моделях, а только потом агентные. Тут нет ничего странного – агентные возможности требуют очень хорошего следования инструкциям и их интерпретации, количество упоминаний что модели галлюцинируют было огромное в статьях и комментариях. В случае же кодинга часть ошибок может отловить компилятор, часть – тесты, часть – программист в виде ревью. Кодинг можно разделить на агентный и на чат-кодинг. Большинство статей на хабре именно про агентный кодинг, когда агент самостоятельно подбирает контекст, планирует подзадачи и их реализует, пишет тесты и выполняет проверки, основываясь на пожеланиях вайб-кодера/программиста, выраженных в виде текста-спецификации. В этой статье идёт обсуждение только формирования контекста для одного языка программирования. Выбор именно С++ связан с 3-мя причинами:
-
знаю более-менее получше;
-
хорошая статическая типизация для отлавливания ошибок;
-
экономия контекста благодаря заголовочным файлам.
С первым пунктом всё понятно. Второй пункт про статическую типизацию уже требует пояснений:
-
уменьшение перплексии – языковая модель с меньшей вероятностью выберет неправильные токены; в целом немного спорно, но тем не менее;
-
проверка от компилятора;
-
повышение наглядности.
Конечно же самое главное, что позволяет С++ – экономия входных токенов без потери качества ответа. То есть в контекст можно включать только заголовочные файлы без реализации в большинстве случаев. Это позволяет в среднем уменьшить объём контекста в более чем 2 раза. Конечно без некоторых неоднозначностей не обходится. Так в ряде случаев есть функции вида void function_name(void). Чаще всего это методы классов. Для лучшего понимания со стороны языковой модели нужно в аргументах передавать изменяемые члены класса в виде ссылки или указателя, так как ориентироваться на название функции было бы ненадёжным вариантом. Мелкие функции в несколько(условно до 3-5) строк имеет смысл хранить полностью в заголовочном файле, а более крупные – выносить в файл реализации. Приведу пример, calendarwidget из примеров Qt5.15.2. Было
//window.h
private slots:
void weekdayFormatChanged();
//window.cpp
void Window::weekdayFormatChanged()
{
QTextCharFormat format;
format.setForeground(qvariant_cast<QColor>(
weekdayColorCombo->itemData(weekdayColorCombo->currentIndex())));
calendar->setWeekdayTextFormat(Qt::Monday, format);
calendar->setWeekdayTextFormat(Qt::Tuesday, format);
calendar->setWeekdayTextFormat(Qt::Wednesday, format);
calendar->setWeekdayTextFormat(Qt::Thursday, format);
calendar->setWeekdayTextFormat(Qt::Friday, format);
}
void Window::createTextFormatsGroupBox()
{
...
connect(weekdayColorCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(weekdayFormatChanged()));
}
Стало
//window.h
private slots:
void weekdayFormatChanged(QCalendarWidget *calendar, QComboBox *weekdayColorCombo);
//window.cpp
void Window::weekdayFormatChanged(QCalendarWidget *calendar,QComboBox *weekdayColorCombo)
{
QTextCharFormat format;
format.setForeground(qvariant_cast<QColor>(
weekdayColorCombo->itemData(weekdayColorCombo->currentIndex())));
calendar->setWeekdayTextFormat(Qt::Monday, format);
calendar->setWeekdayTextFormat(Qt::Tuesday, format);
calendar->setWeekdayTextFormat(Qt::Wednesday, format);
calendar->setWeekdayTextFormat(Qt::Thursday, format);
calendar->setWeekdayTextFormat(Qt::Friday, format);
}
void Window::createTextFormatsGroupBox()
{
...
connect(weekdayColorCombo,
QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int) {
weekdayFormatChanged(calendar, weekdayColorCombo);
});
}
То есть часто void function_name(void) будут поняты языковой моделью правильно, более того, генерация первого варианта кода повыше, но явная статическая типизация позволяет быть коду понятнее для языковой модели и человека, правда, в ряде случаев код будет выглядеть более сложным для слабо изучавших С++.
Важным аспектом структурной эффективности С++ в контексте LLM-разработки является согласование названий файлов заголовков (.h) и реализации (.cpp). Рекомендуется, чтобы имена файлов совпадали, за исключением случая main.cpp, когда отсутствует main.h.
Затронем тему деградации контекста. Основные факторы:
-
внимание многих языковых моделей на больших контекстах может ухудшаться просто вследствие объёма;
-
последовательные правки кода ведут к существенно более худшему пониманию чем просто вопрос-ответ.
Устранение транзитивных зависимостей.
Транзитивные зависимости заголовочных файлов в C++ — это ситуация, при которой один заголовочный файл (например, A.hpp) включает другой (B.hpp), а тот, в свою очередь, включает третий (C.hpp). В результате, если в коде используется A.hpp, то через цепочку включений (A → B → C) доступ к C.hpp также становится доступным, даже если напрямую C.hpp не включён. У транзитивных зависимостей есть существенные недостатки:
-
более длительное время компиляции;
-
лишние заголовочные файлы в контексте для языковой модели.
Метод ручного устранения транзитивных зависимостей является недостаточно точным, так как мало ли что программисту/кодеру может показаться. Точность языковых моделей тоже может быть недостаточна для устранения транзитивных зависимостей (и к тому же это расход токенов). Для решения этой проблемы есть утилита Include-what-you-use (IWYU). В ubuntu ставится sudo apt install iwyu. Пример вывода утилиты:
alex@alex-Default-string:/media/alex/312ECA1C66782878/workspace/project/src$ iwyu -Xiwyu –verbose=2 router.cpp
router.hpp should add these lines:
#include <bits/chrono.h> // for seconds, filesystem
#include <boost/asio/any_io_executor.hpp> // for any_io_executor
#include <boost/beast/http/message.hpp> // for request
#include <boost/beast/http/status.hpp> // for status
#include <boost/beast/http/string_body.hpp> // for string_body
#include <utility> // for pair
router.hpp should remove these lines:
#include <sys/stat.h> // lines 12-12
#include <zlib.h> // lines 10-10
#include <boost/asio.hpp> // lines 4-4
#include <boost/beast.hpp> // lines 3-3
#include <filesystem> // lines 16-16
#include <fstream> // lines 11-11
#include <iostream> // lines 13-13
The full include-list for router.hpp:
#include <bits/chrono.h> // for seconds, filesystem
#include <atomic> // for atomic
#include <boost/asio/any_io_executor.hpp> // for any_io_executor
#include <boost/beast/http/message.hpp> // for request
#include <boost/beast/http/status.hpp> // for status
#include <boost/beast/http/string_body.hpp> // for string_body
#include <functional> // for function
#include <map> // for map
#include <memory> // for shared_ptr
#include <mutex> // for mutex
#include <string> // for string, basic_string
#include <utility> // for pair
#include <vector> // for vector
router.cpp should add these lines:
#include <zconf.h> // for Bytef, uInt
#include <boost/asio/associated_executor.hpp> // for get_associated_e…
#include <boost/asio/impl/any_io_executor.ipp> // for any_io_executor:…
#include <boost/asio/post.hpp> // for post
#include <boost/asio/system_executor.hpp> // for basic_system_exe…
#include <boost/beast/core/impl/string.ipp> // for iless::operator()
#include <boost/beast/core/string_type.hpp> // for string_view
#include <boost/beast/http/field.hpp> // for field
#include <boost/beast/http/impl/field.ipp> // for to_string
#include <boost/beast/http/impl/fields.hpp> // for basic_fields::find
#include <boost/beast/http/impl/message.hpp> // for header<>::target
#include <boost/beast/http/verb.hpp> // for verb
#include <boost/core/detail/string_view.hpp> // for operator<<, oper…
#include <boost/intrusive/detail/algo_type.hpp> // for algo_types
#include <boost/intrusive/detail/list_iterator.hpp> // for operator!=, oper…
#include <boost/intrusive/detail/tree_iterator.hpp> // for operator==
#include <boost/intrusive/link_mode.hpp> // for link_mode_type
#include <cstdint> // for uintmax_t, uint64_t
#include <ctime> // for size_t, time_t
#include <exception> // for exception
#include <new> // for operator new
#include <stdexcept> // for runtime_error
#include <type_traits> // for remove_reference
router.cpp should remove these lines:
#include <algorithm> // lines 6-6
#include <cctype> // lines 7-7
#include <chrono> // lines 11-11
#include <sstream> // lines 5-5
The full include-list for router.cpp:
#include “router.hpp”
#include <zconf.h> // for Bytef, uInt
#include <zlib.h> // for deflateEnd, Z_OK
#include <boost/asio/associated_executor.hpp> // for get_associated_e…
#include <boost/asio/impl/any_io_executor.ipp> // for any_io_executor:…
#include <boost/asio/post.hpp> // for post
#include <boost/asio/system_executor.hpp> // for basic_system_exe…
#include <boost/beast/core/impl/string.ipp> // for iless::operator()
#include <boost/beast/core/string_type.hpp> // for string_view
#include <boost/beast/http/field.hpp> // for field
#include <boost/beast/http/impl/field.ipp> // for to_string
#include <boost/beast/http/impl/fields.hpp> // for basic_fields::find
#include <boost/beast/http/impl/message.hpp> // for header<>::target
#include <boost/beast/http/verb.hpp> // for verb
#include <boost/core/detail/string_view.hpp> // for operator<<, oper…
#include <boost/intrusive/detail/algo_type.hpp> // for algo_types
#include <boost/intrusive/detail/list_iterator.hpp> // for operator!=, oper…
#include <boost/intrusive/detail/tree_iterator.hpp> // for operator==
#include <boost/intrusive/link_mode.hpp> // for link_mode_type
#include <cstdint> // for uintmax_t, uint64_t
#include <ctime> // for size_t, time_t
#include <exception> // for exception
#include <filesystem> // for path, operator<<
#include <fstream> // for basic_ostream
#include <iostream> // for cerr
#include <new> // for operator new
#include <stdexcept> // for runtime_error
#include <type_traits> // for remove_reference
#include “utils.hpp” // for md5_hash
Если заголовочные файлы лежат в отдельных папках и используется include path настройках, то использование утилиты include-what-you-use будет более сложным. Подробности сами можете поспрашивать у языковых моделей.
Также одним из способов уменьшения зависимостей является forward declarations(предварительное объявление без реализации). В C++ предварительное объявление – это способ уведомить компилятор о существовании некоторого типа (класса, структуры, перечисления и т.д.) до его полного определения, позволяя использовать этот тип в определённых контекстах, не требуя при этом полного описания его внутренней структуры. Это особенно полезно при работе с указателями и ссылками на типы, поскольку компилятору не нужно знать детали реализации типа, чтобы понять, что указатель на него занимает определённое количество памяти (обычно 8 байт на 64-битных системах). Например, если в одном классе есть указатель на другой класс, достаточно объявить этот класс, чтобы компилятор мог корректно обработать указатель, не требуя полного определения. Объявления вперёд помогают избежать циклических зависимостей между заголовочными файлами, уменьшают время компиляции, так как не нужно включать полные определения, и упрощают структуру проекта. Однако важно понимать, что предварительное объявление не позволяет использовать объекты типа напрямую — для этого всё равно потребуется полное определение. Также стоит помнить, что предварительное объявление не работают с типами, которые используются как значения (например, передача по значению), поскольку компилятор должен знать размер и структуру типа.
Я нагенерировал программу для сбора контекста https://github.com/SanyaZ7/cppctx Пример вывода программы:
alex@alex-Default-string:/media/alex/312ECA1C66782878/workspace/ctxcpp$ ./ctxcpp -d 1 ‘/home/alex/solvespace/src/clipboard.cpp’
[DEBUG] Includes в "/home/alex/solvespace/src/clipboard.cpp":
(none)
-> trying to resolve "solvespace.h"
found near source: "/home/alex/solvespace/src/solvespace.h"
[DEBUG] Includes в “/home/alex/solvespace/src/solvespace.h”:
(none)
-> trying to resolve “dsc.h”
found near source: “/home/alex/solvespace/src/dsc.h”
-> trying to resolve “polygon.h”
found near source: “/home/alex/solvespace/src/polygon.h”
-> trying to resolve “srf/surface.h”
found near source: “/home/alex/solvespace/src/srf/surface.h”
-> trying to resolve “render/render.h”
found near source: “/home/alex/solvespace/src/render/render.h”
-> trying to resolve “expr.h”
found near source: “/home/alex/solvespace/src/expr.h”
-> trying to resolve “sketch.h”
found near source: “/home/alex/solvespace/src/sketch.h”
-> trying to resolve “ttf.h”
found near source: “/home/alex/solvespace/src/ttf.h”
-> trying to resolve “ui.h”
found near source: “/home/alex/solvespace/src/ui.h”
-> trying to resolve “platform/platform.h”
found near source: “/home/alex/solvespace/src/platform/platform.h”
=== ОТЧЁТ ДЛЯ “/home/alex/solvespace/src/clipboard.cpp” ===
Глубина включений: 1
Найдено файлов (реально существующих): 2
Все кавычечные #include успешно резолвились.
Обнаружено 5 системных заголовков (не включаются в .ctx).
=== ДЕРЕВО ЗАВИСИМОСТЕЙ ===
clipboard.cpp
└─ solvespace.h
├─ dsc.h
├─ polygon.h
├─ surface.h
├─ render.h
├─ expr.h
├─ sketch.h
├─ ttf.h
├─ ui.h
└─ platform.h
Context written to “/home/alex/solvespace/src/clipboard.ctx”
Работа с git.
Использование git – это то что часто игнорируется, так как может казаться проще воспользоваться прямыми изменениями кода и ctrl+z. В ряде случаев языковые модели даже лучше генерируют git diff и могут оказываться буквально переписывать заново целый файл или целую функцию. И понятно что это лишние расходы токенов. Поэтому краткая шпаргалка по применению git diff.
-
Инициализировать репозиторий, если он ещё не существует, и убедиться в чистоте рабочего каталога.
git init (при необходимости)
git status -s
Если есть незакоммиченные изменения, решить их судьбу (добавить, отложить или зафиксировать отдельно). -
Создать базовый коммит, который будет точкой отката для последующего эксперимента. Добавляются только те файлы, которые действительно относятся к текущей версии проекта:
git add <список‑файлов>
git commit -m “Initial commit – project skeleton” (если репозиторий уже имеет историю, вместо «initial» делаем обычный коммит с описанием подготовки эксперимента). -
Переключиться на новую ветку для работы с LLM‑генерированными изменениями.
git checkout -b feature/ai-improvement
Ветка создаётся от только что зафиксированного базового коммита. -
Получить дифф, предложенный языковой моделью, и сохранить его в файл llm.diff. Файл должен содержать только изменения, которые модель предлагает, без лишних правок.
-
Проверить совместимость патча с текущей веткой.
git apply –check llm.diff. Если есть ошибка, то значит синтаксис diff неправильный. -
Применить патч к рабочей копии.
git apply llm.diff -
Скомпилировать проект и выполнить тесты. Если сборка или тесты не проходят, откатить применённый патч:
git reset –hard HEAD~1 (откат только последнего коммита) и вернуться к шагу 4. -
Зафиксировать все изменения, внесённые патчем. git add -u
git commit -m “feat: AI‑generated improvement” -
Переключиться на основную ветку и убедиться в её актуальности.
git checkout main
git pull origin main
Затем слить экспериментальную ветку без потери истории: git merge –no-ff feature/ai-improvement. -
При неудачном слиянии откатить попытку и удалить экспериментальную ветку. Если конфликты возникли, выполнить git merge –abort. Затем удалить ветку: git branch -d feature/ai-improvement. После успешного мерджа ветка может быть оставлена или удалена в зависимости от политики проекта.
Я решил не удалять абзацы про уязвимости С++, которые были в начальном варианте статьи. Проблема управления памятью в С++ имеет низкую актуальность в большинстве случаев, поскольку большинство переменных выделяются на стеке, а динамическое выделение в куче требуется только для больших массивов или объектов, которые превышают размер стека. Стек имеет ограниченный размер (обычно несколько мегабайт), поэтому для больших данных используется heap-выделение. Однако при использовании Qt-фреймворка и виджетов, автоматическое управление памятью значительно упрощается благодаря системе объектов и родительских связей.
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
class MyWidget : public QWidget {
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
QPushButton *button = new QPushButton("Click me");
layout->addWidget(button);
// button и layout автоматически удаляются при удалении MyWidget
// delete неявное, благодаря Qt-системе наследования
}
};
В этом примере виджет MyWidget содержит виджет-контейнер QVBoxLayout, который в свою очередь содержит QPushButton. Все объекты автоматически удаляются при удалении родительского виджета, благодаря Qt-механизму управления памятью. Это делает код более безопасным и предсказуемым, поскольку не требует явного вызова delete, что снижает риск утечек памяти и ошибок при управлении ресурсами.
Кроме того, в С++ существуют умные указатели и ссылки (которые отсутствуют в Си), которые позволяют избежать явного управления памятью в тех случаях, когда не требуется использование обычных указателей. В Qt-фреймворке особенно полезны умные указатели QPointer и QScopedPointer, а также ссылки на объекты, которые автоматически обрабатывают жизненный цикл объектов.
#include <QWidget>
#include <QVBoxLayout>
#include <QPushButton>
#include <QPointer>
class MyWidget : public QWidget {
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
QPushButton *button = new QPushButton("Click me");
layout->addWidget(button);
// Использование QPointer для безопасной работы с указателями
QPointer<QPushButton> smartButton = button;
// Если button удаляется, smartButton автоматически становится nullptr
connect(button, &QPushButton::clicked, [smartButton]() {
if (smartButton) {
smartButton->setText("Clicked!");
}
});
}
};
В этом примере QPointer обеспечивает безопасную работу с указателями, автоматически обнуляя указатель при удалении объекта. Это позволяет избежать использования обычных указателей и уменьшает риск обращения к удалённым объектам. Умные указатели и ссылки в С++ значительно упрощают управление памятью и делают код более надёжным, особенно при работе с Qt-виджетами и асинхронными операциями.
Языковые модели позволяют разобраться с довольно увесистой системой классов в Qt, что делает сравнение ручной разработки и разработки с помощью LLM не совсем корректным. Ручная разработка на С++ требует значительного и сурового опыта, особенно при работе со сложными архитектурами Qt-приложений, которые включают множество классов, сигналов, слотов и компонентов. Простая формочка в Qt — это лишь малая часть возможностей фреймворка.
Важно отметить, что эффективное использование LLM в Qt-разработке требует применения генераторов кода типа Qt Designer совместно с LLM, а не посылать запросы на создание полного проекта. Qt Designer предоставляет визуальную среду для создания интерфейсов и структуры приложения, что позволяет LLM сосредоточиться на логике и бизнес-процессах, а не на низкоуровневом коде. Это сочетание инструментов позволяет значительно повысить эффективность разработки, поскольку:
-
Qt Designer формирует базовую структуру и интерфейс;
-
LLM генерирует логику и обработчики событий по промптам; Такой подход делает разработку более предсказуемой и менее требовательной к опыту, позволяя даже новичкам эффективно использовать LLM для создания сложных Qt-приложений.
При этом знание подавляющего большинства синтаксиса С++ является необходимым (возможно за исключением claude code и codex) для эффективного составления промптов. Это связано с тем, что LLM-агенты должны понимать структуру кода, типы данных, синтаксические конструкции и особенности Qt-фреймворка, чтобы правильно интерпретировать запросы и генерировать корректный код.
Знание С+±синтаксиса является ключевым фактором для эффективного взаимодействия с LLM в Qt-разработке, даже если некоторые современные агенты (как claude code и codex) могут частично компенсировать недостаток знаний синтаксиса. То есть под знанием синтаксиса подразумевается узнавание синтаксических конструкций, что значительно проще чем умение писать. Это означает, что для эффективного составления промптов достаточно понимать структуру кода, распознавать типы данных, знакомые синтаксические конструкции и особенности Qt-фреймворка, но не обязательно уметь самостоятельно писать сложный код.
При работе с LLM в Qt-разработке важно:
-
распознавать основные синтаксические конструкции С++ (классы, функции, указатели, ссылки);
-
понимать структуру Qt-объектов и их взаимодействие;
-
знать типы данных и их использование в Qt-контексте; Это знание позволяет правильно формулировать промпты, чтобы LLM могла генерировать код, соответствующий требованиям проекта. В отличие от полного умения писать код, это более простая задача, которая может быть освоена при минимальной практике.
Значительной проблемой является обновление библиотек. Уже для boost 1.83 приходится писать md-файл для того, чтобы gpt-oss-20b и gpt-oss-120b понимали вышедшие изменения. А последняя версия уже boost 1.90. Поэтому со временем бюджетные модели из кодинга уйдут. Они сравнительно хорошо кодят на старых версиях библиотек, но файлом изменений отделаться не получится.
Затронем также проблему понимания контекста моделями. Для бюджетных моделей лучше чтобы был единый контекст и 1 основной промпт на генерацию кода и, возможно, небольшие корректирующие промпты. Так как кода важна точность и отсутствие синтаксических ошибок. Если первый промпт оказался неудачным, то добавление дополнительной информации вторым промптом ухудшит качество ответа.
Для понимания о каких моделях идёт речь: gpt-oss-20b, gpt-oss-120b, grok-4.1-fast, qwen3-code-next. Важен логически цельный контекст, хорошо описанная задача. Причём задачу лучше писать в форме эссе, то есть пояснять почему то или иное принято, так как директивные указания модель может в ряде случаев игнорировать.
Интересно что если напрямую спросить: какой язык программирования лучше подходит для LLM кодинга из списка, то наиболее вероятно что С++ окажется на последнем или предпоследнем месте. Это связано с тем, что в данных обучения С++ называют сложным языком и модель делает простой вывод: раз язык сложный, то подходит плохо. Вопросы ощущения сложности довольно субъективны и уходят в область психологии.
В других языках со статической типизацией не встречается аналога разделения заголовочных файлов и файлов реализации, поэтому нужно полагаться на RAG, который в cursor ide специально адаптирован под нахождение нужных фрагментов кода. Без аналога заголовочных файлов чат кодинг будет терять эффективность. Про качество обученности конкретных моделей другим языкам со статической типизацией я не буду, так как чего-то объёмного не писал.
Как видно вторая часть статьи немного отличается от первой, но я решил что пусть будет. Так как статья достаточно давно висела в черновиках. Мне стало заметно в последние несколько недель что градус агрессии на хабре стал поменьше (как бы цензура-то есть на хабре, могут заминусовать, особенно карму). Почему-то ругать языковые модели стало уже не модно.
Сложный синтаксис С++ достаточно хорошо нивелируется языковыми моделями, поэтому сравнение языков программирования по сложности не особо актуально по моему мнению в настоящее время. Описанное в стье не является полноценным аналогом агентного программирования, но, возможно, кому-то окажется полезным, особенно с учётом сложности оформления подписок из-за санкций.
Автор: SanyaZ7


