Перейти к содержимому

C++20 модули

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

Почему модули заменяют заголовки

Система #include была основой компиляции C++ на протяжении десятилетий, но она несет в себе серьезные проблемы. Заголовки текстуально включены — препроцессор копирует весь файл в каждый блок перевода. Это означает, что #include может вставлять более 50 000 строк кода в каждый файл .cpp. Результат: * * медленная компиляция * *, * * хрупкая включает в себя заказ * * (макро из одного заголовка, затрагивающего другой) и * * Одно правило определения (ODR) нарушения * *, которые вызывают молчаливое неопределенное поведение.

Модули C++20 решают все эти проблемы. Модуль компилируется один раз в двоичный интерфейс, затем импортируется потребителями без переоценки. Нет ни текстового включения, ни макроутечки, ни зависимости от порядка включения. Сроки строительства могут улучшиться на 5-20x для крупных проектов.

Модуль декларирования и экспорта

Модуль имеет интерфейсный блок (объявляющий, что экспортируется) и необязательно один или несколько единиц реализации . Ключевое слово export делает объявления видимыми для импортеров:

math_utils.cppm
// math_utils.cppm — module interface unit
export module math_utils;    // module declaration

import <cmath>;               // import a header unit

// Exported — visible to importers
export double circle_area(double radius) {
    return std::numbers::pi * radius * radius;
}

export struct Point {
    double x, y;

    double distance_to(const Point& other) const {
        double dx = x - other.x;
        double dy = y - other.y;
        return std::sqrt(dx * dx + dy * dy);
    }
};

// NOT exported — module-private (internal linkage)
double helper_function(double val) {
    return val * val;
}

// Export a namespace — all members become visible
export namespace geometry {
    double triangle_area(double base, double height) {
        return 0.5 * base * height;
    }

    double rectangle_area(double width, double height) {
        return width * height;
    }
}

Импорт модулей

Потребители используют import для доступа к экспортным декларациям модуля. В отличие от #include, import не выполняет текстовое включение — макросы и детали реализации не просачиваются через границы модулей:

main.cpp
// main.cpp — imports the module
import math_utils;

#include <iostream>  // can still use #include for non-modularized headers

int main() {
    double area = circle_area(5.0);
    std::cout << "Circle area: " << area << "\n";

    Point p1{0.0, 0.0};
    Point p2{3.0, 4.0};
    std::cout << "Distance: " << p1.distance_to(p2) << "\n";  // 5.0

    std::cout << "Triangle: " << geometry::triangle_area(10, 5) << "\n";

    // helper_function(42);  // ERROR: not exported, not visible

    return 0;
}

// Compile order matters for modules:
// 1. Compile the module interface first:
//    g++ -std=c++20 -fmodules-ts -c math_utils.cppm
// 2. Then compile the consumer:
//    g++ -std=c++20 -fmodules-ts main.cpp -o main

Модульные разделы

Большие модули можно разделить на разделы — подмодули, которые вместе образуют полный интерфейс модуля. Перегородки являются внутренними для модуля и не могут быть импортированы внешними потребителями напрямую:

module_partitions.cppm
// --- math_utils-algebra.cppm (partition interface) ---
export module math_utils:algebra;

export double square(double x) { return x * x; }
export double cube(double x) { return x * x * x; }

// --- math_utils-trig.cppm (partition interface) ---
export module math_utils:trig;

import <cmath>;

export double deg_to_rad(double degrees) {
    return degrees * std::numbers::pi / 180.0;
}

export double sin_deg(double degrees) {
    return std::sin(deg_to_rad(degrees));
}

// --- math_utils.cppm (primary module interface) ---
export module math_utils;

// Re-export partitions — consumers see everything
export import :algebra;
export import :trig;

// Additional exports can go here
export double hypotenuse(double a, double b) {
    return std::sqrt(square(a) + square(b));
}

// --- Consumer ---
// import math_utils;  // gets algebra, trig, and hypotenuse

Импортный std (C++23)

C++23 представляет import std;, который импортирует всю стандартную библиотеку в качестве модуля. Это изменит правила игры для скорости компиляции:

import std;  // Everything: iostream, vector, string, algorithm, ...

int main() {
    std::println("Hello from C++23 modules!");
    std::vector<int> v{1, 2, 3};
    std::ranges::sort(v);
}

import std; заменяет десятки директив #include одним импортом, который компилируется значительно быстрее, поскольку стандартный библиотечный модуль предварительно компилируется. Существует также import std.compat;, который также предоставляет имена библиотек C в глобальном пространстве имен (например, printf, strlen).

Подводный камень

Поддержка компиляторов продолжается: По состоянию на 2024 год GCC, Clang и MSVC поддерживают все модули, но с различными уровнями полноты и разной системной интеграцией сборки. MSVC имеет самую зрелую поддержку.

Строим системную интеграцию: CMake 3.28+ поддерживает модули с функцией CXX_MODULE_SETS, но экосистема (сканеры зависимости, менеджеры пакетов) все еще догоняет. Построение порядка сейчас имеет значение — интерфейсы модулей должны быть составлены до их потребителей.

Никакого макроэкспорта: Модули намеренно не экспортируют макросы #define. Код, который опирается на макросы из заголовков (например, WIN32_LEAN_AND_MEAN или NOMINMAX), должен обрабатывать это по-разному.

Стратегия миграции: начните с import std; (C++23), чтобы заменить стандартную библиотеку, а затем постепенно модулируйте свой собственный код, начиная с библиотек листьев, которые имеют несколько зависимостей.

Лучшая практика

При принятии модулей:

1. Сохраняйте интерфейсы модулей бережливыми — экспортируйте только то, что нужно потребителям. Неэкспортируемые организации имеют модульную связь и не могут привести к нарушениям ODR.
2. Используйте разделы для больших модулей — разделенные на логические подмодули (например, :types, :algorithms, :io).
3. Предпочтите export namespace — экспортируйте целые пространства имен, а не отдельные декларации, чтобы сохранить интерфейс организованным.
4. Не смешивайте #include и import для одной и той же библиотеки - выберите одну и будьте последовательны в блоке перевода.
5. Используйте расширения .cppm или .ixx для файлов интерфейса модулей, чтобы системы могли автоматически идентифицировать их.

Ключевые выводы
  • Модули заменяют #include на import — без включения текста, без утечки макросов, без ошибок включения порядка
  • Использовать export, чтобы сделать декларации видимыми для импортеров; неэкспортируемые организации являются модульно-частными
  • Модульные разделы разделяют большие модули на подкомпоненты, представляя унифицированный интерфейс
  • import std; (C++23) заменяет всю стандартную библиотеку одним быстрым импортом.
  • Строительные системы должны компилировать интерфейсы модулей перед потребителями - вопросы порядка компиляции

Тест — проверьте свои знания

(15 XP)

1. В чем преимущество модулей перед #include?

2. Что происходит с функцией, определенной в модуле, но не отмеченной `export`?

3. Что делает `import std;` в C++23?