チュートリアル: コマンド ラインからモジュールを使用して C++ 標準ライブラリ (STL) をインポートする

C++ ライブラリ モジュールを使用して C++ 標準ライブラリをインポートする方法について説明します。 これにより、コンパイルが高速になり、ヘッダー ファイルやヘッダー ユニット、プリコンパイル済みヘッダー (PCH) を使用するよりも堅牢になります。

このチュートリアルで学習する内容は次のとおりです:

  • コマンド ラインから標準ライブラリをモジュールとしてインポートする方法。
  • モジュールのパフォーマンスと使いやすさの利点。
  • 2 つの標準ライブラリ モジュール stdstd.compat とそれらの違い。

前提条件

このチュートリアルでは、Visual Studio 2022 17.5 以降が必要です。

標準ライブラリ モジュールの概要

ヘッダー ファイルは、マクロ定義、それらを含める順序、およびコンパイルの速度が遅い場合に応じて変更できるセマンティクスに影響を受けます。 モジュールはこれらの問題を解決します。

ヘッダー ファイルのもつれとしてではなく、標準ライブラリをモジュールとしてインポートできるようになりました。 これは、ヘッダー ファイルやヘッダー ユニット、プリコンパイル済みヘッダー (PCH) を含むよりもはるかに高速で堅牢です。

C++23 標準ライブラリには、次の 2 つの名前付きモジュールが導入されています: stdstd.compat:

  • std は、C++ 標準ライブラリ名前空間 std で定義されている宣言と名前をエクスポートし、例えば std::vector です。 また、std::printf() などの関数を提供する <cstdio><cstdlib> などの C ラッパー ヘッダーの内容もエクスポートします。 グローバル名前空間で定義されている C 関数 (例: ::printf()) はエクスポートされません。 これにより、 <cstdio>のような C ラッパー ヘッダーを含めるとstdio.h などの C ヘッダー ファイルも含め、C グローバル名前空間のバージョンが取り込まれる状況が改善されます。 これはstd をインポートしても問題ありません。
  • std.compat は、std にあるすべてをエクスポートし、C ランタイムグローバル名前空間 (例: ::printf::fopen::size_t::strlen など) を追加します。 この std.compat モジュールを使用すると、グローバル名前空間の多くの C ランタイム関数/型を参照するコードベースを簡単に操作できます。

コンパイラは、import std; または import std.compat; を使用すると標準ライブラリ全体をインポートし、1 つのヘッダー ファイルを取り込むよりも高速になります。 たとえば、#include <vector> するよりも、import std; (または import std.compat) を使用して標準ライブラリ全体を取り込む方が高速です。

名前付きモジュールではマクロが公開されないため、asserterrnooffsetofva_arg などのマクロは、std または std.compat をインポートするときに使用できません。 回避策については「標準ライブラリの名前付きモジュールに関する考慮事項」を参照してください。

C++ モジュールについて

ヘッダー ファイルは、C++ のソース ファイル間で宣言と定義がどのように共有されているかを示します。 標準ライブラリ モジュールの前に、必要な標準ライブラリの一部を #include <vector> ようなディレクティブと共に含めます。 ヘッダー ファイルを含める順序や、特定のマクロが定義されているかどうかによってセマンティクスが変わる可能性があるため、ヘッダー ファイルは脆弱であり、構成が困難です。 また、それらを含むすべてのソース ファイルによって再処理されるため、コンパイルが遅くなります。

C++20 では、 モジュールと呼ばれる最新の代替手段が導入されています。 C++23 では、モジュールのサポートを活用して、標準ライブラリを表す名前付きモジュールを導入することができました。

ヘッダー ファイルと同様に、モジュールを使うとソース ファイル間で宣言と定義を共有できます。 ただし、ヘッダー ファイルとは異なり、モジュールは脆弱ではなく、マクロ定義やインポート順序によってセマンティクスが変更されないため、作成が容易になります。 コンパイラは、#include ファイルを処理するよりもはるかに高速にモジュールを処理でき、コンパイル時にも使用されるメモリが少なくなります。 名前付きモジュールでは、マクロ定義やプライベート実装の詳細は公開されません。

モジュールの詳細については、「C++ のモジュールの概要」を参照してください。その記事では、C++ 標準ライブラリをモジュールとして使用する方法についても説明しますが、以前の実験的な方法を使用しています。

この記事では、標準ライブラリを使用するための新しい最適な方法について説明しています。 標準ライブラリを使用する別の方法の詳細については、「ヘッダー ユニット、モジュール、プリコンパイル済みヘッダーを比較する」を参照してください。

std を使用して標準ライブラリをインポートする

次の例では、コマンド ライン コンパイラを使用して標準ライブラリをモジュールとして使用する方法を示します。 Visual Studio IDE でこれを行う方法については、「ビルド ISO C++23 標準ライブラリ モジュール」を参照してください。

ステートメント import std; または import std.compat; は標準ライブラリをアプリケーションにインポートします。 ただし、最初に、モジュールという名前の標準ライブラリをバイナリ形式にコンパイルする必要があります。 次の手順では、その方法を示します。

例: std をビルドしてインポートする方法

  1. Windows の [スタート] メニューから x86 Native Tools コマンド プロンプトを開き、「x86 native」と入力すると、アプリの一覧にプロンプトが表示されます。 プロンプトが Visual Studio 2022 バージョン 17.5 以降であることを確認します。 間違ったバージョンのプロンプトを使用すると、エラーが発生します。 このチュートリアルで使用する例は、CMD シェル用です。

  2. %USERPROFILE%\source\repos\STLModules などのディレクトリを作成し、現在のディレクトリにします。 書き込みアクセス権がないディレクトリを選択すると、コンパイル中にエラーが発生します。

  3. 次のコマンドを使用して、std 名前付きモジュールをコンパイルします:

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

    エラーが発生した場合は、正しいバージョンのコマンド プロンプトを使用していることを確認してください。

    ビルドされたモジュールをインポートするコードで使用するのと同じコンパイラ設定を使用して、std 名前付きモジュールをコンパイルします。 マルチプロジェクト ソリューションがある場合は、モジュールという名前の標準ライブラリを 1 回コンパイルし、/reference コンパイラ オプションを使用してすべてのプロジェクトから参照できます。

    前のコンパイラ コマンドを使用して、コンパイラは次の 2 つのファイルを出力します:

    • std.ifc は、import std; ステートメントを処理するためにコンパイラが参照する名前付きモジュール インターフェイスのコンパイル済みバイナリ表現です。 これはコンパイル時のみの成果物です。 アプリケーションに付属していません。
    • std.obj には、名前付きモジュールの実装が含まれています。 標準ライブラリから使用する機能をアプリケーションに静的にリンクするようにサンプル アプリをコンパイルするときに、 std.obj をコマンド ラインに追加します。

    この例の主要なコマンド ライン スイッチは次のとおりです:

    Switch 説明
    /std:c++:latest 最新バージョンの C++ 言語標準およびライブラリを使用します。 モジュールのサポートは /std:c++20 で利用できますが、モジュールという名前の標準ライブラリのサポートを受けるためには、最新の標準ライブラリが必要です。
    /EHsc extern "C" としてマークされた関数を除き、C++ 例外処理を使用します。
    /W4 /W4 を使用することをお勧めします。特に新しいプロジェクトでは、すべてのレベル 1、レベル 2、レベル 3、およびほとんどのレベル 4 (情報) の警告が有効になるため、潜在的な問題を早期にキャッチするのに役立ちます。 基本的には、見つけにくいコードの欠陥を最も少なくするのに役立つ lint のような警告が提供されます。
    /c この時点でバイナリの名前付きモジュール インターフェイスを構築しているだけなので、リンクせずにコンパイルします。

    次のスイッチを使用して、オブジェクト ファイル名と名前付きモジュール インターフェイス ファイル名を制御できます:

    • /Fo はオブジェクト ファイルの名前を設定します。 たとえば、/Fo:"somethingelse" のようにします。 既定では、コンパイラは、コンパイルするモジュール ソース ファイル (.ixx) と同じ名前をオブジェクト ファイルに使用します。 この例では、モジュール ファイル std.ixx をコンパイルしているため、オブジェクト ファイル名は既定で std.obj になります。
    • /ifcOutput は、名前付きモジュール インターフェイス ファイル (.ifc) の名前を設定します。 たとえば、/ifcOutput "somethingelse.ifc" のようにします。 既定では、コンパイラは、コンパイルするモジュール ソース ファイル (.ixx) と同じ名前をモジュール インターフェイス ファイル (.ifc) に使用します。 この例では、モジュール ファイル std.ixx をコンパイルしているため生成された ifc ファイルは既定で std.ifc になります。
  4. 作成した std ライブラリをインポートするには、最初に次の内容を含む importExample.cpp という名前のファイルを最初に作成します:

    // 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;
        }
    }
    

    前のコードでは、import std;#include <vector>#include <iostream> を置き換えます。 このステートメント import std; は、すべての標準ライブラリを 1 つのステートメントで使用できるようにします。 標準ライブラリ全体のインポートは、多くの場合、#include <vector> などの単一の標準ライブラリ ヘッダー ファイルを処理するよりもはるかに高速です。

  5. 前の手順と同じディレクトリで次のコマンドを使用して、例をコンパイルします:

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

    コンパイラは、import ステートメントで指定されたモジュール名と一致する .ifc ファイルを自動的に検索するため、この例のコマンド ラインで /reference "std=std.ifc" を指定する必要はありません。 コンパイラが import std; を検出すると、ソース コードと同じディレクトリにある std.ifc を見つけることができます。 .ifc ファイルがソース コードとは異なるディレクトリにある場合は、/reference コンパイラ スイッチを使用して参照します。

    この例では、ソース コードをコンパイルし、モジュールの実装をアプリケーションにリンクする手順は別です。 そうである必要はありません。 cl /std:c++latest /EHsc /nologo /W4 /reference "std=std.ifc" importExample.cpp std.obj を使用して、1 つの手順でコンパイルとリンクできます。 ただし、モジュールという名前の標準ライブラリを 1 回だけビルドし、ビルドのリンクステップでプロジェクトまたは複数のプロジェクトから参照できるため、個別にビルドしてリンクすると便利な場合があります。

    1 つのプロジェクトをビルドする場合は、std 標準ライブラリのモジュールをビルドする手順と、コマンド ラインに "%VCToolsInstallDir%\modules\std.ixx" を追加してアプリケーションをビルドする手順を組み合わせることができます。 std モジュールを使用する .cpp ファイルの前に配置します。

    既定では、出力実行可能ファイルの名前は最初の入力ファイルから取得されます。 /Fe コンパイラ オプションを使用して、必要な実行可能ファイル名を指定します。 このチュートリアルでは、std 名前付きモジュールを個別の手順としてコンパイルする方法について説明します。これは、モジュールという名前の標準ライブラリを 1 回ビルドするだけで済み、プロジェクトまたは複数のプロジェクトから参照できるためです。 ただし、次のコマンド ラインに示すように、すべてを一緒に構築すると便利な場合があります:

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

    前のコマンド ラインを指定すると、コンパイラは importExample.exe という名前の実行可能ファイルを生成します。 実行すると、生成される出力は次のとおりです:

    Import the STL library for best performance
    555
    

std.compat を使用して標準ライブラリとグローバル C 関数をインポートする

C++ 標準ライブラリには、ISO C 標準ライブラリが含まれています。 std.compat モジュールは、std::vectorstd::coutstd::printfstd::scanf など、std モジュールのすべての機能を提供します。 ただし、これらの関数のグローバル名前空間バージョンも提供します (例: ::printf::scanf::fopen::size_t など)。

std.compat 名前付きモジュールは、グローバル名前空間の C ランタイム関数を参照する既存のコードの移行を容易にするために提供される互換性レイヤーです。 グローバル名前空間に名前を追加しないようにするには、import std;を使用します。 多くの非修飾 (グローバル名前空間) C ランタイム関数を使用するコードベースの移行を容易にする必要がある場合は、import std.compat; を使用します。 これにより、グローバル名前空間 C ランタイム名が提供されるため、すべてのグローバル名を std:: で修飾する必要はありません。 グローバル名前空間 C ランタイム関数を使用する既存のコードがない場合は、import std.compat; を使用する必要はありません。 コード内でいくつかの C ランタイム関数のみを呼び出す場合は、import std; を使用し、std:: で必要ないくつかのグローバル名前空間 C ランタイム名を修飾することをお勧めします。 たとえば、std::printf() のようにします。 コードをコンパイルしようとしたときに error C3861: 'printf': identifier not found のようなエラーが表示される場合は、import std.compat; を使用してグローバル名前空間 C ランタイム関数をインポートすることを検討してください。

例: std.compat をビルドしてインポートする方法

import std.compat; を使用する前に、ソース コード形式のモジュール インターフェイス ファイルを std.compat.ixx でコンパイルする必要があります。 Visual Studio には、プロジェクトに一致するコンパイラ設定を使用してモジュールをコンパイルできるように、モジュールのソース コードが付属しています。 この手順は、std 名前付きモジュールをビルドする場合と似ています。 std.compat が依存しているため、名前付きモジュール std が最初にビルドされます:

  1. Windows の [スタート] メニューから Native Tools コマンド プロンプトを開き、「x86 native」と入力すると、アプリの一覧にプロンプトが表示されます。 プロンプトが Visual Studio 2022 バージョン 17.5 以降であることを確認します。 間違ったバージョンのプロンプトを使用すると、コンパイラ エラーが発生します。

  2. %USERPROFILE%\source\repos\STLModules など、この例を試すディレクトリを作成し、現在のディレクトリにします。 書き込みアクセス権がないディレクトリを選択すると、エラーが発生します。

  3. 次のコマンドを使用して、stdstd.compat 名前付きモジュールをコンパイルします。

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

    stdstd.compat は、それらをインポートするコードで使用するのと同じコンパイラ設定を使用してコンパイルする必要があります。 マルチプロジェクト ソリューションがある場合は、それらを 1 回コンパイルし、/reference コンパイラ オプションを使用してすべてのプロジェクトから参照できます。

    エラーが発生した場合は、正しいバージョンのコマンド プロンプトを使用していることを確認してください。

    コンパイラは、前の 2 つの手順から 4 つのファイルを出力します:

    • std.ifc は、import std; ステートメントを処理するためにコンパイラが参照する名前付きモジュール インターフェイスのコンパイル済みバイナリです。 std.compatstd に基づいて構築されているため、コンパイラは import std.compat; を処理する std.ifc も参照します。 これはコンパイル時のみの成果物です。 アプリケーションに付属していません。
    • std.obj には標準ライブラリの実装が含まれています。
    • std.compat.ifc は、import std.compat; ステートメントを処理するためにコンパイラが参照する名前付きモジュール インターフェイスのコンパイル済みバイナリです。 これはコンパイル時のみの成果物です。 アプリケーションに付属していません。
    • std.compat.obj には実装が含まれています。 ただし、ほとんどの実装は std.obj によって提供されます。 標準ライブラリから使用する機能をアプリケーションに静的にリンクするようにサンプル アプリをコンパイルするときに、 std.obj をコマンド ラインに追加します。

    次のスイッチを使用して、オブジェクト ファイル名と名前付きモジュール インターフェイス ファイル名を制御できます:

    • /Fo はオブジェクト ファイルの名前を設定します。 たとえば、/Fo:"somethingelse" のようにします。 既定では、コンパイラは、コンパイルするモジュール ソース ファイル (.ixx) と同じ名前をオブジェクト ファイルに使用します。 この例では、std.ixx および std.compat.obj モジュール ファイルをコンパイルするため、オブジェクト ファイル名は既定では std.objstd.compat.obj になります。
    • /ifcOutput は、名前付きモジュール インターフェイス ファイル (.ifc) の名前を設定します。 たとえば、/ifcOutput "somethingelse.ifc" のようにします。 既定では、コンパイラは、コンパイルするモジュール ソース ファイル (.ixx) と同じ名前をモジュール インターフェイス ファイル (.ifc) に使用します。 この例では、std.ixx および std.compat.ixx モジュール ファイルをコンパイルするため、生成された ifc ファイルは既定では std.ifcstd.compat.ifc になります。
  4. std.compat ライブラリをインポートするには、最初に次の内容を含む stdCompatExample.cpp という名前のファイルを最初に作成します:

    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);
        }
    }
    

    前のコードでは、import std.compat;#include <cstdio>#include <vector> を置き換えます。 import std.compat; ステートメントを使用すると、標準ライブラリと C ランタイム関数を 1 つのステートメントで使用できます。 C++ 標準ライブラリと C ランタイム ライブラリグローバル名前空間関数を含むこの名前付きモジュールのインポートは、#include <vector> などの単一の #include を処理するよりも高速です。

  5. 次のコマンドを使用して、例をコンパイルします:

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

    import ステートメントでモジュール名と一致する .ifc ファイルがコンパイラによって自動的に検索されるため、コマンド ラインで std.compat.ifc を指定する必要はありませんでした。 コンパイラが import std.compat; 見つかると、ソース コードと同じディレクトリに配置されるため、std.compat.ifc が見つかります。コマンド ラインで指定する必要がありません。 .ifc ファイルがソース コードとは異なるディレクトリにある場合、または名前が異なる場合は、/reference コンパイラ スイッチを使用して参照します。

    std.compat をインポートするときは、std.compatstd.obj の両方に対してリンクする必要があります。std.compatstd.obj でコードを使用するためです。

    1 つのプロジェクトをビルドする場合は、コマンド ラインに "%VCToolsInstallDir%\modules\std.ixx""%VCToolsInstallDir%\modules\std.compat.ixx" を (その順序で) 追加することで、モジュールという名前の stdstd.compat 標準ライブラリをビルドする手順を組み合わせることができます。 このチュートリアルでは、モジュールという名前の標準ライブラリを 1 回だけビルドするだけで済み、プロジェクトまたは複数のプロジェクトから参照できるため、標準ライブラリ モジュールを別の手順としてビルドする方法について説明します。 ただし、すべてを一度にビルドするのが便利な場合は、それらを使用する .cpp ファイルの前に配置し、次の例に示すようにビルド exe に名前を付ける /Fe を指定してください:

    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
    

    この例では、ソース コードをコンパイルし、モジュールの実装をアプリケーションにリンクする手順は別です。 そうである必要はありません。 cl /std:c++latest /EHsc /nologo /W4 stdCompatExample.cpp std.obj std.compat.obj を使用して、1 つの手順でコンパイルとリンクできます。 ただし、モジュールという名前の標準ライブラリを 1 回だけビルドし、ビルドのリンクステップでプロジェクトまたは複数のプロジェクトから参照できるため、個別にビルドしてリンクすると便利な場合があります。

    前のコンパイラ コマンドは、stdCompatExample.exe という名前の実行可能ファイルを生成します。 実行すると、生成される出力は次のとおりです:

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

標準ライブラリの名前付きモジュールに関する考慮事項

名前付きモジュールのバージョン管理は、ヘッダーの場合と同じです。 .ixx の名前付きモジュール ファイルはヘッダーと共にインストールされます。たとえば、"%VCToolsInstallDir%\modules\std.ixx。この記述時に使用したツールのバージョンで C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.38.33130\modules\std.ixx に解決されます。 使用するヘッダー ファイルのバージョンを選択するのと同じ方法で、名前付きモジュールのバージョンを選択します。参照するディレクトリを使用します。

インポートヘッダーユニットと名前付きモジュールを混在させないでください。 たとえば、同じファイルに import <vector>;import std; を同時にしないでください。

C++ 標準ライブラリ のヘッダー ファイルと、名前付きモジュールの std または std.compatのインポートを混在させないでください。 たとえば、同じファイルに #include <vector>import std; を同時にしないでください。 ただし、C ヘッダーを含め、名前付きモジュールを同じファイルにインポートできます。 たとえば、同じファイルに import std;#include <math.h> をできます。 C++ 標準ライブラリのバージョン <cmath> を含めないでください。

モジュールを複数回インポートしないように保護する必要はありません。 つまり、モジュール内 #ifndef スタイル ヘッダー ガードは必要ありません。 コンパイラは、名前付きモジュールがいつ既にインポートされたかを認識し、重複する試行を無視します。

assert() マクロを使用する必要がある場合は、#include <assert.h> します。

errno マクロを使用する必要がある場合は、#include <errno.h>。 名前付きモジュールではマクロが公開されないため、たとえば、<math.h> からのエラーを確認する必要がある場合の回避策です。

NANINFINITYINT_MIN などのマクロは、含めることができる <limits.h> によって定義されます。 ただし、import std; の場合は、NANINFINITY の代わりに numeric_limits<double>::quiet_NaN()numeric_limits<double>::infinity()を使用し、INT_MIN の代わりに std::numeric_limits<int>::min() をすることができます。

まとめ

このチュートリアルでは、モジュールを使用して標準ライブラリをインポートしました。 次に、C++ の Named modules チュートリアルでは独自のモジュールを作成およびインポートする方法について説明します。

関連項目

ヘッダー ユニット、モジュール、プリコンパイル済みヘッダーを比較する
C++ のモジュールの概要
Visual Studio での C++ モジュールのツアー
C++ 名前付きモジュールにプロジェクトを移行する