Модули
C++20 модули — export/import, интерфейсные и реализационные единицы, Binary Module Interface, разделы и миграция с
17 вопросов
JuniorТеорияОчень частоЧто делает ключевое слово export в интерфейсе модуля?
Что делает ключевое слово export в интерфейсе модуля?
export помечает объявление как часть публичного API модуля, делая его видимым всем, кто пишет import. Объявления без export остаются внутренними для модуля. export также образует само объявление модуля: export module name;.
Типичные ошибки
- ✗Путать
export(видимость / членство в API) с линковкой — это независимые понятия - ✗Считать, что все объявления в интерфейсе модуля экспортируются по умолчанию
- ✗Думать, что
exportменяет контроль доступа подобноpublic/privateв классе
Уточняющие вопросы
- →Можно ли
exportдляstatic-функции или сущности из анонимного namespace? - →Чем блок
export { ... }отличается от префикса перед каждым объявлением?
JuniorТеорияОчень частоЧем import отличается от #include для потребителя кода?
Чем import отличается от #include для потребителя кода?
#include — это текстовая подстановка препроцессора: текст заголовка вставляется и парсится заново. import загружает уже скомпилированный Binary Module Interface, поэтому макросы не утекают, порядок импортов не важен, и видны только экспортированные имена.
Типичные ошибки
- ✗Ожидать, что макросы, определённые в модуле, будут доступны после
import - ✗Считать, что
importобрабатывается препроцессором как#include - ✗Полагать, что перестановка строк
importменяет результат, как порядок#include
Уточняющие вопросы
- →Почему
importможет быть независимым от порядка, а#include— нет? - →Можно ли использовать
#includeиimportвместе в одном файле?
JuniorТеорияОчень частоКакие проблемы модели #include решают модули C++20?
Какие проблемы модели #include решают модули C++20?
#include текстово копирует заголовок в каждую единицу трансляции, поэтому код парсится повторно (медленная сборка), макросы утекают между файлами, порядок включения важен, нет инкапсуляции. Модули устраняют все четыре проблемы: интерфейс компилируется один раз и изолирован семантически.
Типичные ошибки
- ✗Считать, что include-guard'ы решают стоимость повторного парсинга — они лишь предотвращают двойное включение в одной TU, не между TU
- ✗Думать, что утечка макросов — вопрос стиля, а не структурный дефект текстового включения
- ✗Полагать, что проблемы только в скорости, игнорируя инкапсуляцию и зависимость от порядка
Уточняющие вопросы
- →Почему include-guard'ы не устраняют повторный парсинг между единицами трансляции?
- →Какие из этих проблем решает precompiled header, а какие нет?
JuniorТеорияЧастоЧто такое Binary Module Interface (BMI)?
Что такое Binary Module Interface (BMI)?
BMI — сгенерированный компилятором бинарный файл, хранящий уже разобранное семантическое представление интерфейса модуля. Потребители читают BMI вместо .cppm. Расширения разные: .gcm (GCC), .pcm (Clang), .ifc (MSVC).
Типичные ошибки
- ✗Путать BMI с объектным файлом — BMI хранит разобранную семантику, а не машинный код
- ✗Считать BMI переносимым между компиляторами, раз язык стандартизован
- ✗Полагать, что BMI — читаемый текст, как вывод препроцессора
Уточняющие вопросы
- →Стоит ли коммитить BMI-файлы в систему контроля версий? Почему?
- →Что всё равно попадает в отдельный объектный файл при наличии BMI?
JuniorТеорияЧастоВ чём разница между интерфейсным юнитом модуля и юнитом реализации?
В чём разница между интерфейсным юнитом модуля и юнитом реализации?
Интерфейсный юнит начинается с export module name; и объявляет публичный API модуля — именно его видит import потребителя. Юнит реализации начинается с module name; (без export) и лишь определяет тела; потребители его не видят и не компилируют напрямую.
Типичные ошибки
- ✗Считать, что потребители обязаны компилировать или импортировать юниты реализации ради определений
- ✗Полагать, что интерфейсный юнит не может содержать inline-определений
- ✗Путать
module name;(реализация) сexport module name;(интерфейс)
Уточняющие вопросы
- →Может ли у одного модуля быть больше одного первичного интерфейсного юнита?
- →Куда попадает код юнита реализации — в BMI или в объектный файл?
JuniorТеорияЧастоЧто происходит с объявлениями модуля, не помеченными export?
Что происходит с объявлениями модуля, не помеченными export?
Они полностью пригодны к использованию внутри модуля — через его интерфейсные юниты и юниты реализации — но невидимы любому потребителю, который пишет import. Это настоящая инкапсуляция: вспомогательные функции и детали остаются приватными без отдельной конвенции namespace detail.
Типичные ошибки
- ✗Считать, что неэкспортированные сущности удаляются и непригодны даже внутри модуля
- ✗Полагать, что полное имя даёт потребителю доступ к неэкспортированным объявлениям
- ✗Думать, что всё, что есть в BMI, автоматически импортируемо
Уточняющие вопросы
- →Как это заменяет старую конвенцию
namespace detailдля сокрытия хелперов? - →Может ли неэкспортированный тип быть типом возврата экспортированной функции?
MiddleТеорияЧастоПочему модель BMI компилируется быстрее, чем #include?
Почему модель BMI компилируется быстрее, чем #include?
С #include каждая единица трансляции заново парсит полный текст заголовка. Интерфейс модуля парсится один раз в BMI; затем каждый потребитель читает это предварительно разобранное семантическое представление, вместо повторной токенизации и анализа того же исходника снова и снова.
Типичные ошибки
- ✗Считать, что BMI хранит машинный код и потребитель пропускает генерацию кода
- ✗Полагать, что include-guard'ы уже дают такой же выигрыш кэширования между TU
- ✗Приписывать выигрыш параллельной сборке, а не однократному парсингу
Уточняющие вопросы
- →Почему include-guard'ы не дают такой же однократный парсинг между единицами трансляции?
- →Нужны ли в BMI определения шаблонов, или достаточно объявлений?
MiddleТеорияЧастоПочему модуль не может экспортировать макросы?
Почему модуль не может экспортировать макросы?
Макросы — понятие препроцессора без области видимости, и прекращение их неконтролируемой утечки — одна из главных целей модулей. import передаёт скомпилированные семантические сущности, а не текст препроцессора, поэтому #define внутри модуля просто не доходит до потребителя.
Типичные ошибки
- ✗Считать неэкспорт макросов временным ограничением компилятора, а не намеренным решением
- ✗Думать, что
export #define— валидный синтаксис - ✗Путать макросы (препроцессор) с константами —
constexpr-значения экспортировать можно
Уточняющие вопросы
- →Если ваш API зависит от макросов, как его адаптировать под модули?
- →Доходят ли макросы header unit до потребителя, в отличие от именованного модуля?
MiddleТеорияИногдаЧем BMI отличается от precompiled header?
Чем BMI отличается от precompiled header?
PCH — нестандартный кэш компилятора с текстовым состоянием заголовка: макросы утекают, и один PCH обслуживает весь проект. BMI — стандартный артефакт модуля с настоящей инкапсуляцией: видны только экспортированные имена, а макросы не пересекают границу import.
Типичные ошибки
- ✗Считать BMI просто стандартизованным precompiled header с той же семантикой
- ✗Думать, что PCH даёт ту же изоляцию макросов, что и
importмодуля - ✗Полагать, что PCH умеет выражать модульную инкапсуляцию экспортированных и внутренних имён
Уточняющие вопросы
- →Почему в проекте реально много BMI, но обычно лишь один PCH?
- →Меняет ли PCH смысл кода, или только скорость сборки?
MiddleТеорияИногдаПочему модули требуют поддержки от системы сборки, а не только от компилятора?
Почему модули требуют поддержки от системы сборки, а не только от компилятора?
BMI модуля должен быть собран раньше любого потребителя, который его импортирует, поэтому сборка теперь упорядочена по зависимостям. Система сборки сканирует исходники, чтобы обнаружить рёбра import, и планирует компиляцию в топологическом порядке — одних меток времени файлов мало.
Типичные ошибки
- ✗Считать, что порядок компиляции по-прежнему свободный и параллельный, как с заголовками
- ✗Полагать, что рёбра зависимостей выводятся без сканирования инструкций
import - ✗Думать, что классический Makefile обрабатывает модули без изменений
Уточняющие вопросы
- →Что выдаёт сканер зависимостей модулей и когда он запускается?
- →Какие системы сборки получили нативную поддержку модулей, а какие всё ещё нет?
MiddleТеорияИногдаЧто делает export import в модуле?
Что делает export import в модуле?
export import X; импортирует модуль или раздел X и реэкспортирует его экспортированные имена всем, кто импортирует текущий модуль. Обычный import X; оставляет X приватным; export import строит модули-фасады и собирает разделы в первичный интерфейс.
Типичные ошибки
- ✗Считать, что
export importсоздаёт двунаправленную зависимость между модулями - ✗Полагать, что он реэкспортирует и неэкспортированные объявления импортированного модуля
- ✗Думать, что обычный
importуже делает импортированные имена видимыми нижестоящим потребителям
Уточняющие вопросы
- →Как
export import :partition;помогает собрать первичный интерфейс модуля? - →Если модуль A делает
export import B, становится ли цикл B→A легальным?
MiddleТеорияИногдаЧто такое глобальный фрагмент модуля и для чего он нужен?
Что такое глобальный фрагмент модуля и для чего он нужен?
Это область, открываемая голой строкой module; перед export module name;, где разрешены legacy-директивы #include. Подтянутые туда сущности доступны внутри модуля, но не экспортируются потребителям — это мост совместимости со старыми заголовками.
Типичные ошибки
- ✗Считать, что сущности из
#includeв глобальном фрагменте реэкспортируются потребителям - ✗Ставить строку
module;послеexport module name;, а не до неё - ✗Полагать, что глобальный фрагмент — место для публичного API модуля
Уточняющие вопросы
- →Почему
#includeдолжен быть в глобальном фрагменте, а не в теле модуля? - →Чем это отличается от превращения заголовка в header unit?
MiddleТеорияИногдаЧто такое header unit и чем он отличается от именованного модуля?
Что такое header unit и чем он отличается от именованного модуля?
Header unit — существующий заголовок, скомпилированный в форму BMI и подключаемый через import <vector>;. Он убирает стоимость повторного парсинга, но его макросы, в отличие от именованного модуля, доходят до потребителя — это мост миграции, не полная инкапсуляция.
Типичные ошибки
- ✗Полагать, что header unit изолирует макросы так же, как именованный модуль
- ✗Считать, что header unit — это вновь написанный файл, а не существующий заголовок
- ✗Думать, что
import "foo.h";— просто псевдоним#includeбез участия BMI
Уточняющие вопросы
- →Почему макросы header unit утекают, а макросы именованного модуля — нет?
- →Когда header unit предпочтительнее полного перевода кода в именованный модуль?
MiddleТеорияИногдаЧто такое раздел модуля и когда его применяют?
Что такое раздел модуля и когда его применяют?
Раздел — это именованный подюнит одного модуля, записываемый как export module math:basic;. Разделы позволяют разбить большой модуль на файлы, по-прежнему предъявляя потребителю один модуль. Первичный интерфейс собирает их, часто через export import :basic;.
Типичные ошибки
- ✗Считать, что потребители импортируют раздел напрямую через
import math:basic; - ✗Путать разделы (организация внутри одного модуля) с отдельными модулями
- ✗Полагать, что разделы дают инкапсуляцию, а не структуру на уровне файлов
Уточняющие вопросы
- →Что делает
export import :basic;в первичном интерфейсном юните? - →Может ли раздел содержать только реализацию и вовсе не экспортироваться?
SeniorДизайнИногдаВам достаётся большая C++-кодовая база, целиком построенная на #include-заголовках, и нужно перевести её на модули C++20. Команда не может остановить разработку фич, поэтому миграция должна идти маленькими шагами, сохраняя сборку проекта на каждом коммите, а часть сторонних заголовков вообще нельзя переписать. Опишите, как вы выстраиваете порядок миграции, как переведённый и ещё-не-переведённый код сосуществуют во время перехода и какое требование к системе сборки должно быть выполнено в первую очередь.
Вам достаётся большая C++-кодовая база, целиком построенная на #include-заголовках, и нужно перевести её на модули C++20. Команда не может остановить разработку фич, поэтому миграция должна идти маленькими шагами, сохраняя сборку проекта на каждом коммите, а часть сторонних заголовков вообще нельзя переписать. Опишите, как вы выстраиваете порядок миграции, как переведённый и ещё-не-переведённый код сосуществуют во время перехода и какое требование к системе сборки должно быть выполнено в первую очередь.
Мигрируйте инкрементально, снизу вверх: начните с leaf-библиотек без исходящих зависимостей, оборачивайте legacy-заголовки в глобальный фрагмент модуля и двигайтесь вверх по графу. Для непереведённых зависимостей берите header unit; нужна модуль-осведомлённая сборка вроде CMake 3.28+.
Открыть задачу →Типичные ошибки
- ✗Пытаться сделать единовременный перевод вместо инкрементального, начиная с leaf-узлов
- ✗Забывать, что
#includeиimportмогут сосуществовать во время перехода - ✗Игнорировать, что систему сборки надо обновить до начала любой конверсии
Уточняющие вопросы
- →Почему порядок «от leaf-узлов» необходим, а не просто удобен?
- →Как держать заголовок и его модульную версию синхронными, избегая ODR-проблем?
SeniorДебаггингИногдаКакая ODR-опасность возникает при смешивании #include и import одного и того же кода?
Какая ODR-опасность возникает при смешивании #include и import одного и того же кода?
Если одна TU делает #include заголовка, а другая — import модуля, оборачивающего тот же код, одна сущность получает два определения разными путями. Они должны быть токен-в-токен идентичны; любое расхождение между заголовком и модулем — это нарушение ODR, часто молчаливое и непроверяемое.
Типичные ошибки
- ✗Полагать, что линковщик надёжно обнаруживает и отвергает конфликтующие определения двух путей
- ✗Считать, что нарушение ODR — всегда жёсткая ошибка компиляции, а не молчаливый UB
- ✗Думать, что
importи#includeодного кода гарантированно эквивалентны
Уточняющие вопросы
- →Почему нарушение ODR часто не диагностируется, а не даёт жёсткую ошибку?
- →Какая дисциплина не даёт заголовку и его модульной обёртке разойтись?
SeniorТеорияРедкоПочему каждый toolchain должен пересобирать BMI модуля сам?
Почему каждый toolchain должен пересобирать BMI модуля сам?
BMI — это приватная сериализация внутреннего семантического представления одного компилятора; стандарт C++20 не определяет его формат. Он непереносим между компиляторами и часто между версиями одного и того же, поэтому каждый toolchain обязан пересоздать его из исходника.
Типичные ошибки
- ✗Полагать, что стандарт C++ определяет переносимый формат BMI
- ✗Считать, что компиляторы с одинаковым ABI могут делиться BMI-файлами
- ✗Думать, что BMI хранит машинный код, привязывая его к архитектуре, а не к компилятору
Уточняющие вопросы
- →Почему даже минорное обновление версии компилятора часто инвалидирует BMI?
- →Что непереносимость BMI означает для распространения предкомпилированной библиотеки?