Partilhar via


Tutorial: Importar a biblioteca padrão do C++ usando módulos na linha de comando

Saiba como importar a biblioteca padrão do C++ usando módulos da biblioteca do C++. Isso resulta em uma compilação mais rápida e é mais robusto do que usar arquivos de cabeçalho, unidades de cabeçalho ou PCH (cabeçalhos pré-compilados).

Neste tutorial, saiba mais sobre:

  • Como importar a biblioteca padrão como um módulo por meio da linha de comando.
  • Os benefícios de desempenho e usabilidade dos módulos.
  • Os dois módulos std e std.compat da biblioteca padrão e a diferença entre eles.

Pré-requisitos

Este tutorial exige o Visual Studio 2022 17.5 ou posterior.

Introdução aos módulos da biblioteca padrão

Os arquivos de cabeçalho sofrem com a semântica, que pode mudar dependendo das definições de macro, da ordem em que você os inclui e da compilação lenta. Os módulos resolvem esses problemas.

Já é possível importar a biblioteca padrão como um módulo em vez de como um emaranhado de arquivos de cabeçalho. Isso é muito mais rápido e robusto do que incluir arquivos de cabeçalho, unidades de cabeçalho ou PCH (cabeçalhos pré-compilados).

A biblioteca padrão C++23 introduz dois módulos chamados std e std.compat:

  • std exporta as declarações e os nomes definidos no namespace std da biblioteca padrão do C++, como std::vector. Ele também exporta o conteúdo de cabeçalhos de wrapper C, como <cstdio> e <cstdlib>, que fornecem funções como std::printf(). As funções C definidas no namespace global, como ::printf(), não são exportadas. Isso aprimora a situação em que a inclusão de um cabeçalho wrapper C como <cstdio>também inclui arquivos de cabeçalho C como stdio.h, que trazem as versões de namespace global C. Isso não é um problema se você importar std.
  • std.compat exporta tudo contido em std e adiciona os namespaces globais de runtime C, como ::printf, ::fopen, ::size_t, ::strlen etc. O módulo std.compat facilita o trabalho com bases de código que se referem a muitas funções/tipos de runtime C no namespace global.

O compilador importa toda a biblioteca padrão quando você usa import std; ou import std.compat; e faz isso mais rápido do que trazer um só arquivo de cabeçalho. É mais rápido trazer toda a biblioteca padrão com import std; (ou import std.compat) do que usar #include <vector>, por exemplo.

Como os módulos nomeados não expõem macros, macros como assert, errno, offsetof, va_arg e outras não ficam disponíveis quando você importa std ou std.compat. Veja Considerações sobre o módulo nomeado da biblioteca padrão para obter soluções alternativas.

Sobre os módulos do C++

Os arquivos de cabeçalho são o modo como as declarações e as definições são compartilhadas entre arquivos de origem no C++. Antes dos módulos da biblioteca padrão, você incluía a parte da biblioteca padrão necessária com uma diretiva como #include <vector>. Os arquivos de cabeçalho são frágeis e difíceis de redigir porque a semântica deles pode mudar dependendo da ordem que você os inclui ou se determinadas macros são definidas. Eles também atrasam a compilação porque são reprocessados por todos os arquivos de origem que os incluem.

O C++20 apresenta uma alternativa moderna chamada módulos. No C++23, pudemos aproveitar o suporte a módulos para apresentar módulos nomeados a fim representar a biblioteca padrão.

Assim como os arquivos de cabeçalho, os módulos permitem que você compartilhe declarações e definições entre arquivos de origem. Mas, ao contrário dos arquivos de cabeçalho, os módulos não são frágeis e são mais fáceis de compor porque a semântica deles não muda devido às definições de macro ou à ordem em que você os importa. O compilador pode processar módulos muito mais rápido do que pode processar arquivos #include, além de usar menos memória em tempo de compilação. Os módulos nomeados não expõem definições de macro ou detalhes de implementação privada.

Para obter informações detalhadas sobre os módulos, confira Visão geral dos módulos do C++. Esse artigo também aborda o consumo da biblioteca padrão do C++ como módulos, mas usa uma forma mais antiga e experimental de fazer isso.

Este artigo demonstra a nova e a melhor maneira de consumir a biblioteca padrão. Para obter mais informações sobre maneiras alternativas de consumir a biblioteca padrão, veja Comparar unidades de cabeçalho, módulos e cabeçalhos pré-compilados.

Importar a biblioteca padrão com std

Os exemplos a seguir demonstram como consumir a biblioteca padrão como um módulo usando o compilador de linha de comando. Para obter informações sobre como fazer isso no IDE do Visual Studio, veja Criar módulos da biblioteca padrão C++23 da ISO.

A instrução import std; ou import std.compat; importa a biblioteca padrão para seu aplicativo. Mas, primeiro, você deve compilar a biblioteca padrão chamada módulos em forma binária. As etapas a seguir demonstram como fazer isso.

Exemplo: como compilar e importar std

  1. Abra um Prompt de Comando de Ferramentas Nativas do x86 para VS: no menu Iniciar do Windows, digite x86 nativo e o prompt será exibido na lista de aplicativos. Verifique se o prompt se destina ao Visual Studio 2022 versão 17.5 ou superior. Você receberá erros se usar a versão errada do prompt. Os exemplos usados neste tutorial destinam-se ao shell do CMD.

  2. Crie um diretório, como %USERPROFILE%\source\repos\STLModules, e torne-o o diretório atual. Se você escolher um diretório no qual não tenha acesso de gravação, receberá erros durante a compilação.

  3. Compile o módulo nomeado std com o seguinte comando:

    cl /std:c++latest /EHsc /nologo /W4 /c "%VCToolsInstallDir%\modules\std.ixx"
    

    Se você receber erros, verifique se está usando a versão correta do prompt de comando.

    Compile o módulo nomeado std usando as mesmas configurações do compilador que você pretende usar com o código que importa o módulo compilado. Se você tiver uma solução multiprojeto, poderá compilar o módulo nomeado da biblioteca padrão uma vez e, depois, referenciá-lo em todos os seus projetos usando a opção do compilador /reference.

    Usando o comando do compilador anterior, o compilador produz dois arquivos:

    • std.ifc é a representação binária compilada da interface do módulo nomeado que o compilador consulta para processar a instrução import std;. Esse é um artefato somente de tempo de compilação. Ele não é fornecido com seu aplicativo.
    • std.obj contém a implementação do módulo nomeado. Adicione std.obj à linha de comando ao compilar o aplicativo de exemplo para vincular estaticamente a funcionalidade usada da biblioteca padrão ao aplicativo.

    As principais opções de linha de comando nesse exemplo são:

    Comutador Significado
    /std:c++:latest Use a última versão do padrão e da biblioteca de linguagem C++. Embora o suporte a módulos esteja disponível no /std:c++20, você precisa da biblioteca padrão mais recente para obter suporte aos módulos nomeados da biblioteca padrão.
    /EHsc Use o tratamento de exceções do C++, exceto para funções marcadas como extern "C".
    /W4 O uso de /W4 é geralmente recomendado, em especial para novos projetos, pois permite todos os avisos de nível 1, 2, 3 e a maioria dos avisos de nível 4 (informativos), o que pode ajudar você a detectar possíveis problemas com antecedência. Basicamente, ele fornece avisos semelhantes a lint que podem ajudar a garantir o menor número possível de defeitos de código difíceis de encontrar.
    /c Compile-o sem a vinculação, porque estamos apenas criando a interface do módulo nomeado binário neste ponto.

    Controle o nome do arquivo de objeto e o nome do arquivo de interface do módulo nomeado com as seguintes opções:

    • /Fo define o nome do arquivo de objeto. Por exemplo, /Fo:"somethingelse". Por padrão, o compilador usa o mesmo nome para o arquivo de objeto do arquivo de origem do módulo (.ixx) que você está compilando. No exemplo, o nome do arquivo de objeto é std.obj por padrão, porque estamos compilando o arquivo de módulo std.ixx.
    • /ifcOutput define o nome do arquivo de interface do módulo nomeado (.ifc). Por exemplo, /ifcOutput "somethingelse.ifc". Por padrão, o compilador usa o mesmo nome para o arquivo de interface do módulo (.ifc) que o arquivo de origem do módulo (.ixx) que você está compilando. No exemplo, o arquivo ifc gerado é std.ifc por padrão, porque estamos compilando o arquivo de módulo std.ixx.
  4. Importe a biblioteca std compilada criando primeiro um arquivo chamado importExample.cpp com o seguinte conteúdo:

    // requires /std:c++latest
    
    import std;
    
    int main()
    {
        std::cout << "Import the STL library for best performance\n";
        std::vector<int> v{5, 5, 5};
        for (const auto& e : v)
        {
            std::cout << e;
        }
    }
    

    No código anterior, import std; substitui #include <vector> e #include <iostream>. A instrução import std; disponibiliza toda a biblioteca padrão com uma instrução. Em geral, importar toda a biblioteca padrão é muito mais rápido do que processar um só arquivo de cabeçalho da biblioteca padrão, como #include <vector>.

  5. Compile o exemplo usando o seguinte comando no mesmo diretório da etapa anterior:

    cl /c /std:c++latest /EHsc /nologo /W4 /reference "std=std.ifc" importExample.cpp
    link importExample.obj std.obj
    

    Não é necessário especificar /reference "std=std.ifc" na linha de comando neste exemplo, porque o compilador procura automaticamente o arquivo .ifc correspondente ao nome do módulo especificado pela instrução import. Quando o compilador encontra import std;, ele pode encontrar std.ifc se ele está localizado no mesmo diretório do código-fonte. Se o arquivo .ifc estiver em um diretório diferente do código-fonte, use a opção do compilador /reference para referenciá-lo.

    Neste exemplo, compilar o código-fonte e vincular a implementação do módulo ao aplicativo são etapas separadas. No entanto, elas não precisam ser feitas assim. Você pode usar cl /std:c++latest /EHsc /nologo /W4 /reference "std=std.ifc" importExample.cpp std.obj para a compilação e a vinculação em uma só etapa. Porém, pode ser conveniente fazer a compilação e a vinculação separadamente, porque você só precisará compilar o módulo nomeado da biblioteca padrão uma vez e, depois, referenciá-lo no projeto ou em vários projetos, na etapa de vinculação do build.

    Se você estiver compilando um só projeto, poderá combinar as etapas de compilação do módulo nomeado da biblioteca padrão std e a etapa de compilação do aplicativo adicionando "%VCToolsInstallDir%\modules\std.ixx" à linha de comando. Coloque-o antes de qualquer arquivo .cpp que consuma o módulo std.

    Por padrão, o nome do executável de saída é retirado do primeiro arquivo de entrada. Use a opção do compilador /Fe para especificar o nome do arquivo executável desejado. Este tutorial mostra a compilação do módulo nomeado std como uma etapa separada, porque você só precisa compilar o módulo nomeado da biblioteca padrão uma vez e, depois, referenciá-lo no projeto ou em vários projetos. Porém, pode ser conveniente compilar tudo junto, como mostrado por esta linha de comando:

    cl /FeimportExample /std:c++latest /EHsc /nologo /W4 "%VCToolsInstallDir%\modules\std.ixx" importExample.cpp
    

    Dada a linha de comando anterior, o compilador produz um executável chamado importExample.exe. Ao executá-lo, ele produz a seguinte saída:

    Import the STL library for best performance
    555
    

Importe a biblioteca padrão e as funções C globais com std.compat

A biblioteca padrão C++ inclui a biblioteca padrão ISO C. O módulo std.compat fornece todas as funcionalidades do módulo std, como std::vector, std::cout, std::printf, std::scanf etc. No entanto, ele também fornece as versões de namespace global dessas funções, como ::printf, ::scanf, ::fopen, ::size_t etc.

O módulo nomeado std.compat é uma camada de compatibilidade fornecida para facilitar a migração de código existente que se refere a funções de runtime C no namespace global. Se você quiser evitar adicionar nomes ao namespace global, use import std;. Caso precise facilitar a migração de uma base de código que usa muitas funções de runtime C não qualificadas (namespace global), use import std.compat;. Isso fornece os nomes de runtime C de namespace global para que você não precise qualificar todos os nomes globais com std::. Se você não tiver nenhum código que use as funções de runtime C de namespace global, não será necessário usar import std.compat;. Caso você só chame algumas funções de runtime C no código, talvez seja melhor usar import std; e qualificar os poucos nomes do runtime C de namespace global que precisam dele com std::. Por exemplo, std::printf(). Se você receber um erro como error C3861: 'printf': identifier not found ao tentar compilar o código, considere o uso de import std.compat; para importar as funções de runtime C de namespace global.

Exemplo: como compilar e importar std.compat

Para usar import std.compat;, você precisa compilar o arquivo de interface do módulo encontrado no formato de código-fonte em std.compat.ixx. O Visual Studio fornece o código-fonte para o módulo, de modo que você possa compilar o módulo usando as configurações do compilador que correspondem ao seu projeto. As etapas são semelhantes àquelas usadas para a compilação do módulo nomeado std. O módulo nomeado std é compilado primeiro porque std.compat depende dele:

  1. Abra um Prompt de Comando de Ferramentas Nativas para VS: no menu Iniciar do Windows, digite x86 nativo e o prompt será exibido na lista de aplicativos. Verifique se o prompt se destina ao Visual Studio 2022 versão 17.5 ou superior. Você receberá erros do compilador se usar a versão errada do prompt.

  2. Crie um diretório para experimentar este exemplo, como %USERPROFILE%\source\repos\STLModules, e torne-o o diretório atual. Se você escolher um diretório no qual não tenha acesso de gravação, receberá erros.

  3. Compile os módulos nomeados std e std.compat com o seguinte comando:

    cl /std:c++latest /EHsc /nologo /W4 /c "%VCToolsInstallDir%\modules\std.ixx" "%VCToolsInstallDir%\modules\std.compat.ixx"
    

    Você deve compilar std e std.compat usando as mesmas configurações do compilador que pretende usar com o código que vai importá-los. Se você tiver uma solução multiprojeto, poderá compilá-los uma vez e, depois, referenciá-los em todos os seus projetos usando a opção do compilador /reference.

    Se você receber erros, verifique se está usando a versão correta do prompt de comando.

    O compilador gera quatro arquivos das duas etapas anteriores:

    • std.ifc é a interface do módulo nomeado binário compilado que o compilador consulta para processar a instrução import std;. O compilador também consulta std.ifc para processar import std.compat; porque std.compat se baseia no std. Esse é um artefato somente de tempo de compilação. Ele não é fornecido com seu aplicativo.
    • std.obj contém a implementação da biblioteca padrão.
    • std.compat.ifc é a interface do módulo nomeado binário compilado que o compilador consulta para processar a instrução import std.compat;. Esse é um artefato somente de tempo de compilação. Ele não é fornecido com seu aplicativo.
    • std.compat.obj contém a implementação. No entanto, a maior parte da implementação é fornecida por std.obj. Adicione std.obj à linha de comando ao compilar o aplicativo de exemplo para vincular estaticamente a funcionalidade que você usa na biblioteca padrão ao seu aplicativo.

    Controle o nome do arquivo de objeto e o nome do arquivo de interface do módulo nomeado com as seguintes opções:

    • /Fo define o nome do arquivo de objeto. Por exemplo, /Fo:"somethingelse". Por padrão, o compilador usa o mesmo nome para o arquivo de objeto do arquivo de origem do módulo (.ixx) que você está compilando. No exemplo, os nomes de arquivo de objeto são std.obj e std.compat.obj por padrão porque estamos compilando os arquivos de módulo std.ixx e std.compat.obj.
    • /ifcOutput define o nome do arquivo de interface do módulo nomeado (.ifc). Por exemplo, /ifcOutput "somethingelse.ifc". Por padrão, o compilador usa o mesmo nome para o arquivo de interface do módulo (.ifc) que o arquivo de origem do módulo (.ixx) que você está compilando. No exemplo, os arquivos ifc gerados são std.ifc e std.compat.ifc por padrão, porque estamos compilando os arquivos de módulo std.ixx e std.compat.ixx.
  4. Importe a biblioteca std.compat criando primeiro um arquivo chamado stdCompatExample.cpp com o seguinte conteúdo:

    import std.compat;
    
    int main()
    {
        printf("Import std.compat to get global names like printf()\n");
    
        std::vector<int> v{5, 5, 5};
        for (const auto& e : v)
        {
            printf("%i", e);
        }
    }
    

    No código anterior, import std.compat; substitui #include <cstdio> e #include <vector>. A instrução import std.compat; torna a biblioteca padrão e disponibiliza as funções de runtime C com uma só instrução. A importação desse módulo nomeado, que inclui a biblioteca padrão C++ e as funções de namespace global da biblioteca de runtime C, é mais rápida do que o processamento de um arquivo #include individual como #include <vector>.

  5. Compile o exemplo usando o seguinte comando:

    cl /std:c++latest /EHsc /nologo /W4 stdCompatExample.cpp
    link stdCompatExample.obj std.obj std.compat.obj
    

    Não foi necessário especificar std.compat.ifc na linha de comando porque o compilador procura automaticamente o arquivo .ifc que corresponde ao nome do módulo em uma instrução import. Quando o compilador encontra import std.compat;, ele encontra std.compat.ifc, já que o colocamos no mesmo diretório do código-fonte, eliminando a necessidade de especificá-lo na linha de comando. Se o arquivo .ifc estiver em um diretório diferente do código-fonte ou tiver outro nome, use a opção do compilador /reference para referenciá-lo.

    Ao importar std.compat, vincule std.compat e std.obj, porque std.compat usa o código em std.obj.

    Se você estiver compilando um só projeto, combine as etapas de compilação dos módulos nomeados da biblioteca padrão std e std.compat adicionando "%VCToolsInstallDir%\modules\std.ixx" e "%VCToolsInstallDir%\modules\std.compat.ixx" (nessa ordem) à linha de comando. Este tutorial mostra a compilação dos módulos da biblioteca padrão como uma etapa separada, pois você só precisa compilar os módulos nomeados da biblioteca padrão uma vez e, depois, referenciá-los no projeto ou em vários projetos. Porém, se for conveniente compilá-los todos de uma vez, lembre-se de colocá-los antes de arquivos .cpp que os consumam e especifique /Fe para nomear o exe compilado, conforme mostrado neste exemplo:

    cl /c /FestdCompatExample /std:c++latest /EHsc /nologo /W4 "%VCToolsInstallDir%\modules\std.ixx" "%VCToolsInstallDir%\modules\std.compat.ixx" stdCompatExample.cpp
    link stdCompatExample.obj std.obj std.compat.obj
    

    Neste exemplo, compilar o código-fonte e vincular a implementação do módulo ao aplicativo são etapas separadas. No entanto, elas não precisam ser feitas assim. Você pode usar cl /std:c++latest /EHsc /nologo /W4 stdCompatExample.cpp std.obj std.compat.obj para a compilação e a vinculação em uma só etapa. Porém, pode ser conveniente fazer a compilação e a vinculação separadamente, porque você só precisará compilar os módulos nomeados da biblioteca padrão uma vez e, depois, referenciá-los no projeto ou em vários projetos, na etapa de vinculação do build.

    O comando do compilador anterior produz um executável chamado stdCompatExample.exe. Ao executá-lo, ele produz a seguinte saída:

    Import std.compat to get global names like printf()
    555
    

Considerações sobre o módulo nomeado da biblioteca padrão

O controle de versão para os módulos nomeados é o mesmo dos cabeçalhos. Os arquivos de módulo nomeado .ixx são instalados ao lado dos cabeçalhos, por exemplo: "%VCToolsInstallDir%\modules\std.ixx, que é resolvido para C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.38.33130\modules\std.ixx na versão das ferramentas usadas no momento da elaboração deste artigo. Selecione a versão do módulo nomeado da mesma forma que você escolhe a versão do arquivo de cabeçalho a ser usado: pelo diretório no qual você o referencia.

Não misture e combine unidades de cabeçalho de importação com módulos nomeados. Por exemplo, não use import <vector>; e import std; no mesmo arquivo.

Não misture e combine a importação de arquivos de cabeçalho da biblioteca padrão C++ com os módulos nomeados std ou std.compat. Por exemplo, não use #include <vector> e import std; no mesmo arquivo. No entanto, você pode incluir cabeçalhos C e importar módulos nomeados no mesmo arquivo. Por exemplo, você pode use import std; e #include <math.h> no mesmo arquivo. Só não inclua a versão <cmath> da biblioteca padrão C++.

Você não precisa se proteger contra a importação de um módulo várias vezes. Ou seja, você não precisa usar #ifndef em proteções de cabeçalho nos módulos. O compilador sabe quando ele já importou um módulo nomeado e ignora tentativas duplicadas de realizar a mesma ação.

Caso você precise usar a macro assert(), use #include <assert.h>.

Caso você precise usar a macro errno, use #include <errno.h>. Como os módulos nomeados não expõem macros, essa é a solução alternativa se você precisa verificar se há erros em <math.h>, por exemplo.

Macros como NAN, INFINITY e INT_MIN são definidas por <limits.h>, que você poderá incluir. No entanto, se você usar import std;, poderá usar numeric_limits<double>::quiet_NaN() e numeric_limits<double>::infinity() em vez de NAN e INFINITY, e std::numeric_limits<int>::min() em vez de INT_MIN.

Resumo

Neste tutorial, você importou a biblioteca padrão usando módulos. A seguir, saiba mais sobre como criar e importar seus módulos no tutorial Módulos nomeados em C++.

Confira também

Comparar unidades de cabeçalho, módulos e cabeçalhos pré-compilados
Visão geral dos módulos no C++
Um tour por módulos C++ no Visual Studio
Mover um projeto para módulos nomeados C++