В стандарте 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.ixxexport module foo:bar; // ========================// bar.cppmodule 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, ведь оба инструмента разработаны одной и той же компанией.
Резюмируя: модули — вкусная штука, но осадок от них горчит. Я не хочу возвращаться к работе с заголовками, поскольку, когда работа с модулями налажена, всё проходит блестяще. Но также мне не нравится с опаской обновлять какие-либо зависимости, компилятор или систему, ожидая, что из-за этого мне повстречаются какие-нибудь лавкрафтовские чудовища.
ссылка на оригинал статьи https://habr.com/ru/articles/1045676/