C++ Módulos C++20

Desde os anos 1970, programas em C e C++ compartilham código com #include: o pré-processador copia e cola o texto de um arquivo dentro do outro. Funciona, mas é lento e cheio de armadilhas. O C++20 trouxe uma forma nova de organizar o código, os módulos: você diz o que um arquivo exporta ("exporta", deixa ver) e os outros fazem import ("importam"). Esta aula mostra o problema que os módulos resolvem, como usá-los com o g++ 15 (só o que conferimos que funciona) e, com honestidade, em que pé está o suporte em 2026.

Você vai aprender
  • Os problemas do #include: texto copiado, cabeçalho incluído duas vezes, macros que vazam.
  • Escrever um módulo com export module e export.
  • Usá-lo com import, e o que fica escondido.
  • Compilar com o g++ 15: a opção -fmodules e a ordem de compilação.
  • import std; C++23: a biblioteca padrão inteira numa linha.
  • Compilar módulos com CMake (e por que o Make não serve).
  • O estado do suporte nos compiladores e ferramentas em 2026.

Tudo foi conferido com o g++ 15.2, o clang++ 21.1 e o CMake 4.2 na máquina fictícia da Ana, na pasta ~/cpp. Esta aula pressupõe que você já viu Arquivos .h e .cpp, Compilar Vários Arquivos, O Pré-processador e CMake.

Módulos não rodam no editor do guia

Módulos exigem compilar os arquivos numa ordem certa, com opções especiais, e o editor "Experimente" compila tudo de uma vez. Por isso os exemplos com import aparecem só no terminal do laboratório. As caixas de "Experimente" desta aula mostram, com arquivos .h, os problemas que os módulos resolvem.


A ideia: fotocópia × biblioteca com balcão

O #include é como fotocopiar o livro inteiro toda vez que você quer consultar uma receita: cada arquivo .cpp que diz #include <iostream> recebe uma cópia completa do texto, e o compilador relê tudo, de novo, em cada arquivo. E a cópia vem com tudo que estava no livro, inclusive anotações de rodapé (as macros) que se espalham pelo seu texto.

Um módulo é como uma biblioteca com balcão: o livro é catalogado uma vez só (compilado), e quem pede recebe exatamente o que o catálogo oferece (o que foi exportado). O resto fica lá dentro.


1. O problema dos cabeçalhos

O texto copiado é enorme

O comando g++ -E mostra o arquivo depois que o pré-processador fez as "colagens" (aula O Pré-processador). Veja o tamanho do menor programa do guia:

$ cat ola.cpp
#include <iostream>
int main() { std::cout << "Olá, mundo!\n"; }
$ g++ -std=c++23 -E ola.cpp | wc -l
68843                                « linhas que o compilador realmente lê
$ g++ -std=c++23 -E ola.cpp | wc -c
1808549                              « cerca de 1,8 MB de texto, por causa de uma linha de #include

Duas linhas viraram quase 69 mil. E num projeto com 100 arquivos .cpp que incluem <iostream>, esse texto é lido e analisado 100 vezes. É por isso que projetos C++ grandes demoram tanto para compilar.

O mesmo cabeçalho incluído duas vezes

Como o #include só cola texto, se um arquivo chega por dois caminhos, a definição aparece duas vezes. O exemplo tem três arquivos (clique nas abas):

Missão: o main.cpp inclui ponto.h e também forma.h, que inclui ponto.h de novo. Rode e veja o erro de redefinição. Depois conserte acrescentando #pragma once na primeira linha de ponto.h. A saída deve ser Área: 12.

#include <iostream>
#include "ponto.h"
#include "forma.h"

int main() {
    Retangulo r{{0, 0}, 4, 3};
    std::cout << "Área: " << r.largura * r.altura << '\n';
}
struct Ponto {
    int x;
    int y;
};
#include "ponto.h"

struct Retangulo {
    Ponto canto;
    int largura;
    int altura;
};
#pragma once
struct Ponto {
    int x;
    int y;
};

No laboratório, antes do conserto:

$ g++ -std=c++23 -Wall -Wextra main.cpp -o main
In file included from forma.h:1,
                 from main.cpp:3:
ponto.h:1:8: error: redefinition of ‘struct Ponto’
    1 | struct Ponto {
      |        ^~~~~
In file included from main.cpp:2:
ponto.h:1:8: note: previous definition of ‘struct Ponto’
    1 | struct Ponto {
      |        ^~~~~

Redefinition of ‘struct Ponto’ = "redefinição da struct Ponto": ela chegou uma vez por main.cpp:2 e outra por forma.h:1. Todo cabeçalho precisa de #pragma once (ou das "guardas" #ifndef/#define/#endif, aula Arquivos .h e .cpp) para se defender disso. Com módulos, esse problema não existe: importar duas vezes é o mesmo que importar uma.

Macros vazam

Um #define dentro de um cabeçalho vale para todo arquivo que o inclui, inclusive para o seu código que não tem nada a ver com ele:

$ cat config.h
#pragma once
#define LIMITE 10
$ cat main.cpp
#include <iostream>
#include "config.h"

int main() {
    int LIMITE = 50;
    std::cout << "Limite da Ana: " << LIMITE << '\n';
}
$ g++ -std=c++23 -Wall -Wextra main.cpp -o main
In file included from main.cpp:2:
main.cpp: In function ‘int main()’:
config.h:2:16: error: expected unqualified-id before numeric constant
    2 | #define LIMITE 10
      |                ^~
main.cpp:5:9: note: in expansion of macro ‘LIMITE’
    5 |     int LIMITE = 50;
      |         ^~~~~~

O pré-processador trocou o seu LIMITE por 10, e a linha virou int 10 = 50;. Por isso o g++ reclama de um "identificador" esperado antes de uma "constante numérica" e mostra a nota in expansion of macro ‘LIMITE’ ("na expansão da macro"). Módulos não exportam macros: o que acontece dentro de um módulo fica lá.


2. Escrever um módulo: export module

Um módulo é um arquivo de código que começa declarando seu nome e marca com export o que os outros podem usar:

export module matematica;

export int dobro(int x) {
    return x * 2;
}

export double media(double a, double b) {
    return (a + b) / 2;
}

int segredo() {
    return 42;
}
export module matematica;
"Este arquivo é a interface do módulo chamado matematica." Precisa ser a primeira declaração do arquivo.
export int dobro(int x) { … }
Uma função exportada: quem importar o módulo pode usá-la.
int segredo() { … }
Sem export: só existe dentro do módulo. Ninguém de fora vê.

Repare que não há .h separado nem #pragma once: o próprio módulo diz o que é público. O nome do arquivo não precisa ser igual ao do módulo, mas é costume. Muitos projetos usam extensões especiais para módulos (.cppm, .ixx, .mpp); o g++ aceita o .cpp comum, e o Clang e o CMake preferem .cppm.

3. Usar: import

#include <iostream>
import matematica;

int main() {
    std::cout << "Dobro de 21: " << dobro(21) << '\n';
    std::cout << "Média de 7 e 10: " << media(7, 10) << '\n';
}

import matematica; (sem #, sem aspas, com ponto e vírgula) traz tudo o que o módulo exportou. Dá para misturar #include e import no mesmo arquivo, como aqui.

Compilar com o g++ 15: a opção -fmodules

Com o comando padrão do guia, o g++ 15 ainda não entende import:

$ g++ -std=c++23 -Wall -Wextra main.cpp -o programa
main.cpp:2:1: error: ‘import’ does not name a type
    2 | import matematica;
      | ^~~~~~
main.cpp:2:1: note: C++20 ‘import’ only available with ‘-fmodules’, which is not yet enabled with ‘-std=c++20’
main.cpp: In function ‘int main()’:
main.cpp:5:37: error: ‘dobro’ was not declared in this scope
    5 |     std::cout << "Dobro de 21: " << dobro(21) << '\n';
      |                                     ^~~~~
main.cpp:6:41: error: ‘media’ was not declared in this scope
    6 |     std::cout << "Média de 7 e 10: " << media(7, 10) << '\n';
      |                                         ^~~~~

A nota é clara: C++20 ‘import’ only available with ‘-fmodules’, which is not yet enabled with ‘-std=c++20’ ("o import só está disponível com -fmodules, que ainda não é ligado pelo -std=c++20"). Mesmo com -std=c++23, o g++ 15 exige que você ligue os módulos à mão. (Em versões anteriores do g++, a opção se chamava -fmodules-ts; o g++ 15 ainda aceita esse nome antigo.)

A ordem importa

Ligando -fmodules, mas compilando o main.cpp primeiro:

$ g++ -std=c++23 -Wall -Wextra -fmodules main.cpp -o programa
In module imported at main.cpp:2:1:
matematica: error: failed to read compiled module: Arquivo ou diretório inexistente
matematica: note: compiled module file is ‘gcm.cache/matematica.gcm’
matematica: note: imports must be built before being imported
matematica: fatal error: returning to the gate for a mechanical issue
compilation terminated.
failed to read compiled module
"Falhou ao ler o módulo compilado." O Arquivo ou diretório inexistente vem do sistema, em português.
compiled module file is ‘gcm.cache/matematica.gcm’
O g++ procurou o módulo já compilado neste arquivo, e ele não existe.
imports must be built before being imported
A regra de ouro: "o que é importado precisa ser compilado antes".
returning to the gate for a mechanical issue
"Voltando ao portão por um problema mecânico": uma piada dos autores do g++ (como um avião que volta ao portão). Só quer dizer que a compilação parou.

Então, primeiro o módulo, depois quem o usa:

$ g++ -std=c++23 -Wall -Wextra -fmodules -c matematica.cpp     « 1. o módulo
$ ls
gcm.cache
main.cpp
matematica.cpp
matematica.o
segredo.cpp
$ ls gcm.cache
matematica.gcm                       « a "ficha do catálogo" do módulo
$ g++ -std=c++23 -Wall -Wextra -fmodules main.cpp matematica.o -o programa     « 2. o main, ligado ao módulo
$ ./programa
Dobro de 21: 42
Média de 7 e 10: 8.5
  1. -c matematica.cpp compila o módulo sem montar o programa (aula Compilar Vários Arquivos). Saem dois arquivos: matematica.o (o código de máquina das funções) e gcm.cache/matematica.gcm (a descrição do que o módulo exporta, que o import lê).
  2. Depois, o main.cpp é compilado: o import encontra o .gcm, e o matematica.o entra na montagem do programa.

O que não foi exportado fica escondido

$ cat segredo.cpp
import matematica;

int main() {
    return segredo();
}
$ g++ -std=c++23 -Wall -Wextra -fmodules segredo.cpp matematica.o -o segredo
segredo.cpp: In function ‘int main()’:
segredo.cpp:4:12: error: ‘segredo’ was not declared in this scope
    4 |     return segredo();
      |            ^~~~~~~

A função existe no módulo, mas como não tem export, para quem importa é como se não existisse. Com cabeçalhos, esconder uma função exigia truques (static, namespaces anônimos); com módulos, é o comportamento normal.


4. import std;: a biblioteca padrão inteira C++23

O C++23 transformou a biblioteca padrão inteira num módulo chamado std. Uma linha substitui todos os #include da biblioteca:

import std;

int main() {
    std::vector<int> notas = {7, 10, 4};
    std::ranges::sort(notas);
    std::println("Notas: {}", notas);
}

No g++ 15, o módulo std não vem pronto: na primeira vez, você precisa compilá-lo. Sem isso, o erro é o mesmo de antes (std: error: failed to read compiled module, compiled module file is ‘gcm.cache/std.gcm’). O código-fonte do módulo vem com o g++, no arquivo bits/std.cc, e a opção -fsearch-include-path manda o g++ procurá-lo nas pastas da biblioteca:

$ time g++ -std=c++23 -fmodules -fsearch-include-path -c bits/std.cc     « uma vez só

real	0m12,437s                       « a biblioteca inteira, em alguns segundos
user	0m7,054s
sys	0m0,522s
$ ls gcm.cache
matematica.gcm
std.gcm
$ g++ -std=c++23 -Wall -Wextra -fmodules ola_std.cpp -o ola_std
$ ./ola_std
Notas: [4, 7, 10]

E ficou mais rápido? Comparamos com o mesmo programa escrito com #include <algorithm>, <print> e <vector>:

$ time g++ -std=c++23 -Wall -Wextra ola_include.cpp -o ola_include

real	0m2,298s
user	0m2,127s
sys	0m0,171s
$ time g++ -std=c++23 -Wall -Wextra -fmodules ola_std.cpp -o ola_std

real	0m1,729s
user	0m1,553s
sys	0m0,176s

Num programa de um arquivo, cerca de meio segundo a menos, e com a biblioteca inteira disponível, contra só três cabeçalhos. O ganho aparece de verdade em projetos com centenas de arquivos, em que o std.gcm é aproveitado por todos. Os tempos variam de máquina para máquina.

import std; não traz as macros

Como módulos não exportam macros, coisas como assert (de <cassert>) e INT_MAX (de <climits>) não vêm com o import std;. Para elas, continue usando o #include correspondente, ou as alternativas sem macro (std::numeric_limits<int>::max()).


5. Módulos com CMake (e por que o Make não serve)

Em projetos reais, ninguém digita a ordem de compilação à mão. O problema é que, para saber a ordem, a ferramenta precisa ler os arquivos e descobrir quem importa quem. O Make não faz isso sozinho. O CMake 4.2 faz, desde que o gerador seja o Ninja:

cmake_minimum_required(VERSION 3.28)
project(contas CXX)

set(CMAKE_CXX_STANDARD 23)

add_executable(contas main.cpp)
target_sources(contas
    PUBLIC FILE_SET CXX_MODULES FILES matematica.cppm)

A novidade em relação à aula CMake é o target_sources(… FILE_SET CXX_MODULES FILES …): ele avisa que matematica.cppm é um módulo. O main.cpp é o mesmo de antes.

$ cmake -S . -B build -G Ninja
-- The CXX compiler identification is GNU 15.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.3s)
-- Generating done (0.0s)
-- Build files have been written to: /home/ana/cpp/contas/build
$ cmake --build build
[1/6] Scanning /home/ana/cpp/contas/matematica.cppm for CXX dependencies
[2/6] Scanning /home/ana/cpp/contas/main.cpp for CXX dependencies
[3/6] Generating CXX dyndep file CMakeFiles/contas.dir/CXX.dd
[4/6] Building CXX object CMakeFiles/contas.dir/matematica.cppm.o
[5/6] Building CXX object CMakeFiles/contas.dir/main.cpp.o
[6/6] Linking CXX executable contas
$ ./build/contas
Dobro de 21: 42
Média de 7 e 10: 8.5

Os passos 1 a 3 são a "leitura" (scanning): o CMake descobre que main.cpp importa matematica e, por isso, no passo 4 compila o módulo antes do main.cpp (passo 5). O CMake liga o -fmodules sozinho. Com o gerador padrão do Linux, que produz um Makefile, não funciona:

$ cmake -S . -B build-make
-- The CXX compiler identification is GNU 15.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.3s)
CMake Error in CMakeLists.txt:
  The target named "contas" has C++ sources that may use modules, but modules
  are not supported by this generator:

    Unix Makefiles

  Modules are supported only by Ninja, Ninja Multi-Config, and Visual Studio
  generators for VS 17.4 and newer.  See the cmake-cxxmodules(7) manual for
  details.  Use the CMAKE_CXX_SCAN_FOR_MODULES variable to enable or disable
  scanning.


-- Generating done (0.0s)
CMake Generate step failed.  Build files cannot be regenerated correctly.

A mensagem diz tudo: "o alvo contas tem fontes C++ que podem usar módulos, mas módulos não são suportados por este gerador: Unix Makefiles. Módulos só são suportados pelo Ninja, Ninja Multi-Config e Visual Studio 17.4 ou mais novo". Solução: -G Ninja (o pacote ninja-build no Ubuntu).


6. O estado do suporte em 2026, com honestidade

Módulos entraram no padrão em 2020, mas são uma mudança enorme para compiladores e ferramentas, e o suporte chegou devagar. A tabela separa o que conferimos no laboratório do que só conhecemos pela documentação:

FerramentaMódulos próprios (export module)import std;Como conferimos
g++ 15.2Funciona, com -fmodules e ordem manualFunciona, depois de compilar bits/std.cc uma vezTestado no laboratório (seções 3 e 4)
clang++ 21.1Funciona, com outra receita: --precompile gera um .pcm e -fmodule-file= o usaNão conferido no laboratório. Segundo a documentação do CMake 4.2, funciona a partir do Clang 18.1.2, com a biblioteca libc++ ou com a do g++Testado só o módulo próprio
Visual Studio (MSVC)Foi o primeiro a suportar, desde o Visual Studio 2019Suportado desde o Visual Studio 2022 versão 17.5, segundo a MicrosoftNão testado (o laboratório é Linux)
CMake 4.2Funciona com Ninja (e Visual Studio); não com MakefilesAinda experimental: a documentação do CMake 4.2 exige ligar uma opção "de teste" (CMAKE_EXPERIMENTAL_CXX_IMPORT_STD)Testado o módulo próprio com Ninja e o erro com Makefiles
Make puroSó escrevendo a ordem à mãoIdemNão há suporte automático
Editor do guiaNão usado nesta aula: o editor compila tudo de uma vez—

No clang++ 21, o mesmo módulo e o mesmo main.cpp (com o módulo salvo como matematica.cppm):

$ clang++ -std=c++23 --precompile matematica.cppm -o matematica.pcm
$ clang++ -std=c++23 -Wall -Wextra -fmodule-file=matematica=matematica.pcm main.cpp matematica.pcm -o programa_clang
$ ./programa_clang
Dobro de 21: 42
Média de 7 e 10: 8.5

Repare que cada compilador tem sua receita, e os arquivos intermediários (.gcm no g++, .pcm no Clang) não servem para o outro. É justamente por isso que, em projetos de verdade, quem cuida dos módulos é o CMake.

Então, devo usar módulos?

  • Para aprender e para projetos pessoais novos: vale experimentar, principalmente com CMake + Ninja.
  • Para projetos que precisam compilar em vários sistemas e compiladores: em 2026, a maioria dos projetos ainda usa #include. Editores, ferramentas de análise e bibliotecas externas ainda estão se adaptando.
  • Neste guia: todas as outras aulas usam #include, que é o que você vai encontrar em quase todo código C++ hoje. Os módulos são o futuro; os cabeçalhos ainda são o presente.

O padrão ainda tem outras peças que não vimos: dividir um módulo em vários arquivos (partitions, "partições", com export module matematica:geometria;) e importar cabeçalhos antigos como se fossem módulos (header units, import <iostream>;), cujo suporte é ainda menor.


Quando dá errado

error: ‘import’ does not name a type / note: C++20 ‘import’ only available with ‘-fmodules’

O que significa: o g++ 15 não liga módulos sozinho, nem com -std=c++23.

Como consertar: acrescente -fmodules ao comando.

error: failed to read compiled module / imports must be built before being imported

O que significa: o arquivo que importa foi compilado antes do módulo, ou o gcm.cache está em outra pasta (ele fica na pasta em que você rodou o g++). Para import std;, falta compilar bits/std.cc.

Como consertar: compile primeiro os módulos (g++ -std=c++23 -fmodules -c modulo.cpp), depois o resto, sempre da mesma pasta. Para std: g++ -std=c++23 -fmodules -fsearch-include-path -c bits/std.cc. Ou deixe o CMake + Ninja cuidar da ordem.

error: ‘x’ was not declared in this scope com o import funcionando

O que significa: a função ou classe existe no módulo, mas não foi exportada.

Como consertar: acrescente export na frente da declaração, dentro do módulo.

CMake Error … modules are not supported by this generator: Unix Makefiles

O que significa: o CMake só sabe ordenar módulos com Ninja ou Visual Studio.

Como consertar: apague a pasta de build e configure com cmake -S . -B build -G Ninja.


Referência rápida

EscritaO que faz
export module nome;Primeira linha da interface de um módulo
export int f();Deixa f visível para quem importa
export { … }Exporta tudo dentro das chaves
import nome;Usa o módulo
import std;A biblioteca padrão inteira (C++23), sem macros
g++ -std=c++23 -fmodules -c modulo.cppCompila o módulo (gera .o e gcm.cache/nome.gcm)
g++ -std=c++23 -fmodules main.cpp modulo.o -o progCompila quem importa e monta o programa
g++ -std=c++23 -fmodules -fsearch-include-path -c bits/std.ccPrepara o import std; no g++ 15
target_sources(alvo PUBLIC FILE_SET CXX_MODULES FILES m.cppm)Declara um módulo no CMake
cmake -S . -B build -G NinjaConfigura com o gerador que entende módulos

Exercícios

1. A primeira linha

Complete a primeira linha do módulo loja:

loja;

2. A opção do g++

Complete o comando para o g++ 15 aceitar import:

g++ -std=c++23 -c loja.cpp

3. Um módulo tem export int a(); e int b();. Quem faz import pode chamar…

4. Você compilou main.cpp (que importa loja) antes de loja.cpp. O que acontece?

5. Qual destes problemas do #include os módulos não resolvem sozinhos?

Missão final: a macro que vaza

Missão: o config.h define a macro LIMITE, que "invade" o main.cpp e estraga a variável da Ana. Rode e veja o erro. Depois conserte o config.h: troque a macro por uma constante de verdade com outro nome, constexpr int limite_padrao = 10; (aula constexpr). Uma constante respeita escopo e não troca texto às escondidas; e num módulo, nem macros nem nomes não exportados escapariam.

#include <iostream>
#include "config.h"

int main() {
    int LIMITE = 50;
    std::cout << "Limite da Ana: " << LIMITE << '\n';
}
#pragma once
#define LIMITE 10
#pragma once
constexpr int limite_padrao = 10;

A solução, no laboratório:

$ cat config.h
#pragma once
constexpr int limite_padrao = 10;
$ g++ -std=c++23 -Wall -Wextra main.cpp -o main
$ ./main
Limite da Ana: 50

Resumo

  • #include cola texto: dezenas de milhares de linhas relidas em cada arquivo, cabeçalhos duplicados e macros que vazam.
  • Um módulo começa com export module nome; e marca com export o que é público. Quem usa escreve import nome;.
  • No g++ 15: -fmodules, e o módulo é compilado antes de quem o importa (gera gcm.cache/nome.gcm).
  • import std; (C++23) funciona no g++ 15 depois de compilar bits/std.cc uma vez; não traz macros.
  • Em projetos, use CMake 4.2 com FILE_SET CXX_MODULES e o gerador Ninja; com Makefiles, não funciona.
  • Em 2026, g++, Clang e MSVC compilam módulos, cada um com sua receita; a maior parte do código C++ ainda usa #include.