Модули
Мотивация
Иногда, когда после препроцессирования получаются очень большие текстовые файлы, становится важной скорость компиляции.
Как выглядит:
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: транзишн в обратную сторону