26 5月 2025

C++のモジュール

C++ のエコシステムは、数十年間で最も抜本的な変革を遂げ、コードの編成とコンパイル方法を根本的に再構築しています。長年、開発者は#includeシステムの制約、すなわちその鈍いコンパイル時間、遍在するマクロ汚染、そして適切なカプセル化の欠如に苦しめられてきました。プリプロセッサベースのアプローチに内在するこれらの欠陥は、大規模開発におけるC++の可能性を制約し、エンジニアに複雑な回避策やビルドシステムの採用を強いてきました。

C++ 20モジュールは、長年にわたるこれらの課題に対する包括的な解決策として登場し、言語の誕生以来、C++ のコード編成における最初の主要なパラダイムシフトを象徴しています。テキストによるインクルードを構造化されたバイナリインターフェースに置き換えることで、モジュールはコンパイル速度、コードの分離性、インターフェースの明確さにおいて革新的な改善をもたらします。これは単なる漸進的な改善ではなく、C++プログラム構築のアーキテクチャそのものに対処する根幹的な変更です。

C++モジュールが不可欠な理由

従来のヘッダーファイルモデルは、現代のC++開発に4つの根本的な制約を課していました。

  1. コンパイルのボトルネック: ヘッダーファイルがインクルードされると、その内容全体、そしてすべての依存関係を含め、それをインクルードする各翻訳単位で再解析および再コンパイルされていました。この冗長な処理、特に標準ライブラリのような広く使用されるヘッダーの場合、大規模プロジェクトのコンパイル時間を劇的に増加させました。コンパイラは同じ宣言と定義を何度も処理する必要があり、かなりのオーバーヘッドにつながっていました。
  2. マクロ汚染: ヘッダーファイル内で定義されたマクロは、それらをインクルードする任意の翻訳単位のグローバル名前空間に「漏れ出し」ました。これにより、名前衝突、予期しない動作、そして診断が困難な微妙なバグが頻繁に発生しました。マクロは言語キーワードを再定義したり、他の正当な識別子と干渉したりする可能性があり、堅牢なコーディングにとって敵対的な環境を作り出していました。
  3. 弱いカプセル化: ヘッダーファイルは、宣言、定義、さらには内部ヘルパー関数やデータなど、そのすべての内容を実質的に公開していました。コンポーネントの「公開インターフェース」と内部実装の詳細を明確に定義する直接的なメカニズムがありませんでした。この堅牢なカプセル化の欠如は、コードの可読性、保守性、安全なリファクタリングを妨げました。
  4. 単一定義規則(ODR)違反: ODRは異なる翻訳単位間で同じエンティティの複数の定義を禁止していますが、ヘッダーベースのシステムでは、特にテンプレートやインライン関数において、この規則を順守することが困難でした。偶発的な違反は、リンカーエラーや未定義動作につながる可能性がありました。

C++モジュールは、これらの課題に直接対処します。それらは一度バイナリ表現にコンパイルされ、コンパイラはテキストベースのヘッダーファイルよりも大幅に高速にインポートおよび処理できます。このコードの変換により、コンパイル中の冗長な作業が大幅に削減されます。モジュールはまた、可視性を厳密に制御し、明示的にマークされたもののみをエクスポートするため、マクロ汚染を防ぎ、より強力なカプセル化を強制します。明示的にエクスポートされていない名前やマクロは、モジュールに対してプライベートなままであり、コードの分離性を向上させます。

C++モジュールの例を探索する

基本的な構文を理解することは、C++モジュールを使用した効果的なプログラミングの基礎を形成します。モジュール定義には通常、モジュールが何をエクスポートするかを宣言するインターフェースユニットと、定義を提供する実装ユニットが含まれます。

数学モジュールの簡単なC++モジュールの例を考えてみましょう。

1. モジュールインターフェースユニット (math.ixx)

// math.ixx - 'math'のプライマリモジュールインターフェースユニット
export module math;

// エクスポートされた関数宣言
export int add(int a, int b);
export int multiply(int a, int b);

// 内部ヘルパー関数、エクスポートされない
int subtract_internal(int a, int b);

ここでは、export module math;が、このファイルをmathという名前のモジュールのプライマリインターフェースとして宣言しています。addmultiplyの前のexportキーワードは、これらの関数がモジュールの公開インターフェースの一部であり、mathをインポートする他の翻訳単位からアクセス可能であることを示します。exportのないsubtract_internal関数は、モジュールに対してプライベートなままです。

2. モジュール実装ユニット (math.cpp)

// math.cpp - 'math'のモジュール実装ユニット
module math; // このファイルを'math'モジュールに関連付ける

// エクスポートされた関数の定義
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

// 内部ヘルパー関数の定義
int subtract_internal(int a, int b) {
    return a - b;
}

このファイルはmodule math;で始まり、mathモジュールにリンクします。math.ixxで宣言された関数の定義が含まれています。実装ファイルではモジュール宣言でexportを使用しないことに注意してください。

3. モジュールの利用 (main.cpp)

// main.cpp - 'math'モジュールの利用
import math; // 'math'モジュールをインポートする
#include <iostream> // コンソール出力用

int main() {
    int sum = add(10, 5); // エクスポートされた関数を呼び出す
    int product = multiply(10, 5); // エクスポートされた関数を呼び出す

    std::cout << "Sum: " << sum << std::endl;
    std::cout << "Product: " << product << std::endl;

    // int difference = subtract_internal(10, 5); // エラー: subtract_internalはエクスポートされていません
    return 0;
}

main.cppでは、import math;によってmathモジュールからエクスポートされた宣言が利用可能になります。addmultiplyは直接使用できますが、subtract_internalにはアクセスできないままであり、モジュールの強力なカプセル化を示しています。

このC++モジュールの例は、基本的なパターンを示しています。モジュールインターフェースユニットはexport moduleを使用してモジュールの名前とその公開インターフェースを宣言し、モジュール実装ユニットはmoduleを使用してモジュールの内部実装に貢献します。

C++ 20およびC++ 23モジュール:高度な機能と将来

基本的なexportimportを超えて、C++ 20モジュールは複雑なプロジェクト構造とレガシーコード変換を処理するためのいくつかの高度な機能を導入しています。C++ 23モジュールは、さらなる改良とコンパイラサポートの改善により、この進化を続けています。

モジュールユニットと構造

名前付きモジュールは、同じモジュール名を共有するモジュールユニット(ソースファイル)の集合です。

  • プライマリモジュールインターフェースユニット: 各名前付きモジュールには、厳密に1つのプライマリインターフェースユニット(export module MyModule;)が必要です。このユニットのエクスポートされたコンテンツは、import MyModuleしたときに可視になります。
  • モジュールインターフェースユニット(一般): export moduleで宣言された任意のユニットはインターフェースユニットです。これにはプライマリインターフェースユニットとパーティションインターフェースユニットが含まれます。
  • モジュール実装ユニット: module MyModule;exportなし)で宣言されたユニットは実装ユニットです。モジュールの内部ロジックに貢献しますが、インポーターに直接インターフェースを公開しません。

グローバルモジュールフラグメント

モジュールへのコード移植中に、構成のためにプリプロセッサマクロに依存する既存のヘッダーとの統合が重要な課題となります。グローバルモジュールフラグメントはこれに対処します。

// mymodule_with_legacy.ixx
module; // グローバルモジュールフラグメントの開始
#define USE_ADVANCED_FEATURE 1 // グローバルフラグメントで定義されたマクロ
#include <legacy_header.h>    // レガシーヘッダーのインクルード(仮想)

export module mymodule_with_legacy; // グローバルモジュールフラグメントの終了、モジュールの開始
import <iostream>; // 標準ライブラリモジュールのインポート

export void perform_action() {
#if USE_ADVANCED_FEATURE
    std::cout << "Advanced action performed!" << std::endl;
#else
    std::cout << "Basic action performed!" << std::endl;
#endif
    // 必要に応じてlegacy_header.hからの宣言を使用
}

module;宣言はグローバルモジュールフラグメントを開始します。このフラグメント内の#includeディレクティブやマクロ定義は、モジュール自体が定義される前に処理されます。これにより、モジュールのインターフェース内でマクロ汚染なしに、モジュールがプリプロセッサに依存するレガシーコードと相互作用できるようになります。ここで定義されたマクロは、このフラグメントに含まれるヘッダーの処理には影響を与えますが、モジュールで後からヘッダーユニットとしてインポートされるヘッダーには影響しません。

プライベートモジュールフラグメント

開発者が実装詳細をプライマリインターフェースユニット内に保持しつつ、インポーターから完全に隠したい大規模なモジュールの場合、C++ 20モジュールはプライベートモジュールフラグメントを提供します。

// main_module.ixx
export module main_module;

export int public_function();

module :private; // プライベートモジュールフラグメントの開始

// この関数はmain_module.ixx自体の中でのみ可視であり、
// 'main_module'をインポートするコードからは見えません。
int internal_helper_function() {
    return 36;
}

int public_function() {
    return internal_helper_function();
}

module :private;宣言は、モジュールの公開インターフェースとそのプライベートな実装詳細との境界を示します。この宣言以降のコードは、プライマリインターフェースユニット内において、そのユニットおよび同じ名前付きモジュールに属する他のユニットからのみアクセス可能であり、外部のインポーターからはアクセスできません。これにより、単一の.ixxファイルでモジュールを表現しながら、公開セクションとプライベートセクションを明確に分離できるため、カプセル化が強化されます。プライベートモジュールフラグメントを含むモジュールユニットは、通常、そのモジュールの唯一のユニットとなります。

モジュールパーティション

非常に大規模なモジュールの場合、それらをより小さく、管理しやすい単位に分解することは有益です。モジュールパーティション(C++ 20モジュール)は、この内部モジュール性を可能にします。パーティションは基本的に、より大きな名前付きモジュールのサブモジュールです。

// large_module_part1.ixx - パーティションインターフェースユニット
export module large_module:part1; // 'large_module'の'part1'という名前のパーティションを宣言する

export void do_something_in_part1();
// large_module_part1.cpp - パーティション実装ユニット
module large_module:part1; // このファイルを'part1'パーティションにリンクする

void do_something_in_part1() {
    // 実装
}
// large_module_main.ixx - 'large_module'のプライマリモジュールインターフェースユニット
export module large_module;

// パーティションインターフェースをエクスポートインポートする。
// これにより、`large_module`がインポートされたときに`do_something_in_part1`が可視になる。
export import :part1;

// 実装パーティション(存在する場合)はインポートできるが、エクスポートはできない。
// import :internal_part;

export void do_main_thing() {
    do_something_in_part1(); // パーティション関数を直接呼び出すことができる
}

パーティションは、名前付きモジュール内でのみ可視です。外部の翻訳単位はlarge_module:part1;を直接インポートすることはできません。代わりに、プライマリモジュールインターフェースユニット(large_module_main.ixx)がexport import :part1;を行うことで、part1のエクスポートされたエンティティがlarge_moduleのインポーターに可視になります。この階層構造は、大規模なソフトウェア開発プロジェクトにおいて複雑さを管理するための堅牢な方法を提供します。

ヘッダーユニット

C++ 20モジュールは、ヘッダーユニットも導入しており、import <header_name>;またはimport "header_name";を使用して従来のヘッダーファイルをインポートできます。ヘッダーがヘッダーユニットとしてインポートされると、一度解析され、その宣言がモジュールと同様に効率的に利用可能になります。決定的に重要なのは、インポートする翻訳単位でimportステートメントの前に定義されたプリプロセッサマクロが、#includeとは異なり、ヘッダーユニット自体の処理に影響を与えないことです。これにより、より一貫性のある動作が提供され、大規模なヘッダーのコンパイルを大幅に高速化できます。

// my_app.cpp
#define CUSTOM_SETTING 10 // このマクロは<vector>や"my_utility.h"には影響しません

import <vector>;         // 標準のvectorヘッダーをヘッダーユニットとしてインポートする
import "my_utility.h"; // カスタムヘッダーをヘッダーユニットとしてインポートする(仮想)

int main() {
    std::vector<int> numbers = {1, 2, 3};
    // ...
    return 0;
}

この区別はコード移植の取り組みにとって重要です。ヘッダーが構成のためにプリプロセッサディレクティブに依存しており、それがインポートする翻訳単位のプリプロセッサ状態に影響される必要がある場合、グローバルモジュールフラグメントが適切な解決策です。それ以外の場合、ヘッダーユニットとしてインポートすることは、#includeよりも高速でクリーンな代替手段です。

既存のコードベースへのC++モジュールの統合

大規模な既存のC++ プロジェクトを完全なモジュールシステムにコード変換およびコード移植することは、段階的なプロセスとなり得ます。開発者はコードベース全体を一度に変換する必要はありません。C++モジュールは、従来のヘッダーファイルとシームレスに共存するように設計されています。

  • 並行使用: C++ソースファイルは、モジュールをimportし、ヘッダーファイルを#includeすることができます。この柔軟性により、一度に1つのコンポーネントまたはライブラリを変換するなど、段階的な導入が可能になります。
  • ヘッダーからモジュールへの変換: 深刻なコンパイルのボトルネックを引き起こすコアライブラリや頻繁にインクルードされるヘッダーを特定することから始めます。これらを名前付きモジュールに変換します。これにより、最も即座なコンパイル速度の恩恵が得られることがよくあります。
  • 戦略的なモジュールパーティション: 非常に大規模な内部ライブラリの場合、モジュールパーティションを使用してコンポーネントを整理することを検討します。これにより、モジュールインターフェースをきれいに保ちながら、内部的なモジュール性が提供されます。
  • マクロ依存性への対処: 広範なプリプロセッサマクロを使用するレガシーヘッダーについては、ヘッダーユニットとしてインポートできるかどうか、またはマクロ構成がグローバルモジュールフラグメントの使用を必要とするかどうかを評価します。この情報に基づいた意思決定は、成功するコード変換にとって不可欠です。

鍵は、実験と測定を行うことです。開発者は、コンパイル時間の意味のある削減とコード編成の改善を達成できるかどうかに基づいて、導入を決定すべきです。

コンパイラサポートとC++モジュールの将来

C++ 20モジュールのサポートは、主要なコンパイラ間で継続的に進化しています。

  • Microsoft Visual C++ (MSVC): MSVCはC++ 20モジュールに対して堅牢なサポートを提供しており、最近のバージョンでは重要な進歩を遂げています。Visual Studio 2022バージョン17.5以降、C++標準ライブラリをモジュールとしてインポートすることが標準化され、完全に実装されており、著しいパフォーマンス向上をもたらしています。
  • Clang: ClangもC++ 20モジュールに対して十分なサポートを提供しており、その実装とパフォーマンス特性を洗練するための継続的な開発が行われています。
  • GCC: GCCのC++ 20モジュールサポートは着実に進展しており、より幅広い開発者が利用可能になっています。

C++ 23モジュールがより普及するにつれて、さらなる改良とコンパイラの成熟度の向上が、全体的な開発者体験を向上させるでしょう。完全かつ最適化されたモジュールサポートへの道のりは進行中ですが、C++ 20で築かれた基盤は、C++ソフトウェア開発がどのように行われるかにおいて、抜本的な変化を表しています。この継続的な進化は、言語にとってさらに堅牢で効率的な将来を約束します。

結論

C++ モジュールは革新的な機能であり、C++ におけるプログラミングの慣行を根本的に再構築しています。これらは、ヘッダーベースのシステムに関連する長年の問題、例えば遅いコンパイル、マクロの干渉、弱いカプセル化に直接対処します。明示的なインターフェース定義と効率的なコンパイルのための堅牢なシステムを提供することにより、C++ モジュールはコード編成を強化し、ビルド時間を改善し、懸念事項のより明確な分離を促進します。C++ 20モジュールを採用し、C++ 23モジュールでの将来の強化に備えることは、開発者がよりスケーラブルで保守性が高く、効率的なソフトウェア開発プロジェクトを構築できるようにし、言語に新たな時代をもたらします。