Visão geral dos módulos no C++

O C++20 apresenta módulos. Um módulo é um conjunto de arquivos de código-fonte compilados independentemente dos arquivos de origem (ou, mais precisamente, as unidades de tradução que os importam.

Os módulos eliminam ou reduzem muitos dos problemas associados ao uso de arquivos de cabeçalho. Eles geralmente reduzem os tempos de compilação, às vezes significativamente. Os macros, diretivas de pré-processador e nomes não exportados declarados em um módulo não são visíveis fora do módulo. Eles não têm nenhum efeito na compilação da unidade de tradução que importa o módulo. Você pode importar módulos em qualquer ordem sem se preocupar com redefinições de macro. As declarações na unidade de tradução de importação não participam da resolução de sobrecarga nem da pesquisa de nome no módulo importado. Depois que um módulo é compilado uma vez, os resultados são armazenados em um arquivo binário que descreve todos os tipos, funções e modelos exportados. O compilador pode processar esse arquivo muito mais rápido do que um arquivo de cabeçalho. Além disso, o compilador pode reutilizá-lo em todos os locais onde o módulo é importado em um projeto.

Você pode usar módulos lado a lado com arquivos de cabeçalho. Um arquivo de origem C++ pode import módulos e também #include arquivos de cabeçalho. Em alguns casos, você pode importar um arquivo de cabeçalho como um módulo, o que é mais rápido do que usar #include para processá-lo com o pré-processador. Recomendamos que você use módulos em novos projetos em vez de arquivos de cabeçalho o máximo possível. Para projetos existentes de maior porte em desenvolvimento ativo, experimente converter cabeçalhos herdados em módulos. Para decidir quanto à adoção, pondere se você obtém uma redução significativa nos tempos de compilação.

Para comparar os módulos com outras maneiras de importar a biblioteca padrão, confira Comparar unidades de cabeçalho, módulos e cabeçalhos pré-compilados.

Habilitar módulos no compilador do Microsoft C++

A partir do Visual Studio 2022 versão 17.1, os módulos padrão C++20 são totalmente implementados no compilador do Microsoft C++.

Antes de ser especificado pelo C++20 padrão, a Microsoft tinha suporte experimental para módulos. O compilador também deu suporte à importação de módulos predefinidos da Biblioteca Padrão, descritos abaixo.

A partir do Visual Studio 2022 versão 17.5, a importação da Biblioteca Padrão como um módulo é padronizada e totalmente implementada no compilador do Microsoft C++. Esta seção descreve o método experimental mais antigo, que ainda tem suporte. Para obter informações sobre a nova maneira padronizada de importar a Biblioteca Padrão usando módulos, confira Importar a biblioteca padrão C++ usando módulos.

Você pode usar o recurso de módulos para criar módulos de partição única e importar os módulos da Biblioteca Padrão fornecidos pela Microsoft. Para habilitar o suporte para módulos da Biblioteca Padrão, compile com /experimental:module e /std:c++latest. Em um projeto do Visual Studio, clique com o botão direito do mouse no nó do projeto no Gerenciador de Soluções e escolha Propriedades. Defina a lista suspensa Configuração como Todas as Configurações e escolha Propriedades de Configuração>C/C++>Linguagem>Habilitar Módulos C++ (experimental).

Um módulo e o código que o consome devem ser compilados com as mesmas opções do compilador.

Consumir a Biblioteca Padrão C++ como módulos (experimental)

Esta seção descreve a implementação experimental, que ainda tem suporte. A nova maneira padronizada de consumir a Biblioteca Padrão C++ como módulos é descrita em Importar a biblioteca padrão C++ usando módulos.

Ao importar a Biblioteca Padrão do C++ como módulos, em vez de incluí-la por meio de arquivos de cabeçalho, você pode acelerar os tempos de compilação dependendo do tamanho do projeto. A biblioteca experimental é dividida nos seguintes módulos nomeados:

  • std.regex fornece o conteúdo do cabeçalho <regex>
  • std.filesystem fornece o conteúdo do cabeçalho <filesystem>
  • std.memory fornece o conteúdo do cabeçalho <memory>
  • std.threading fornece o conteúdo dos cabeçalhos <atomic>, <condition_variable>, <future>, <mutex>, <shared_mutex> e <thread>
  • std.core fornece todo o resto na Biblioteca Padrão do C++

Para consumir esses módulos, adicione uma declaração de importação à parte superior do arquivo de código-fonte. Por exemplo:

import std.core;
import std.regex;

Para consumir os módulos da Biblioteca Padrão da Microsoft, compile seu programa com as opções /EHsc e /MD.

Exemplo

O exemplo a seguir mostra uma definição de módulo simples em um arquivo de origem chamado Example.ixx. A extensão .ixx é necessária para arquivos de interface de módulo no Visual Studio. Neste exemplo, o arquivo de interface contém a definição da função e a declaração. No entanto, você também pode colocar as definições em um ou mais arquivos de implementação de módulo separados, conforme mostrado em um exemplo posterior.

A instrução export module Example; indica que esse arquivo é a interface primária de um módulo chamado Example. O modificador export no f() indica que essa função fica visível quando outro programa ou módulo importa Example.

// Example.ixx
export module Example;

#define ANSWER 42

namespace Example_NS
{
   int f_internal() {
        return ANSWER;
      }

   export int f() {
      return f_internal();
   }
}

O arquivo MyProgram.cpp usa import para acessar o nome exportado pelo Example. O nome do namespace Example_NS está visível aqui, mas nem todos dos membros porque eles não foram exportados. Além disso, a macro ANSWER não está visível porque as macros não foram exportadas.

// MyProgram.cpp
import Example;
import std.core;

using namespace std;

int main()
{
   cout << "The result of f() is " << Example_NS::f() << endl; // 42
   // int i = Example_NS::f_internal(); // C2039
   // int j = ANSWER; //C2065
}

A declaração import só pode aparecer no escopo global.

Gramática de módulos

module-name:
module-name-qualifier-seqoptidentifier

module-name-qualifier-seq:
identifier .
module-name-qualifier-seq identifier .

module-partition:
: module-name

module-declaration:
exportoptmodulemodule-namemodule-partitionoptattribute-specifier-seqopt;

module-import-declaration:
exportoptimportmodule-nameattribute-specifier-seqopt;
exportoptimportmodule-partitionattribute-specifier-seqopt;
exportoptimportheader-nameattribute-specifier-seqopt;

Implementando módulos

Uma interface de módulo exporta o nome do módulo e todos os namespaces, tipos, funções e assim por diante que compõem a interface pública do módulo.
Uma implementação de módulo define as coisas exportadas pelo módulo.
Em sua forma mais simples, um módulo pode ser um único arquivo que combina a interface e a implementação do módulo. Você também pode colocar a implementação em um ou mais arquivos de implementação do módulo separados, semelhante a como os arquivos .h e .cpp fazem.

Para módulos maiores, você pode dividir partes do módulo em submódulos chamados partições. Cada partição consiste em um arquivo de interface do módulo que exporta o nome da partição do módulo. Uma partição também pode ter um ou mais arquivos de implementação de partição. O módulo como um todo tem uma interface de módulo principal, que é a interface pública do módulo. Ele pode exportar as interfaces da partição, se desejado.

Um módulo consiste de uma ou mais unidades de módulo. Uma unidade de módulo é uma unidade de tradução (um arquivo de origem) que contém uma declaração de módulo. Há vários tipos de unidades de módulo:

  • Uma unidade de interface do módulo exporta um nome do módulo ou nome da partição do módulo. Uma unidade de interface de módulo tem export module na respectiva declaração de módulo.
  • Uma unidade de implementação do módulo não exporta um nome do módulo ou um nome da partição do módulo. Como o nome indica, ele implementa um módulo.
  • Uma unidade de interface do módulo primário exporta o nome do módulo. Precisa haver uma e apenas uma unidade de interface do módulo primário em um módulo.
  • Uma unidade de interface da partição do módulo exporta um nome da partição do módulo.
  • Uma unidade de implementação da partição do módulo tem um nome da partição do módulo em sua declaração do módulo, mas nenhuma palavra-chave export.

A palavra-chave export é usada apenas em arquivos de interface. Um arquivo de implementação pode import outro módulo, mas não pode export nenhum nome. Os arquivos de implementação podem ter qualquer extensão.

Módulos, namespaces e pesquisa dependente de argumentos

As regras para namespaces nos módulos são as mesmas de qualquer outro código. Se uma declaração em um namespace for exportada, o namespace delimitador (excluindo membros que não foram explicitamente exportados nesse namespace) também será exportado implicitamente. Se um namespace for exportado explicitamente, todas as declarações dentro dessa definição de namespace serão exportadas.

Quando o compilador faz uma pesquisa dependente de argumento para resoluções de sobrecarga na unidade de tradução de importação, ele considera as funções declaradas na mesma unidade de tradução (incluindo as interfaces do módulo) como o local onde os argumentos da função são definidos.

Partições de módulo

Uma partição de módulo é semelhante a um módulo, exceto:

  • Ela compartilha a propriedade de todas as declarações em todo o módulo.
  • Todos os nomes exportados pelos arquivos de interface da partição são importados e exportados pelo arquivo de interface principal.
  • O nome de uma partição precisa começar com o nome do módulo seguido de dois-pontos (:).
  • As declarações em qualquer uma das partições ficam visíveis em todo o módulo.\
  • Não são necessárias precauções especiais para evitar erros de ODR (regra de definição única). Você pode declarar um nome (função, classe e assim por diante) em uma partição e defini-lo em outra.

Um arquivo de implementação de partição começa dessa forma e é uma partição interna de uma perspectiva de padrões C++:

module Example:part1;

Um arquivo de interface da partição começa assim:

export module Example:part1;

Para acessar declarações em outra partição, uma partição deve importá-las. Mas elas só pode usar o nome da partição, não o nome do módulo:

module Example:part2;
import :part1;

A unidade de interface primária precisa importar e exportar novamente todos os arquivos de partição da interface do módulo da seguinte forma:

export import :part1;
export import :part2;

A unidade de interface primária pode importar arquivos de implementação de partição, mas não pode exportá-los. Esses arquivos não têm permissão para exportar nenhum nome. Essa restrição permite que um módulo mantenha os detalhes da implementação internos no módulo.

Módulos e arquivos de cabeçalho

Você pode incluir arquivos de cabeçalho em um arquivo de origem do módulo colocando uma diretiva #include antes da declaração do módulo. Esses arquivos são considerados como estando no fragmento do módulo global. Um módulo só pode ver os nomes no fragmento do módulo global que estão nos cabeçalhos que ele inclui explicitamente. O fragmento do módulo global contém apenas símbolos que são usados.

// MyModuleA.cpp

#include "customlib.h"
#include "anotherlib.h"

import std.core;
import MyModuleB;

//... rest of file

Você pode usar um arquivo de cabeçalho tradicional para controlar quais módulos são importados:

// MyProgram.h
import std.core;
#ifdef DEBUG_LOGGING
import std.filesystem;
#endif

Arquivos de cabeçalho importados

Alguns cabeçalhos são suficientemente autocontidos que podem ser trazidos usando a palavra-chave import. A principal diferença entre um cabeçalho importado e um módulo importado é que quaisquer definições do pré-processador no cabeçalho ficam visíveis no programa de importação imediatamente após a instrução import.

import <vector>;
import "myheader.h";

Confira também

module, import, export
Tutorial de módulos nomeados
Comparar unidades de cabeçalho, módulos e cabeçalhos pré-compilados