Модули

Мотивация

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

Как выглядит:

export module sum; export int add(int a, int b) { return a + b; } // main.cpp import sum; int main() { int c = add(42, 42); }

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

Если хотим поиспользовать обычные хедеры, то нужен global module fragment (обязан идти в самом начале):

module; #define _POSIX_C_SOURCE 200809L #include <stdlib.h> ...

Почему компилятор не может просто закэшировать содержимое большого хедера? Например, когда у нас N .cpp файлов инклудят <algorithm>. Потому что хедер не обязан быть самодостаточным кодом, это может быть рандомная последовательность токенов. Или, например, некоторые инклудят внутрь extern "C" блока. А ещё хедера сильно зависят от контекста (например куча ifdef в библиотеках).

Модули же не зависят от контекста.

Как это работает

Расширение для файла с модулем: .ixx. При его компиляции генерируются два файла: .obj (машинный код), и .ifc (интерфейс модуля). Чтобы скомпилировать main.cpp нужен будет только .ifc файл. Вроде стало лучше, но есть один нюанс: с хедерами компиляция отдельных .cpp могла идти параллельно, но с модулями приходится компилировать всё в порядке топологической сортировки зависимостей. Т.е. нужно учить билд-систему компилировать всё в правильном порядке.

По дефолту импорты не транзитивны, т.е. модуль, который импортит algorithm не будет торчать им наружу. Для транзитивных импортов надо делать так:

export { import algorithm; } // Либо export import algorithm;

Ещё пример

// foo.ixx export module foo; struct result { int a; int b; }; export result get(); // main.cpp import foo; int main() { get().a; // валидно result t = get(); // а так уже нельзя }

В proposal по модулям дали два новых определения: visible и reachable имена. Visible имена нам доступны, а у reachable имён мы можем только использовать их свойства (как с лямбдами, их типы только reachable, но при этом мы видим operator()).

Что, если хотим объявить что-то локальное для модуля, как для единицы трансляции, то нужно объявить это в анонимном неймспейсе:

// foo.ixx export module foo; namespace { struct result { int a; int b; }; } export result get(); // error

Файл со строчкой вида

export module sum;

называется primary module interface unit.

Уже упоминалось что при компиляции программ с модулями возникают зависимости. Иногда это может быть даже хуже традиционной компиляции. Пример:

// sum.ixx int sum(int a, int b) { // 100mb of code } // user.ixx, 100mb import sum; void foo() { // 100mb of code }

В случае модулей перед компиляцией user нам придется скомпилировать sum. Но в случае хедеров, мы просто инклюднем в user декларацию sum и мы сможем параллельно скомпилировать оба файла. Как же это починили? Ввели понятие module implementation unit. Выглядит это примерно так:

// primary module interface unit export module sum; int sum(int a, int b); ----------------------- // module implementation unit module sum; int sum(int a, int b) { return a + b; }

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

Ещё одна штука, с которой помогают модули - ODR. Модули владеют функциями (и классами), которые в них объявлены. Например, если мы заимпортим два модуля с одной и той же функцией, то всё будет компилироваться, но мы не сможем вызвать эту функцию.

Возможен такой случай: пусть мы решили разделить модуль на две части (перенесли нужные объявления в другой файл и скопировали туда все импорты). Тогда мы сломаем бинарную совместимость (насколько я понял, это значит что мы не сможем слинковать когда-то скомпилированный с нашим модулем объектный файл), т.к. каждое объявление привязано к своему модулю.

Чтобы можно было, оставаясь в пределах одного модуля, распределять его части по файлам придумали partition:

export module foo:p1; // либо module foo:p2;

Чтобы это работало, главный primary module interface unit должен переэкспортить все импорты партишнов. Партишены локальны для модуля - снаружи о них никто не знает (точно? я не уверен что правильно это понял).

TODO: private partition

Transition

Хотим использовать в модуляризированных программах хедеры и наоборот. В одну сторону - легко, просто воспользоваться global module fragment.

TODO: транзишн в обратную сторону