В стандарте C++20 было представлено множество нововведений, и одним из наиболее крупных и долгожданных в их числе являлись модули. Теперь, когда с тех пор минуло около шести лет, то воодушевление сменилось здоровым цинизмом. Так, авторы сайта Are We Modules Yet прогнозируют, что поддержка модулей во всех библиотеках будет обеспечена к 1 мая 2167 года, а на Reddit не проходит и двух недель, как возникает очередной тред на тему: «Ну что, ими уже можно пользоваться»? (спойлер: нет).
Моя собственная одиссея по работе с модулями началась с того, как я в очередной раз взялся переписывать мою воксельную игру. Насколько же слабо я представлял, во что ввязываюсь.
Первым делом я попробовал Visual Studio
Я приступил к работе, вооружившись Visual Studio 2022, и на первых порах всё было нормально. Судьба уберегла меня от Intelli(Non)Sense, поскольку я всегда пользовался ReSharper++, который «просто работает» с модулями. Однако был и нерабочий элемент: MSVC. Всякий раз при повышении версии Visual Studio в ней что-то ломается, и требуется изыскивать способы вновь умилостивить компилятор. Тем хуже, что даже те баги, о которых сообщают, устраняются редко, а на то, чтобы поднять правку в главную ветку разработки (и она там закрепилась), требуются годы (sic!).
Разумеется, не во всём этом вина компилятора. Определённо бывало и так, что я сам что-то неверно понимал. Рассмотрим, к примеру, следующий код:
export module foo:bar; // блок интерфейса раздела модуля // ... module foo:bar; // блок реализации раздела модуля // ...
Видите баг? Разделы модулей и разделы реализации модулей могут фигурировать под разными именами. Мне представляется, что с давних пор блоки интерфейсов модулей и блоки реализации явно соотносились примерно как парные файлы «заголовочный файл/файл с исходным кодом». Поэтому представьте, как я удивился, когда ReSharper вдруг стал показывать мне ошибки. А компилятор это воспринял нормально: оказывается, в MSVC есть расширение, которое бесшумно это разрешает.
В теории import std; также казался отличной идеей, но на практике он постоянно отказывает даже в случае со следующей простейшей программой:
module; #include <boost/container/small_vector.hpp> module foo; import std;
На большинстве версий MSVC она просто отказывала — это было в порядке вещей. Со временем ситуация улучшилась, и к концу 2024 года я, наконец, смог преобразовать всю мою базу кода к виду import std;. Кроме того, я научился договариваться с компилятором и интуитивно стал неплохо распознавать те вещи, которых стоило избегать, чтобы его не злить.
Тем временем успели выйти новые фичи C++: дедуктивный вывод this и многоиндексный operator[] с std::mdspan. Это работало с нормальным C++, но не с модулями (MSVC выдаёт услужливое: «Сожалею, для модулей пока не реализовано!»), что меня слегка раздражало. В то же время хотелось поскорее приступать к экспериментам с P2300 (std::execution) и справочной реализацией NVIDIA. Всё это у меня ни разу не скомпилировалось. Честно говоря, даже не знаю, а должно ли оно компилироваться под Windows/ MSVC.
Как минимум, здесь есть обходные пути. Можно хотя бы перегрузить std::mdspan::operator[] при помощи std::array — кажется, что так делать нельзя, но это работает. Удивительно, что некоторые другие вещи также никогда не доставляли проблем; boost::mp11, равно как и заголовки vulkan.hpp, казались надёжными, как скала.
А потом появилась Visual Studio 2026
В 2025 году стали распространяться слухи, что вот-вот выкатят VS 2026. Я наивно полагал и даже очень надеялся, что баги, существовавшие в Студии годами, будут, наконец, устранены в релизной ветке этой версии. Я с нетерпением скачал инсайдерское превью — и лишь горько разочаровался. Код, нормально компилировавшийся под VS 2022, теперь вновь стал выбрасывать внутренние ошибки компилятора.
Даже в канале инсайдеров ситуация ничуть не улучшилась. Разочаровавшись из-за того, что работа пробуксовала на протяжении нескольких месяцев (и вообще наевшись Windows), я решил, что пора что-то менять. Может быть, я ретроград, но я привык, что операционная система должна служить мне и делать то, что я хочу. Терпеть не могу, когда создаётся ощущение, как будто этап за этапом моим выбором пренебрегают, и машина просто действует по своему усмотрению. Пара свежих примеров — Windows Recall, а также современное движение, в соответствии с которым вся работа пронизана взаимодействием с copilot.
Тогда я решил воспользоваться CMake
Порыскав по Reddit, обнаружил, что многие пользователи довольны работой с Clang. Также казалось, что там достаточно рано были реализованы некоторые крутые новинки (в частности, рефлексия). Тогда я решил скачать CMake, преобразовать мой проект в её формат, а затем воспользоваться CLion и Clang. Честно признаюсь, мне так и не удалось её как следует сконфигурировать. Поддержка модулей C++ и/или import std; в CMake остаётся экспериментальной и, чтобы пользоваться этими возможностями, вам придётся повозиться со всякими GUID. Думаю, что попробовал подставлять для этих GUID все значения, какие только можно, но CMake продолжает жаловаться, что она их не поддерживает (насколько могу судить, это могло быть каким-то образом прописано в файле CMakeCache.txt, но никак не могу понять, почему эти возможности не работают).
Так я пришёл к использованию Arch
Тогда мне захотелось проверить, будет ли вообще компилироваться этот код, если воспользоваться другим компилятором (хотелось самому себе доказать, что это не я схожу с ума). Стал подыскивать подходящий дистрибутив Linux и, наконец, остановился на Arch, поскольку успел хорошо напрактиковаться с ним в университете, и так как в его состав входят очень свежие версии всех сборочных инструментов. Так что я подключил к системе ещё один SSD, поднял там среду Arch Live, почти отформатировал мой SSD с Windows (как поступил бы профессионал!) и установил Arch Linux.
Под Arch всё просто взяло и заработало. Я склонировал мой проект, установил vcpkg, дал vcpkg файл с цепочкой инструментов, в котором было сказано использовать Clang вместо GCC, нажал «сконфигурировать» и собрал проект. Как же я был наивен.
Также хочу отметить, что мой проект сильно нагружен зависимостями. В файле манифеста vcpkg перечислено более 30 зависимостей, в том числе с десяток библиотек Boost, а также Vulkan, LLVM, ImGui, FlatBuffers и многое другое. Даже когда я попытался скомпилировать столь крупную штуку как LLVM — всё просто сработало.
Единственным «уродом в семье» оказался GCC, который — что бы я ни делал — не хотел компилировать мой проект. Правда, поскольку Clang во всех случаях сработал, меня это не особенно беспокоило.
Что я успел изучить за работой
В конце концов, наладив работу всех инструментов, я смог перейти от борьбы с компилятором к написанию кода как такового. Со временем я выработал ряд паттернов, которые функционировали хорошо.
Во-первых, я постарался обернуть каждую из сторонних зависимостей в собственный модуль. В таком случае #include остаётся заключено в единственном месте, а для остальной базы кода предоставляется чистый интерфейс import:
module; #include <boost/container/deque.hpp> #include <boost/container/devector.hpp> #include <boost/container/flat_map.hpp> #include <boost/container/flat_set.hpp> #include <boost/container/small_vector.hpp> #include <boost/container/stable_vector.hpp> #include <boost/container/static_vector.hpp> #include <boost/container/vector.hpp> export module boost.container; namespace boost::container { export using boost::container::small_vector; export using boost::container::static_vector; export using boost::container::vector; export using boost::container::deque; export using boost::container::devector; export using boost::container::flat_map; export using boost::container::flat_set; export using boost::container::stable_vector; }
При фокусе с export using мы реэкспортируем только те типы, которые нам нужны. Макросы можно переопределить как переменные constexpr static inline.
Также оказалось, что PIMPL удивительно помогает укрощать компиляторы, а на производительность никакого измеримого негативного влияния не оказывает (но не уверен, что вам понравится).
Что касается организации самих модулей, я решил придерживаться соглашения «по папке на модуль», примерно так, как устроена система пакетов в Java:
worldgen/ sandbox.worldgen module └── biome_provider/ sandbox.worldgen.biome_provider module ├── extrusion/ sandbox.worldgen.biome_provider.extrusion module └── pipeline/ sandbox.worldgen.biome_provider.pipeline module
Внутренние модули могут импортировать модули извне (так что sandbox.worldgen.biome_provider может зависеть от sandbox.worldgen, но не наоборот). Так удаётся избежать закольцованных импортов. Как по мне, система работает хорошо, но я не знаю, как охарактеризовать такой подход — как рекомендуемую практику или потенциально даже как антипаттерн. Это требуется, так как в worldgen может быть такой класс как AbstractBiomeProvider — именно от него наследует ExtrusionBiomeProvider в субмодуле.
У каждого из файлов .ixx внутри каждого модуля имеется собственный раздел, где за их export import отвечает $module.ixx. Некоторым файлам .ixx сопутствуют файлы *.cpp — это обычные блоки для реализации разделов. Я сначала писал их просто как блоки реализации модулей (то есть, не разделов), но впоследствии решил перейти на реализацию разделов, чтобы избежать неприятностей, возникавших в первом варианте — дело в том, что блоки реализации неявно импортируют основной блок интерфейса. Однако, оказалось, что такая практика не поддерживается в CMake , так что я вернулся к моей исходной попытке.
// foo.ixx – модуль export module foo; export import :bar; // ======================== // bar.ixx export module foo:bar; // ======================== // bar.cpp module foo;
В данном случае я бы предостерёг от создания «сверх»-модулей. В самом начале у меня был вспомогательный модуль, в котором содержался разнообразный материал «на всякий случай». В итоге сложилось так, что мой проект примерно на 80% зависел от этого модуля и перекомпилировался целую вечность. А перекомпилировать код приходилось часто, поскольку именно в этот модуль я обращался за всякими вспомогательными вещами. Был ещё один такой модуль для работы с serde. Он мне пригодился при разработке (де)сериализации. Большинство моих классов должны были использовать типы из этого модуля, чтобы десериализовать себя.
Есть такой пост C++20 Modules: Best Practices from a User’s Perspective , написанный в декабре 2025 года. В нём подробно разобрано, как использовать модули в реальном коде. В частности, там рекомендуется: «В проекте должен объявляться всего один модуль. Если нужно представить множество трансляционных единиц — пользуйтесь для этого разделами модулей».
Как минимум, в том, что касается моего проекта, я принципиально не согласен с этим тезисом. При разработке игрового движка кажется очень странным заводить всего один модуль engine, так как напрашиваются более мелкие, например engine.renderer. Опять же, не буду утверждать, что сам могу исчерпывающе ответить на все эти вопросы и рекомендую вам самостоятельно прочесть вышеупомянутую статью, чтобы вы могли сформировать о ней собственное мнение.
В конце концов, мне просто нравится, как в модулях всё инкапсулируется, а также что в реализациях не наблюдается таких случайных деталей, из-за которых возникали бы утечки. Кроме того, стало казаться, что сборочный процесс удалось значительно ускорить (это лишь интуитивное ощущение, я ни разу не компилировал проект с заголовками). Проект вырос, в нём скопилось примерно 250 файлов с модулями (не считая зависимостей), причём 36 из этих модулей относились к ядру проекта (не были сторонними). На модуль приходилось в среднем 6,4 раздела. Притом, что этот проект не назовёшь огромным, он уже несравним с игрушечным примером уровня «Hello, World!».
Но потом начались проблемы
Мне бы очень хотелось закончить этот пост в духе «я попробовал модули, немного с ними помучился, но теперь я окончательно всем доволен». К сожалению, не могу так сказать. Я допустил непростительную ошибку — решился на апгрейд всей системы. Спустя всего один pacman -Syu мой мир оказался в руинах.
В настоящий момент мне не удаётся скомпилировать проект ни при помощи Clang, ни при помощи GCC. Clang продолжает жаловаться на неоднозначный оператор operator new; этот баг известен, но в Clang 22 не исправлен по причине смешивания #includes и imports. Помните ту обёртку boost.container, которую я показывал выше? Clang 22 стал жаловаться на то, что в ней не хватает оператора new, или что он поставлен неоднозначно. Если написать export using ::operator new, то компилятор просто откажет с ошибкой. Интуитивно я не понимаю, почему этому оператору может понадобиться эксперт — с одной стороны, естественно, он может быть нужен stable_vector в его определении, но это деталь реализации, разве она не должна разрешаться автомат(г)ически? «Правится» эта ситуация так: явно ставим включение в глобальный фрагмент импорта, имеющийся во всех модулях, но в таком случае теряется смысл в обёртке как таковой. В довершение всего этого в Clang также происходит внутренняя ошибка компилятора, диагностикой которой я ещё собираюсь заняться.
Что касается GCC, ему не удаётся скомпилировать код из-за того, что сторонние библиотеки не рассчитаны на работу с модулями. В их заголовках содержатся сущности, локальные на уровне единиц трансляции (это статические функции, символы анонимных пространств имён), на которые ссылаются экспортированные шаблоны, и GCC полагают, что тем самым нарушаются правила ODR/видимости.
Кроме того, у меня возникали странные ошибки, в случае с которых я не могу понять, виноват ли в ошибке компилятор, или это я что-то делаю не так (а более новые компиляторы корректно отвергают недопустимый код). Этой проблемы можно было бы избежать, если бы библиотеки поставлялись в виде модулей, но суть в том, что даже код из boost невозможно потреблять в виде модулей (за исключением нескольких библиотек).
К чему же мы пришли
Некоторые из проблем, с которыми я столкнулся, полностью обусловлены библиотеками (или, как минимум, исправимы только на уровне библиотек). Вспоминаются те переменные, которые должны быть static inline, но сделаны просто static, либо перегрузки ::operator new. Со временем библиотеки улучшаются, а подавление предупреждений в моей практике пока не приводило ни к чему плохому.
Очевидный выход был бы в том, чтобы кто-нибудь (может быть, даже я сам) контрибьютил правки в эти проекты, так, чтобы модули либо стали поддерживаться в них нативно, либо чтобы эти вещи не мешали. Спасибо @anarthal, который написал Deep Dive on C++20 modules and boost и пришёл к следующему выводу:
Обсудив ситуацию с мейнтейнерами, мы пока решили поставить инициативу на паузу. Рассчитываю вернуться к этой работе, когда будут исправлены те баги MSVC, которые я нашёл, а поддержка import std в CMake стабилизируется.
Ещё одна битва разворачивается вокруг заголовочных единиц (import <my_legacy_header.hpp>). Они отлично работают с MSBuild и MSVC, но, насколько мне известно, в CMake они просто пока не поддерживаются, вообще. Ещё в те времена, когда я нацеливался на Windows/ MSVC, были устранены проблемы с некоторыми малопонятными внутренними ошибками компилятора (ICE).
В IDE по-прежнему сохраняются огромные проблемы при работе с модулями: так, в IntelliSense поддержка модулей до сих пор обозначается как экспериментальная. CLion катастрофически проявил себя в моём проекте, как со старым аналитическим движком, так и с новым. На Reddit сообщают, что с clangd также не всё хорошо, но я с ним не работаю. На мой взгляд, удобнее всего было иметь дело с комбинацией Visual Studio и ReSharper++, которая «просто работала». Не знаю, чем вызваны такие отличия между ними и CLion, ведь оба инструмента разработаны одной и той же компанией.
Резюмируя: модули — вкусная штука, но осадок от них горчит. Я не хочу возвращаться к работе с заголовками, поскольку, когда работа с модулями налажена, всё проходит блестяще. Но также мне не нравится с опаской обновлять какие-либо зависимости, компилятор или систему, ожидая, что из-за этого мне повстречаются какие-нибудь лавкрафтовские чудовища.
Комментарии (7)

SilverTrouse
09.06.2026 23:50С gcc да согласен несколько сложнее жить. gcc в некоторых аспектах строже clang и сильно фанатичнее следует стандарту ( разработчики LLVM можно сказать оставили допущения на переходный период), плюс clangd сейчас умеет работать только clang модулями ( за то оч хорошо сейчас работает, проверял на macos и linux)

AoD314
09.06.2026 23:50На сколько я понимаю основной плюс перехода на модули это сокращение время компиляции, ну а если нет, то нафига оно. Ну так как, на сколько сократилось время компиляции?

SilverTrouse
09.06.2026 23:50это только один из плюсов, есть и другие два - контроль над экспортом ( что не дают инклуды, благодаря чему есть namespace detail) и просто другая организация кода. что по скорости то gcc сейчас в 3 раза медленее билдит модули чем кланг

Jijiki
09.06.2026 23:50пользуюсь клангом,
такие шаблоны у меня
Скрытый текст
файл .clangd // CompileFlags: Add: [ -std=c++26, -stdlib=libc++, -I/usr/lib/llvm-20/share/libc++/v1, -fmodule-file=std=std.pcm, -fmodule-file=base3D=base3D.pcm, ] file builder.sh // #!/bin/bash # Настройки те же CXX="clang++-20" STD="-std=c++2c -O2" STDLIB="-stdlib=libc++ -Wno-unused-command-line-argument" #-lSDL2 -lSDL2_image -lSDL2_mixer -lGL -lGLEW LIBS="" MOD_EXT="cppm" STD_MODULE_PATH="/usr/lib/llvm-20/share/libc++/v1/std.cppm" # 1. База (std.pcm) if [ ! -f "std.pcm" ]; then echo "--- Сборка стандартного модуля ---" $CXX $STD $STDLIB --precompile -o std.pcm "$STD_MODULE_PATH" fi # 2. Генерируем build.ninja cat <<EOF > build.ninja rule pcm command = $CXX $STD $STDLIB -fmodule-file=std=std.pcm -fprebuilt-module-path=. --precompile \$in -o \$out description = Precompiling module \$out rule obj command = $CXX $STD $STDLIB -fmodule-file=std=std.pcm -fprebuilt-module-path=. -c \$in -o \$out description = Compiling object \$out rule link command = $CXX $STD $STDLIB -fmodule-file=std=std.pcm -fprebuilt-module-path=. \$in $LIBS -o \$out description = Linking app EOF # 3. Находим все модули и прописываем их в ninja # ALL_OBJS="" # ALL_PCMS="" # for f in *.$MOD_EXT; do # name=$(basename "$f" .$MOD_EXT) # echo "build $name.pcm: pcm $f | std.pcm" >> build.ninja # echo "build $name.o: obj $name.pcm" >> build.ninja # ALL_OBJS="$ALL_OBJS $name.o" # ALL_PCMS="$ALL_PCMS $name.pcm" # Накапливаем список имен pcm # done # Собираем список файлов в массив MOD_FILES=( *."$MOD_EXT" ) # Проверяем, существуют ли файлы (массив не пуст и первый элемент — реальный файл) if [ -e "${MOD_FILES[0]}" ]; then ALL_OBJS="" ALL_PCMS="" for f in "${MOD_FILES[@]}"; do name=$(basename "$f" ."$MOD_EXT") echo "build $name.pcm: pcm $f | std.pcm" >> build.ninja echo "build $name.o: obj $name.pcm" >> build.ninja ALL_OBJS="$ALL_OBJS $name.o" ALL_PCMS="$ALL_PCMS $name.pcm" done fi # 4. Теперь main.o железно знает, каких pcm ждать echo "build main.o: obj main.cpp | $ALL_PCMS" >> build.ninja # 5. Финальный таргет (линкуем всё вместе) echo "build app: link main.o $ALL_OBJS" >> build.ninja # 5. Запуск Ninja и программы ninja if [ $? -eq 0 ]; then echo "--- Запуск ---" ./app fi file base3d module // // base3D.cppm module; // Начало глобального фрагмента // #define GLEW_STATIC #include <GL/glew.h> #include <SDL2/SDL.h> // Все инклуды ДОЛЖНЫ быть здесь #include <SDL2/SDL_image.h> #include <SDL2/SDL_mixer.h> #include <functional> #include <map> #include <string> #include <memory> #include <vector> #include <unordered_map> #include <sstream> #include <print> // #include <glm/glm.hpp> // #include <glm/gtc/matrix_transform.hpp> // #include <glm/gtc/type_ptr.hpp> export module base3D; export struct Options{ std::string name="Test"; int posx=SDL_WINDOWPOS_CENTERED, posy=SDL_WINDOWPOS_CENTERED, width=1290, height=720, index=-1; uint32_t window_flags=SDL_WINDOW_ALLOW_HIGHDPI|SDL_WINDOW_RESIZABLE|SDL_WINDOW_OPENGL, sdl_init=SDL_INIT_VIDEO|SDL_INIT_AUDIO, sdl_image_init=IMG_INIT_PNG; }; // Теперь экспортируем нужные функции или структуры export using SDL_Event = ::SDL_Event; // Экспортируем тип из глобального пространства export using SDL_Window = ::SDL_Window; export using SDL_GLContext = ::SDL_GLContext; // Экспортируем функции (теперь они часть модуля) export using ::SDL_SetRenderDrawColor; export using ::SDL_CreateTexture; export using ::SDL_CreateRGBSurface; export using ::SDL_CreateTextureFromSurface; export using ::SDL_GetKeyboardState; export using ::SDL_SetRelativeMouseMode; export using ::SDL_GetRelativeMouseState; export using ::SDL_WarpMouseInWindow; export using ::SDL_GetTicks; export using ::SDL_GetTicks64; export using ::SDL_GetPerformanceCounter; // export using ::SDL_RenderClear; // export using ::SDL_RenderPresent; export using ::SDL_PollEvent; // export using ::SDL_FillRect; // export using ::SDL_RenderDrawRect; // export using ::SDL_RenderFillRect; export using ::SDL_MapRGB; export using ::SDL_RenderCopy; // export using ::SDL_HasIntersection; export using ::SDL_GetMouseState; export using ::SDL_GetWindowSize; export using ::SDL_SetWindowSize; export using ::SDL_SetWindowPosition; // export using ::SDL_RenderSetVSync; export using ::SDL_Delay; // export using ::SDL_RenderDrawPoint; export using ::SDL_GL_CreateContext; export using ::SDL_GL_DeleteContext; export using ::SDL_GL_SwapWindow; export using ::SDL_GL_SetSwapInterval; export using ::SDL_GL_SetAttribute; export using ::SDL_Quit; //TexturePtr texture{ SDL_CreateTextureFromSurface(app.render.get(), temp_surface) }; // Экспортируем типы, которые эти функции используют // export using ::SDL_Renderer; export using ::SDL_Window; export using ::SDL_Texture; export using ::SDL_Surface; // export using ::SDL_Rect; export using ::SDL_Color; // export using ::SDL_Point; export using ::Uint32; export using ::IMG_Load; export unsigned int CENTERED =0x2FFF0000u; // ============================================= // Универсальный хелпер для SDL ресурсов export template<typename T, auto Deleter> using SDL_UniquePtr = std::unique_ptr<T, std::integral_constant<decltype(Deleter), Deleter>>; // Теперь создание новых типов превращается в элегантный список: export { using WindowPtr = SDL_UniquePtr<SDL_Window, SDL_DestroyWindow>; using GLcontextPtr = SDL_UniquePtr<void, &SDL_GL_DeleteContext>; using TexturePtr = SDL_UniquePtr<SDL_Texture, SDL_DestroyTexture>; using SurfacePtr = SDL_UniquePtr<SDL_Surface, SDL_FreeSurface>; using SoundPtr = SDL_UniquePtr<Mix_Chunk, Mix_FreeChunk>; } export struct StateApp{ Options default_options; WindowPtr window; GLcontextPtr context; // RenderPtr render; bool run=false; }; export struct SoundEfffect { std::string name; SoundPtr sound; }; export struct TextureStorage { std::string name; // Rect rect; }; export struct StorageTexture{ std::map<std::string, TextureStorage> storage; }; export struct StorageSoundEffect{ std::map<std::string, SoundEfffect> storage; }; export void sound_load(std::map<std::string, SoundEfffect>& storage,const std::string& key, const std::string& path) { Mix_Chunk* raw = Mix_LoadWAV(path.c_str()); if (raw) { storage[key] = {key, SoundPtr(raw)}; } } export void sound_play(std::map<std::string, SoundEfffect>& storage,const std::string& key) { if (storage.contains(key)) { // C++20 фича .contains() Mix_PlayChannel(-1, storage[key].sound.get(), 0); } } export class EventManager { public: using Handler = std::function<void(const SDL_Event&)>; // Подписка на событие конкретного типа void subscribe(Uint32 eventType, Handler handler) { handlers[eventType].push_back(handler); } // Обработка всех накопившихся событий SDL void update() { SDL_Event event; while (SDL_PollEvent(&event)) { if (handlers.count(event.type)) { for (auto& handler : handlers[event.type]) { handler(event); } } } } private: std::map<Uint32, std::vector<Handler>> handlers; }; // export struct Camera{ // // Переменные состояния камеры // glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 0.0f); // glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); // glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); // float yaw = -90.0f, pitch = -0.0f; // Углы поворота // float cameraSpeed = 1.0f; // Скорость полета // }; // export glm::mat4 getView(Camera& camera){ // return glm::lookAt(camera.cameraPos, camera.cameraPos + camera.cameraFront, camera.cameraUp); // } // export void cameraUpdate(Camera& camera){ // const uint8_t* state = SDL_GetKeyboardState(NULL); // float speed = camera.cameraSpeed; // if (state[SDL_SCANCODE_W]) camera.cameraPos += speed * camera.cameraFront; // if (state[SDL_SCANCODE_S]) camera.cameraPos -= speed * camera.cameraFront; // glm::vec3 right = glm::normalize(glm::cross(camera.cameraFront, camera.cameraUp)); // if (state[SDL_SCANCODE_A]) camera.cameraPos -= right * speed; // if (state[SDL_SCANCODE_D]) camera.cameraPos += right * speed; // // Вертикальное движение теперь по Y // if (state[SDL_SCANCODE_SPACE]) camera.cameraPos.y += speed; // if (state[SDL_SCANCODE_LSHIFT]) camera.cameraPos.y -= speed; // } export void initApp(StateApp &app){ if (SDL_Init(app.default_options.sdl_init) < 0) return; SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 6); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); if (IMG_Init(app.default_options.sdl_image_init)< 0) return; Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048); // reset() сам удалит старое окно, если оно было app.window.reset(SDL_CreateWindow( app.default_options.name.data(), app.default_options.posx, app.default_options.posy, app.default_options.width, app.default_options.height, app.default_options.window_flags )); app.context.reset(SDL_GL_CreateContext(app.window.get())); // 2. Инициализация GLEW glewExperimental = GL_TRUE; if (glewInit() != GLEW_OK) return; if (app.window) app.run = true; SDL_GL_SetSwapInterval(1); } export void closeApp(){ Mix_CloseAudio(); Mix_Quit(); IMG_Quit(); SDL_Quit(); } // ========================================================================================================= // // SHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEM // // ========================================================================================================= // export struct Shader { std::uint32_t id = 0; std::unordered_map<std::string, int32_t> locs; // В OpenGL location — это int32_t, а не uint32_t // Запрещаем копирование, чтобы избежать случайного удаления ID в деструкторе Shader() = default; Shader(const Shader&) = delete; Shader& operator=(const Shader&) = delete; // Разрешаем перемещение (move) Shader(Shader&& other) noexcept : id(other.id), locs(std::move(other.locs)) { other.id = 0; } Shader& operator=(Shader&& other) noexcept { if (this != &other) { if (id) glDeleteProgram(id); id = other.id; locs = std::move(other.locs); other.id = 0; } return *this; } bool checkstatus(uint32_t object_id, uint32_t type) { int32_t success = 0; char info_log[512]; if (type == GL_COMPILE_STATUS) { glGetShaderiv(object_id, type, &success); if (!success) { glGetShaderInfoLog(object_id, 512, nullptr, info_log); std::println("Ошибка компиляции шейдера: {}", info_log); } } else if (type == GL_LINK_STATUS) { // Для программы используются другие функции! glGetProgramiv(object_id, type, &success); if (!success) { glGetProgramInfoLog(object_id, 512, nullptr, info_log); std::println("Ошибка линковки программы: {}", info_log); } } return static_cast<bool>(success); } uint32_t compget(const std::string& vss, uint32_t type) { const char* src_ptr = vss.c_str(); uint32_t vs = glCreateShader(type); // nullptr вместо &len указывает OpenGL, что строка заканчивается на '\0' glShaderSource(vs, 1, &src_ptr, nullptr); glCompileShader(vs); if (!checkstatus(vs, GL_COMPILE_STATUS)) { glDeleteShader(vs); return 0; } return vs; } void addLocs(const std::vector<std::string>& vecs){ for (auto a: vecs){ std::istringstream str(a); std::string line; std::string uname=""; while(std::getline(str,line)){ if(line.contains("uniform")){ auto start = line.find('=')+1; auto end = line.find(')'); auto str = line.substr(start,end-start); auto startName = line.find_last_of(" ")+1; auto endName = line.find_last_of(";"); auto strName = line.substr(startName,endName-startName); //std::println("{}",strName); int32_t loc = glGetUniformLocation(id,strName.c_str()); uname = strName; if (loc != -1) { locs.insert_or_assign(uname,loc); // std::println("Зарегистрирован юниформ: {} -> location: {}", uname, loc); } } } } } void compile_shader(const std::vector<std::string>& vecs) { if (vecs.size() < 2) return; uint32_t vs = compget(vecs[0], GL_VERTEX_SHADER); uint32_t fs = compget(vecs[1], GL_FRAGMENT_SHADER); if (vs == 0 || fs == 0) return; // Если хоть один не скомпилировался — выходим id = glCreateProgram(); glAttachShader(id, vs); glAttachShader(id, fs); glLinkProgram(id); // Проверяем линковку у PROGRAM (id), а не у шейдера! if (!checkstatus(id, GL_LINK_STATUS)) { glDeleteProgram(id); id = 0; // Чистим за собой промежуточные объекты шейдеров glDetachShader(id, vs); glDetachShader(id, fs); glDeleteShader(vs); glDeleteShader(fs); return; } addLocs(vecs); // Чистим за собой промежуточные объекты шейдеров glDetachShader(id, vs); glDetachShader(id, fs); glDeleteShader(vs); glDeleteShader(fs); } int32_t get_uniform(const std::string& name){ auto it = locs.find(name); if (it != locs.end()) { return it->second; } return -1; } // slot — это индекс слота (0, 1, 2...). // texture_id — это ID самой текстуры, полученный от glGenTextures void set_texture(const std::string& name, int32_t slot, uint32_t texture_id) { int32_t loc = get_uniform(name); if (loc != -1) { // 1. Активируем нужный текстурный слот (GL_TEXTURE0 + 0, GL_TEXTURE0 + 1 и т.д.) glActiveTexture(GL_TEXTURE0 + slot); // 2. Привязываем текстуру к этому активному слоту glBindTexture(GL_TEXTURE_2D, texture_id); // 3. Передаем номер слота в юниформ шейдера glUniform1i(loc, slot); } } // slot — это индекс слота (0, 1, 2...). // texture_id — это ID самой текстуры, полученный от glGenTextures void set_texture_array(const std::string& name, int32_t slot, uint32_t texture_id) { int32_t loc = get_uniform(name); if (loc != -1) { // 1. Активируем нужный текстурный слот (GL_TEXTURE0 + 0, GL_TEXTURE0 + 1 и т.д.) glActiveTexture(GL_TEXTURE0 + slot); // 2. Привязываем текстуру к этому активному слоту glBindTexture(GL_TEXTURE_2D_ARRAY, texture_id); // 3. Передаем номер слота в юниформ шейдера glUniform1i(loc, slot); } } void set_int(const std::string& name,int32_t i){ glUniform1i(get_uniform(name),i); } // matrix_ptr — указатель на первый элемент массива из 16 float (float[16]) // transpose — нужно ли транспонировать матрицу. // Если ваша либа хранит матрицы по строкам (row-major), передайте true. // Если по столбцам (column-major, как GLM), передайте false. void set_mat4(const std::string& name, const float* matrix_ptr, bool transpose = false) { int32_t loc = get_uniform(name); if (loc != -1) { // 1 — количество матриц // GL_FALSE/GL_TRUE — флаг транспонирования glUniformMatrix4fv(loc, 1, transpose ? GL_TRUE : GL_FALSE, matrix_ptr); } } void use(){ glUseProgram(id); } ~Shader() { if (id) { glDeleteProgram(id); } } }; export class ShaderInc{ public: // name shader std::unordered_map<std::string,Shader> shaders; void add(const std::string& name,const std::vector<std::string>& vecs){ Shader temp; temp.compile_shader(vecs); // Проверяем, что шейдер вообще скомпилировался, прежде чем портить мапу if (temp.id == 0) { std::println("Не удалось добавить шейдер '{}': ошибка компиляции.", name); return; } // Перемещаем напрямую, избегая лишнего дефолтного конструирования shaders.insert_or_assign(name, std::move(temp)); } // Быстрый метод для активации шейдера по имени void use(const std::string& name) { auto it = shaders.find(name); if (it != shaders.end()) { it->second.use(); // Вызывает glUseProgram } else { std::println("Шейдер с именем '{}' не найден!", name); } } // Быстрый метод для активации шейдера по имени void set_int(const std::string& sname,const std::string& uname,int32_t i) { auto it = shaders.find(sname); if (it != shaders.end()) { it->second.set_int(uname,i); // Вызывает glUseProgram } else { std::println("Шейдер с именем '{}' не найден!", sname); } } // shader_name — имя шейдера в мапе (например, "chunk_shader") // uniform_name — имя сэмплера в коде GLSL (например, "u_TexturePack") void set_texture(const std::string& shader_name, const std::string& uniform_name, int32_t slot, uint32_t texture_id) { auto it = shaders.find(shader_name); if (it != shaders.end()) { it->second.set_texture(uniform_name, slot, texture_id); } else { std::println("Шейдер '{}' не найден для установки текстуры!", shader_name); } } // shader_name — имя шейдера в мапе (например, "chunk_shader") // uniform_name — имя сэмплера в коде GLSL (например, "u_TexturePack") void set_texture_array(const std::string& shader_name, const std::string& uniform_name, int32_t slot, uint32_t texture_id) { auto it = shaders.find(shader_name); if (it != shaders.end()) { it->second.set_texture_array(uniform_name, slot, texture_id); } else { std::println("Шейдер '{}' не найден для установки текстуры!", shader_name); } } void set_mat4(const std::string& shader_name, const std::string& uniform_name, const float* matrix_ptr, bool transpose = false) { auto it = shaders.find(shader_name); if (it != shaders.end()) { it->second.set_mat4(uniform_name, matrix_ptr, transpose); } else { std::println("Шейдер '{}' не найден для установки матрицы!", shader_name); } } }; // else if(!line.contains("layout")&&!line.contains("buffer")&&line.contains("in")&&line.contains("uniform")){ // std::println("{}",line); // } // // ========================================================================================================== // // SHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEMSHADER SYSTEM // // ========================================================================================================== file main.cpp компилируюсю через билдер import std; import base3D; #include <GL/glew.h> // enum ShaderType{ // VERTEX, // FRAGMENT, // GEOMETRY, // COMPUTE, // }; int main(){ StateApp a; initApp(a); EventManager eventManagerGame; eventManagerGame.subscribe(SDL_QUIT, [&](const SDL_Event&) { a.run = false; }); std::vector<std::string> tempv{tvsc,tfsc}; // Shader shader; // shader.compile_shader(tempv); ShaderInc shaders; shaders.add("main", tempv); shaders.use("main"); shaders.set_int("main","player",1); while(a.run){ eventManagerGame.update(); SDL_GL_SwapWindow(a.window.get()); } closeApp(); return 0; }сборка быстрее, пока пишу код ниче не нагружает редактор для пересборки, оно и логично потомучто тут инклуды только в модулях.
SilverTrouse
у буста немного другой подход к модуляризаци своих либ, не через такую прослойку. Посмотрите доклад антона полухина с С++ZeroCostConf 2025 ну или посмотрите на уже модулязирование либы буста. И пропробуйте модуляризировать в их стиле ( часть либ уже модулязирована)
domix32
Це перевод если что. Врядли Шрёнбергер станет слушать доклад на русском. Для интересующихся ссылка.
SilverTrouse
ну Антон один из основных майнтейнеров буста, так что как минимум ему можно написать