Модули

Мотивация

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

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

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: транзишн в обратную сторону