Última atualização:2026-10-12
Módulos (Modules) no C++20¶
Quando usamos cabeçalhos escritos por nós mesmos ou bibliotecas de terceiros, geralmente recorremos à diretiva #include para importar a biblioteca — uma forma que todos usam com frequência. Na prática, esse método copia todo o código de um arquivo-fonte (cabeçalho) para dentro de outro arquivo.
Isso traz os seguintes problemas:
- O arquivo-fonte pode ser incluído várias vezes no mesmo destino; por isso, normalmente usamos
#pragma onceou#ifndefpara impedir que o arquivo-fonte seja incluído várias vezes na mesma unidade de tradução. - A cópia de código torna o tempo de compilação mais longo: basta modificar um cabeçalho para que arquivos que o incluem indiretamente precisem ser recompilados.
- Problemas de ordem de
#include; às vezes nos deparamos com erros de compilação inexplicáveis.
O C++20 introduziu uma nova forma de substituir a diretiva #include, chamada módulos.
Vamos aprender módulos em profundidade a seguir.
1.Unidades de módulo¶
Um módulo C++ é composto por uma ou mais unidades de tradução (tu), que contêm palavras-chave específicas para a declaração do módulo. Essas unidades de tradução são chamadas de unidades de módulo.
Unidades de tradução que não são unidades de módulo são consideradas parte do módulo global; o módulo global é anônimo, não possui interface e contém o código comum não modular.
1.1 Global Module Fragment¶
Uma unidade de módulo pode ter um fragmento de módulo global como prefixo; quando não é possível importar cabeçalhos (especialmente quando o cabeçalho usa macros de pré-processador como configuração), esse fragmento de módulo global permite usar diretamente o código original.
Por exemplo: no código abaixo, o trecho entre module; e export module Foo; é o global module fragment.
module;
#include <iostream>
#ifdef Say
void hello();
#endif
export module Foo;
// purivew
void world();
Um ponto que merece atenção: se o módulo tem um fragmento de módulo global, a primeira declaração deve ser module;; ou seja, colocar essa declaração em outra posição causa erro.
1.2 purview¶
Pode-se entender o purview como todo o escopo do módulo. Começa na declaração do módulo e se estende até o fim da unidade de tradução.
Por exemplo: hello não está no purview; world e GetData estão.
module;
void hello();
// <- 这里不在Foo模块的purview内
export module Foo;
// <- 在Foo模块的purview内
void world();
export void GetData();
1.3 Private module fragment¶
A unidade de interface do módulo principal pode ter um fragmento de módulo privado como sufixo; essa parte só pode aparecer na unidade de interface do módulo principal e, se existir, a unidade de módulo em que ela aparece deve ser a única unidade do módulo. Seu objetivo é encapsular a interface e a implementação do módulo em uma única unidade de tradução, sem expor os detalhes da implementação.
Por exemplo: quero criar um Shape e calcular sua área.
Para o mundo externo, basta expor uma interface que crie um Shape concreto, chamando a interface comum de cálculo de área; assim, podemos escrever o módulo a seguir.
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);
}
Colocar todos os detalhes internos dentro do private? Minha versão do g++ é a 13 e, por enquanto, não há suporte: ela relata o erro a seguir:
Para a situação atual de suporte do gcc, clique aqui
O clang local é a versão 16; testei e funciona normalmente!
➜ 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
Dessas três partes, os fragmentos de módulo global e privado não são indispensáveis para a existência do módulo; o purview é a parte obrigatória do módulo.
2.Uso de módulos¶
2.1 Criar um módulo¶
Criar um módulo é semelhante a definirmos um arquivo de cabeçalho: também é um arquivo, geralmente com o sufixo .cppm. Basta usar nesse arquivo as palavras-chave export e module, seguidas do nome do módulo, e assim criamos um módulo exportável. Por exemplo:
A palavra-chave export pode ser usada em classes, variáveis, etc.; normalmente há as duas formas de escrita a seguir:
Uma forma coloca a palavra-chave export antes de declarações comuns; a outra é o bloco de exportação, semelhante à forma de escrever namespace, podendo exportar vários conteúdos.
2.2 Exportar¶
Aqui surge uma questão importante: o que pode ser exportado?
- variables, classes, structs, functions, namespaces, template functions/classes, concepts podem ser exportados
- O que tem vinculação interna não pode ser exportado, como variáveis e funções static, e namespace anônimo.
- A declaração de exportação deve ocorrer no nível de namespace
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; // 不能导出成员变量
};
- Exportar um namespace exporta implicitamente todo o seu conteúdo
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}; // 隐式导出
};
- A primeira declaração da entidade exportada deve ser uma declaração de exportação. Declarações e definições subsequentes não precisam da palavra-chave 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
- Módulo não exportado não pode ser exportado
module foo;
export void print { } // error: 'export' may only occur after a module interface declaration
2.3 Importar¶
O correspondente é a importação; importar também tem suas regras; por exemplo:
-
Não é permitido importar o próprio módulo
-
Em uma unidade de módulo, todas as importações devem aparecer antes de qualquer declaração dessa unidade de módulo. Não é possível importar em um ponto qualquer da unidade de módulo.
void func() {}
import shape; // error: post-module-declaration imports must be contiguous
---------------
import shape; // ok
void func() {}
- Somente é permitida importação em escopo global
- Importações circulares não são permitidas
2.4 include em módulos¶
Como usar #include <iostream> em módulos?
2.4.1 #include na região purview¶
Substituir #include por import
Por exemplo:
Compilando com g++-13 como abaixo, é possível compilar para gcm passando iostream após c++-system-header.
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
Se for um cabeçalho seu, por exemplo: consts.h, descobri que ele pode ser colocado diretamente dentro do módulo com #include; por exemplo:
2.4.2 #inlcude na região Global Module Fragment¶
Para #include e macros, ambos podem ser usados diretamente nessa região; por exemplo:
3.Decomposição de módulos¶
Quando queremos decompor um módulo grande em módulos menores, podemos usar os dois métodos a seguir:
- Partições de módulo.
- Ou seja, permitem-nos dividir o módulo em vários arquivos. Na prática, isso é invisível para quem usa: basta importar o módulo normalmente.
- Submódulos.
- Ou seja, permitem-nos decompor módulos maiores em uma hierarquia com qualquer número de submódulos. Quem usa pode optar por importar o módulo inteiro ou apenas submódulos específicos.
3.1 Partições de módulo¶
Sintaxe:
Por exemplo: tenho um shape; para uso externo, basta import shape e então chamar as interfaces correspondentes; aqui foram usadas as interfaces da partição circle e da partição rectangle.
O shape tem duas partições: uma é circle e a outra é rectangle; assim, o conteúdo do módulo shape.cppm é:
O conteúdo das duas subpartições é:
// 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; }
Quando compilei com clang e g++, descobri que o clang-16 relata erro de compilação: não há suporte.
O g++-13 oferece suporte; note que, ao compilar, é preciso seguir a ordem subpartição -> partição principal, caso contrário ocorrerá erro.
➜ 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
Deveria ser:
3.2 Submódulos¶
Sintaxe:
export module <module-name>.<sub_module-name>;
import <module-name>.<sub_module-name>; // 可以在import前面添加export导出该分区接口
O exemplo acima também pode ser implementado com submódulos: mantemos shape como módulo principal e os dois submódulos são circle e 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; }
Pode-se ver que, no uso, as partições de módulo empregam :, enquanto os submódulos empregam ..
O código do lado que chama é o mesmo das partições de módulo.
Porém, há algumas diferenças no uso; por exemplo: quando uma subpartição é introduzida, usar sua interface provoca o erro: internal compiler error: Segmentation fault: 11, ao passo que o submódulo pode ser introduzido e usado normalmente.
// 引入子分区
import shape:circle;
int main() {
DrawCircle(); // error
return 0;
}
// 引入子模块
import shape.circle;
int main() {
DrawCircle(); // ok
return 0;
}
Para os submódulos, ao importar, não é permitido omitir o nome do módulo principal; acima, ao introduzir o módulo de partição na partição principal, podemos usar :circle, e aqui não é possível usar .circle. O erro é o seguinte:
4.Interface e implementação¶
Normalmente, ao escrever código, separamos o código em "arquivo de cabeçalho" e "arquivo de implementação"; para módulos, essa operação deixa de ser necessária. Mas ainda podemos seguir o estilo de escrita de antes.
Por exemplo: desenhar um shape.
- Definir o módulo shape.cppm
- Implementar o módulo shape.cc
import shape;
import <iostream>;
Shape::Shape() {}
void Shape::Draw() { std::cout << "draw a shape" << std::endl; }
Isso é basicamente semelhante ao modo de separação .h/.cpp que usamos no dia a dia.
https://clang.llvm.org/docs/StandardCPlusPlusModules.html
https://timsong-cpp.github.io/cppwp/n4861/module.import#7.sentence-2
Tópicos relacionados¶
- Introdução às corrotinas no C++20 | C++那些事
- Prática com channel no C++20 | C++那些事
- Concepts (conceitos) no C++20 | C++那些事
- constexpr e a expressão vírgula no C++20 | C++那些事
- Ordem de bytes (endian) no C++20 | C++那些事
- Expressões Lambda no C++20 (parte 1) | C++那些事
- Biblioteca Ranges do C++20 | C++那些事
- Design e armadilhas do std::span no C++20 | C++那些事
- Guia dos novos recursos do C++20 | C++那些事
- [[likely]] e [[unlikely]] no C++20 | C++那些事