Перейти к содержанию

Последнее обновление:2026-10-12

Модули Modules в C++20

Когда мы используем собственные заголовочные файлы или сторонние библиотеки, обычно применяется директива #include для подключения библиотеки — это всем привычный способ. На самом деле этот подход копирует весь код одного исходного файла (заголовочного файла) в другой файл.

Это приводит к следующим проблемам:

  • Исходный файл может включаться в одну цель несколько раз, поэтому мы обычно используем #pragma once или #ifndef, чтобы предотвратить многократное включение исходного файла в одну единицу трансляции.
  • Копирование кода увеличивает время компиляции: достаточно изменить один заголовочный файл — и все файлы, косвенно включающие его, будут перекомпилированы.
  • Проблемы порядка #include: иногда возникают непонятные ошибки компиляции.

C++20 привнёс новый способ заменить директиву #include — модули.

Давайте глубоко разберёмся с модулями.

1.Модульные единицы

Модуль C++ состоит из одной или нескольких единиц трансляции (tu), содержащих специальные ключевые слова для объявления модуля. Такие единицы трансляции называются модульными единицами (module units).

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

1.1 Global Module Fragment

Модульная единица может начинаться с глобального фрагмента модуля в качестве префикса; когда невозможно импортировать заголовочный файл (в частности, когда заголовочный файл использует препроцессорные макросы в качестве конфигурации), этот глобальный фрагмент модуля может напрямую использовать прежний код.

Например: в приведённом ниже коде участок между module; и export module Foo; — это global module fragment.

module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();

Обязательно нужно помнить: если у модуля есть глобальный фрагмент модуля, первым объявлением должно быть module;, то есть помещение этого объявления в другое место приведёт к ошибке.

1.2 purview

purview можно понимать как всю область модуля: она начинается с объявления модуля и простирается до конца единицы трансляции.

Например: hello не входит, а world и GetData входят в purview.

module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();

1.3 Private module fragment

Главная интерфейсная единица модуля может завершаться приватным фрагментом модуля в качестве суффикса; эта часть может появляться только в главной интерфейсной единице модуля, и если она есть, то модульная единица, в которой она появляется, должна быть единственной единицей этого модуля. Её цель — инкапсулировать интерфейс и реализацию модуля в одной единице трансляции, не раскрывая деталей реализации.

Например: я хочу создать Shape и вычислить его площадь.

Наружу достаточно expose'нуть только интерфейс создания конкретного Shape и вызывать общий интерфейс вычисления площади — так мы можем написать следующий модуль.

export module Shape;

export class Shape {
  public:
   virtual double CalculateArea() = 0;
};

export { std::shared_ptr<Shape> CreateRectangle(double length, double width); }


module :private; // here

class Rectangle : public Shape {
 private:
  double length;
  double width;

 public:
  Rectangle(double l, double w) : length(l), width(w) {}

  double CalculateArea() override { return length * width; }
};

std::shared_ptr<Shape> CreateRectangle(double length, double width) {
  return std::make_shared<Rectangle>(length, width);
}

Помещать ли все внутренние детали в private? Моя версия g++ — 13, она пока этого не поддерживает и выдаст такую ошибку:

Текущую поддержку в gcc можно посмотреть здесь

https://gcc.gnu.org/projects/cxx-status.html

shape.cppm:14:1: sorry, unimplemented: private module fragment
   14 | module :private;

Локальный clang у меня версии 16 — я проверил, и всё работает!

➜  clang++ -std=c++20 shape.cppm --precompile -o shape.pcm  
➜  clang++ -std=c++20 shape.cc -fprebuilt-module-path=. shape.pcm -o shape
➜  ./shape 
area is 2

Из трёх описанных частей глобальный и приватный фрагменты модуля не обязательны для существования модуля, а purview — обязательная часть модуля.

2.Использование модулей

2.1 Создание модуля

Создание модуля похоже на определение заголовочного файла: у него тоже есть свой файл, обычно с расширением .cppm. Нам достаточно использовать в этом файле ключевые слова export и module, после которых указать имя модуля, — так создаётся экспортируемый модуль. Например:

// foo.cppm
export module foo;

// main.cc
import foo;

Ключевое слово export может применяться к классам, переменным и т. д.; обычно есть две формы записи:

export void func();

export {
  void func();
  constexpr double PI{3.14};
};

Одна форма — ключевое слово export перед обычным объявлением; другая — блок экспорта, похожий по записи на namespace, позволяющий экспортировать сразу несколько сущностей.

2.2 Экспорт

Здесь возникает важный вопрос: что можно экспортировать?

  • variables, classes, structs, functions, namespaces, template functions/classes, concepts — можно экспортировать
  • Сущности с внутренней компоновкой не экспортируются, например static-переменные и функции, анонимные namespace.
export static constexpr double PI = 3.14; // 不可导出
  • Объявление экспорта должно происходить на уровне пространства имён
namespace {
  export void print_no_export() { // 匿名命名空间,不可导出
    std::cout << "print no export" << std::endl;
  }
};
namespace light {
  export void print_export() { // ok
    std::cout << "print export" << std::endl;
  }
};

struct Foo {
  export int a; // 不能导出成员变量
};
  • Экспорт пространства имён неявно экспортирует всё его содержимое
export class Rectangle {
 private:
  double length;
  double width;
 public:
  Rectangle(double l, double w) : length(l), width(w) {} 

  double CalculateArea() { return length * width; } // 隐式导出
};

export namespace {
  void print_export() { // 隐式导出
    std::cout << "print export" << std::endl;
  }
};

export {
  void func();  // 隐式导出
  constexpr double PI{3.14}; // 隐式导出
};
  • Первое объявление экспортируемой сущности должно быть объявлением экспорта. Последующим объявлениям и определениям ключевое слово export не требуется.
export class Foo; // ok
export class Foo; // ok,只是会冗余

class Foo { // ok,隐式export
 public:
  void print() { std::cout << "this is foo" << std::endl; }
};

class Bar;  // not export
export class Bar; // 无效,第一个声明已经是不可导出,后续的不可导出


Foo f; // ok
f.print(); // ok
Bar b; // not ok
  • Неэкспортируемый модуль не может экспортировать
module foo;

export void print { }  // error: 'export' may only occur after a module interface declaration

2.3 Импорт

Ему в пару идёт импорт; у импорта тоже есть правила, например:

  • Нельзя импортировать самого себя

  • В модульной единице все импорты должны появляться до любых объявлений в этой модульной единице. Нельзя импортировать в произвольной точке модульной единицы.

void func() {}
import shape; // error: post-module-declaration imports must be contiguous

---------------
import shape; // ok
void func() {}
  • Разрешён только импорт в глобальной области видимости
namespace light {
  import shape; // not ok
};
  • Циклический импорт не допускается
// shape.cppm
export module shape;
import circle; // 循环导入

// circle.cppm
import shape;

2.4 include в модулях

Как использовать #include <iostream> в модуле?

2.4.1 #include в области purview

Замените #include на import

export module foo;
import <iostream>;

Например:

Компиляция с g++-13: можно с помощью c++-system-header и следующего за ним iostream скомпилировать в gcm.

g++-13 -std=c++20 -fmodules-ts -x c++-system-header iostream 
g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm shape.cc

Если это ваш собственный заголовочный файл, например consts.h, выясняется, что его можно прямо разместить в модуле через #include, например:

export module foo;
#include "consts.h"

2.4.2 #inlcude в области Global Module Fragment

В этой области можно напрямую использовать #include и макросы, например:

module;
#ifdef
#include <iostream>
#endif
export module foo;

3.Разложение модулей

Когда мы хотим разбить большой модуль на более мелкие, можно воспользоваться двумя способами:

  • Разделение модуля на части (partitions).
  • То есть можно разложить модуль на несколько файлов. При этом для пользователя это фактически незаметно: при использовании достаточно обычного импорта модуля.
  • Подмодули.
  • То есть можно разложить большой модуль на иерархию из любого числа подмодулей. Пользователь может импортировать весь модуль целиком или только конкретные подмодули.

3.1 Разделение модуля на части

Синтаксис:

export module <module-name>:<part-name>;
import :<part-name>; // 可以在import前面添加export导出该分区接口

Например: у меня есть shape; для внешнего использования достаточно import shape и вызова соответствующих интерфейсов — здесь используются интерфейсы разделов circle и rectangle.

import shape;

int main() {
    DrawCircle();
    DrawRectangle();
    return 0;
}

У shape есть два раздела: circle и rectangle, поэтому содержимое модуля shape.cppm таково:

export module shape;

export import :circle;
export import :rectangle;

Содержимое двух подразделов:

// circle.cppm
module;
#include <iostream>
export module shape:circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

--------------
// rectangle.cppm

module;
#include <iostream>
export module shape:rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

Скомпилировав с clang и g++, я обнаружил, что clang-16 выдаёт ошибку компиляции — не поддерживает.

 error: sorry, module partitions are not yet supported

g++-13 поддерживает; обратите внимание, что при компиляции нужно следовать порядку подраздел -> главный раздел, иначе возникнет ошибка.

➜ g++-13 -fmodules-ts -std=c++20 -x c++ shape.cppm circle.cppm rectangle.cppm shape.cc
shape:circle: error: failed to read compiled module: No such file or directory
shape:circle: note: compiled module file is 'gcm.cache/shape-circle.gcm'
shape:circle: note: imports must be built before being imported
shape:circle: fatal error: returning to the gate for a mechanical issue

Следует писать так:

g++ -fmodules-ts -std=c++20 -x c++ circle.cppm rectangle.cppm shape.cppm shape.cc

3.2 Подмодули

Синтаксис:

export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口

Приведённый выше пример можно реализовать и через подмодули: оставим shape главным модулем, а двумя подмодулями будут circle и rectangle.

// shape.cppm
export module shape;

export import shape.circle;
export import shape.rectangle;

---------------------
// rectangle.cppm
module;
#include <iostream>
export module shape.rectangle;
export void DrawRectangle() { std::cout << "draw rectangle" << std::endl; }

---------------------

// circle.cppm
module;
#include <iostream>
export module shape.circle;
export void DrawCircle() { std::cout << "draw circle" << std::endl; }

Видно, что при использовании разделы модуля применяют :, а подмодули — ..

Код вызывающей стороны такой же, как для разделов модуля.

Однако при использовании между ними есть различия: например, при импорте подраздела использование его интерфейса вызывает ошибку internal compiler error: Segmentation fault: 11, тогда как подмодуль импортируется и используется нормально.

// 引入子分区
import shape:circle;

int main() {
    DrawCircle(); // error
    return 0;
}


// 引入子模块
import shape.circle;

int main() {
    DrawCircle(); // ok
    return 0;
}

Для подмодулей при import нельзя опускать имя главного модуля; выше в главном разделе мы импортировали модуль-раздел и могли использовать :circle, а здесь нельзя использовать .circle. Ошибка такова:

shape.cppm:3:8: error: 'import' does not name a type
    3 | export import .circle;

4.Интерфейс и реализация

Обычно при написании кода мы разделяем его на «заголовочный файл» и «файл реализации»; для модулей это больше не требуется, но можно по-прежнему придерживаться прежнего стиля.

Например: нарисовать shape.

  • Определяем модуль shape.cppm
export module shape;

export class Shape {
 public:
  Shape();
  void Draw();
};
  • Реализуем модуль shape.cc
import shape;
import <iostream>;

Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }

Это практически аналогично привычному разделению на .h и .cpp.

https://clang.llvm.org/docs/StandardCPlusPlusModules.html

https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2

https://gcc.gnu.org/projects/cxx-status.html

Похожие темы

Комментарии