C++のモジュール機能について

AI実装検定のご案内

C++のモジュール機能は、C++20で導入された新しいコード分割・依存管理の仕組みです。

従来のC++では、機能を分割する際にヘッダーファイルとソースファイルを使い、別のファイルから利用するときは #include によってヘッダーを読み込んでいました。

しかし、#include は実際にはファイルの中身をその場に貼り付けるテキスト展開であるため、大規模なプロジェクトではコンパイル時間の増加やマクロ汚染、依存関係の複雑化といった問題が起こりやすくなります。

モジュールは、こうした従来のヘッダー方式の弱点を改善するために導入された仕組みです。

公開したい機能だけを明示的に外部へ提供し、利用側はそれをインポートして使います。

簡単に言えば、C++モジュールは「ヘッダーを読み込む仕組み」ではなく、「公開されたインターフェースを利用する仕組み」です。

目次

従来のヘッダー方式の問題点

ヘッダーはテキストとして展開される

従来の #include は、指定したヘッダーファイルの内容をその場に貼り付ける仕組みです。

そのため、同じヘッダーが複数のソースファイルで読み込まれると、そのたびに同じ内容が解析されます。

小規模なプログラムでは大きな問題になりにくいですが、大規模なC++プロジェクトでは、これがコンパイル時間を長くする大きな原因になります。

特に、標準ライブラリやテンプレートを多く含むヘッダー、依存関係の深いヘッダーを何度も読み込む場合、ビルド全体の負荷はかなり大きくなります。

マクロ汚染が起こりやすい

ヘッダー内で定義されたマクロは、読み込んだ側のコードにも影響します。

マクロは名前空間の影響を受けないため、意図しない名前の衝突や、別のライブラリとの干渉が起こることがあります。

特に大規模開発では、どこで定義されたマクロがどのコードに影響しているのかを追跡しづらくなります。

モジュールでは、従来のヘッダー方式に比べてマクロの影響を抑えやすくなります。

依存関係が分かりにくい

ヘッダーは別のヘッダーを読み込み、そのヘッダーがさらに別のヘッダーを読み込む、という構造になりがちです。

その結果、実際にはどのファイルに依存しているのか、どの宣言がどこから来ているのかが分かりにくくなります。

また、あるヘッダーを直接読み込んでいないのに、別のヘッダー経由でたまたま使えてしまうこともあります。

このような間接的な依存は、コードの保守性を下げる原因になります。

インクルード順序の問題が起こる

従来のヘッダー方式では、ファイルを読み込む順番によってコンパイル結果が変わる場合があります。

あるヘッダーを先に読み込むとコンパイルできるが、順番を変えるとエラーになる、といった問題です。

これは、マクロや前方宣言、ヘッダー内の依存関係が複雑に絡むことで発生します。

モジュールでは、依存関係をより明示的に扱えるため、このような問題を減らしやすくなります。

C++モジュールの基本的な考え方

公開するものを明示する

C++モジュールでは、外部から使えるようにしたい関数、クラス、変数、テンプレートなどを明示的に公開します。

従来のヘッダー方式では、ヘッダーに書かれた宣言は基本的に読み込んだ側から見えます。

一方、モジュールでは、外部に見せたいものと内部だけで使いたいものを明確に分けられます。

この仕組みによって、公開APIと内部実装の境界を整理しやすくなります。

利用側はインポートする

モジュールを利用する側は、従来のようにヘッダーをインクルードするのではなく、モジュールをインポートします。

インポートは、ヘッダーの中身を単純に貼り付ける仕組みではありません。

モジュールとして提供されたインターフェースを利用可能にする仕組みです。

この違いにより、コンパイル時間の改善や、依存関係の整理が期待できます。

モジュールはヘッダーの単なる置き換えではない

モジュールは、見た目だけで考えると「ヘッダーの代わり」のように見えます。

しかし、実際には単なる置き換えではありません。

ヘッダーはテキストを読み込む仕組みですが、モジュールは言語機能としてコードの公開範囲や依存関係を扱います。

そのため、モジュールは「新しいファイル分割の書き方」というよりも、「C++における依存関係管理を改善する仕組み」と考えた方が正確です。

モジュールインターフェースとモジュール実装

モジュールインターフェースとは

モジュールインターフェースは、そのモジュールが外部に公開する内容を定義する部分です。

従来のヘッダーファイルに近い役割を持ちますが、完全に同じではありません。

ヘッダーは読み込まれた側に内容が展開されますが、モジュールインターフェースは、モジュールとしてコンパイルされ、利用側からインポートされます。

モジュールインターフェースには、外部に公開したい宣言や定義を記述します。

モジュール実装とは

モジュール実装は、モジュール内部の具体的な処理を書く部分です。

従来のソースファイルに近い役割を持ちます。

公開インターフェースで宣言した関数の実装や、モジュール内部でのみ使う補助関数などを記述します。

モジュール実装に書いたものは、外部に公開しない限り、利用側から直接参照されません。

インターフェースと実装を分けるメリット

インターフェースと実装を分けることで、利用側に見せる情報を最小限にできます。

公開APIだけをインターフェースに置き、詳細な実装は実装ファイル側に隠すことで、コードの見通しがよくなります。

また、内部実装を変更しても、公開インターフェースが変わらなければ、利用側への影響を抑えやすくなります。

exportの役割

外部に公開するための指定

C++モジュールでは、外部から使えるようにするものを明示的に指定します。

このときに使うのが export という考え方です。

公開された関数やクラスは、別の翻訳単位から利用できます。

一方、公開されていないものは、基本的にモジュール内部でのみ使われます。

これにより、意図しない内部実装が外部から利用されることを防ぎやすくなります。

公開APIを整理しやすい

従来のヘッダー方式では、ヘッダーに書いたものが外部に見えやすくなります。

そのため、本来は内部用の宣言であっても、ヘッダーに置かざるを得ない場合がありました。

モジュールでは、公開するものだけを明示できるため、API設計がしやすくなります。

特にライブラリ開発では、「利用者に使ってほしい機能」と「内部実装の都合で存在する機能」を分けることが重要です。モジュールはこの分離を言語機能として支援します。

名前空間とモジュール名の違い

モジュール名はインポートの単位

モジュール名は、そのモジュールを利用するときに指定する名前です。

たとえば、数学関連の機能をまとめたモジュールであれば、モジュール名として math のような名前を付けることがあります。

この名前は、利用側がそのモジュールを読み込むための識別子です。

名前空間は名前の衝突を避ける仕組み

名前空間は、C++に従来からある仕組みで、関数名やクラス名の衝突を避けるために使われます。

モジュール名と名前空間名は、同じ名前にすることもできますが、言語上は別物です。

たとえば、モジュール名を geometry にし、名前空間も geometry にすることはできます。

しかし、これはあくまで設計上そうしているだけで、モジュールと名前空間が同一の概念になるわけではありません。

実務では対応させると分かりやすい

実務では、モジュール名と名前空間名をある程度対応させると、コードの見通しがよくなります。

たとえば、ネットワーク関連の機能を扱うなら、モジュール名も名前空間もネットワーク関連で統一すると、利用者が理解しやすくなります。

ただし、必ず一致させる必要はありません。

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

既存ヘッダーとの橋渡しに使う

グローバルモジュールフラグメントは、モジュール宣言の前に置く特別な領域です。

主に、既存のヘッダーをモジュール内で使うために利用されます。

C++には長年にわたって作られてきたヘッダー前提のライブラリが多く存在します。

そのため、モジュールを導入しても、既存ヘッダーと完全に無関係になるわけではありません。

グローバルモジュールフラグメントは、そうした既存コードとモジュールを共存させるために重要です。

使いすぎには注意が必要

グローバルモジュールフラグメントは便利ですが、従来のヘッダー依存を大量に持ち込むと、モジュールの利点が薄れます。

特に、マクロを多用するヘッダーや、巨大な依存関係を持つヘッダーを多く読み込むと、モジュール化してもコンパイル時間や依存関係の改善効果が限定的になる場合があります。

したがって、既存ヘッダーを扱うための現実的な手段として使いつつ、依存関係を整理する意識が重要です。

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

1つのファイルに公開部分と実装をまとめられる

プライベートモジュールフラグメントは、モジュールインターフェースの中に、外部へ公開しない実装部分を含めるための仕組みです。

小さなモジュールであれば、公開宣言と実装を1つのファイルにまとめられるため、構成がシンプルになります。

大規模開発では分離した方がよい場合もある

一方で、大きなプロジェクトでは、インターフェースと実装を別ファイルに分けた方が管理しやすい場合があります。

ファイルが大きくなりすぎると、公開APIと内部実装の見通しが悪くなります。

また、複数人で開発する場合、差分管理やレビューのしやすさも重要です。

そのため、プライベートモジュールフラグメントは便利な機能ですが、常に使うべきものではありません。

小規模なモジュールや、実装が非常に単純なケースで使うのが向いています。

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

大きなモジュールを内部的に分割する仕組み

モジュールパーティションは、1つの大きなモジュールを内部的に複数の部分へ分割するための仕組みです。

たとえば、1つのライブラリモジュールの中に、文字列処理、数値処理、ファイル処理など複数の機能がある場合、それらをパーティションとして分けることができます。

これにより、モジュール全体を1つのまとまりとして外部に見せつつ、内部構造を整理できます。

外部APIの分割とは少し違う

モジュールパーティションは、基本的には同じ名前付きモジュールの内部を整理するための仕組みです。

外部利用者に対して、個別の独立したライブラリとして見せるというより、モジュール内部の構成を分けるためのものと考えた方がよいです。

外部にどのパーティションの内容を公開するかは、メインのモジュールインターフェース側で制御できます。

ヘッダーユニット

既存ヘッダーをインポートする仕組み

ヘッダーユニットは、既存のヘッダーをモジュール風に扱う仕組みです。

標準ライブラリのヘッダーや既存のライブラリヘッダーを、従来のインクルードではなくインポートの形で使える場合があります。

ただし、ヘッダーユニットは自作の名前付きモジュールとは性質が異なります。

環境依存が大きい

ヘッダーユニットは、コンパイラ、標準ライブラリ、ビルドシステムの対応状況に大きく左右されます。

そのため、学習用の説明では、自作モジュールのインポートと、標準ライブラリヘッダーのインポートを分けて考えるのが安全です。

実務では、標準ライブラリや既存ライブラリについては、当面は従来通りインクルードを使う方が安定する場合も多いです。

標準ライブラリモジュール

import stdは自作モジュールとは別に考える

近年のC++では、標準ライブラリをモジュールとして利用する import std という書き方も注目されています。

ただし、これはC++20の基本的な名前付きモジュールの仕組みとは分けて理解した方がよいです。

自分で定義したモジュールをインポートする話と、標準ライブラリ全体をモジュールとしてインポートする話は、実装状況や利用条件が異なります。

コンパイラと標準ライブラリの対応が重要

標準ライブラリモジュールは、コンパイラだけでなく、標準ライブラリ実装やビルドシステムの対応も必要になります。

そのため、ある環境では使えても、別の環境ではそのまま使えないことがあります。

入門段階では、まず自作モジュールの基本を理解し、その後で標準ライブラリモジュールの対応状況を確認するのがよいです。

テンプレートとモジュール

テンプレートも公開できる

C++モジュールでは、テンプレートも外部に公開できます。

従来のC++では、テンプレートの定義はヘッダーに置くことが多くありました。

これは、テンプレートが利用側でインスタンス化されるため、定義が見えている必要があるからです。

モジュールでも、テンプレートを外部に公開することは可能です。

実装を完全に隠せるとは限らない

ただし、テンプレートを使う場合、利用側で必要になる情報が多くなります。

通常の関数であれば、宣言だけを公開し、実装を別の実装単位に隠すことができます。

しかし、テンプレート、インライン関数、コンセプト、constexpr関数などは、利用側で定義情報が必要になることがあります。

そのため、モジュールを使えばテンプレートの実装を完全に隠せる、とは単純には言えません。

マクロとモジュール

名前付きモジュールではマクロ汚染を抑えやすい

モジュールの大きなメリットの1つは、マクロ汚染を抑えやすいことです。

従来のヘッダー方式では、ヘッダー内のマクロが読み込んだ側にも影響します。

一方、自作の名前付きモジュールでは、マクロを通常のAPIのように外部へ公開するわけではありません。

そのため、従来よりもマクロによる予期しない影響を減らせます。

ヘッダーユニットや既存ヘッダーでは注意が必要

ただし、モジュールを使えばマクロの問題が完全になくなるわけではありません。

既存ヘッダーを読み込む場合や、ヘッダーユニットを使う場合、マクロの扱いには引き続き注意が必要です。

特に、Cライブラリや古いC++ライブラリ、プラットフォーム依存のAPIでは、マクロが多用されていることがあります。

そうしたコードをモジュールと組み合わせる場合は、影響範囲を慎重に確認する必要があります。

C++モジュールのメリット

コンパイル時間の改善が期待できる

モジュールを使うことで、同じヘッダーを何度も解析する負担を減らせる可能性があります。

特に、大規模プロジェクトやテンプレートの多いコードでは、従来のヘッダー方式がビルド時間の大きな負担になることがあります。

モジュールでは、インターフェースをコンパイル済みの形で利用する実装が一般的なため、コンパイル時間の改善が期待できます。

ただし、これは常に保証されるものではありません。

プロジェクト構成、モジュール分割の粒度、コンパイラ、ビルドシステム、クリーンビルドか差分ビルドかによって効果は変わります。

公開APIと内部実装を分けやすい

モジュールでは、外部に公開するものを明示できます。

これにより、ライブラリの利用者に見せたい機能と、内部でのみ使う実装詳細を分けやすくなります。

API設計の観点では、これは大きなメリットです。

内部実装に依存した使い方を防ぎやすくなり、保守性の高い設計につながります。

インクルード順序の問題を減らせる

従来のヘッダー方式では、インクルード順序によってコンパイル結果が変わることがあります。

モジュールでは、依存関係をインポートとして扱うため、こうした問題を減らしやすくなります。

ただし、既存ヘッダーやマクロを併用する場合は、従来の問題が残ることもあります。

依存関係が明確になる

モジュールを利用する場合、どのモジュールに依存しているのかが明示されます。

これにより、コードを読む人にとっても、ビルドシステムにとっても、依存関係を把握しやすくなります。

特に大規模開発では、依存関係の見通しがよくなることは保守性に大きく影響します。

C++モジュールの注意点

ビルドシステムの対応が重要

モジュールは、単にソースファイルを書き換えれば使えるというものではありません。

利用側がモジュールをインポートするためには、そのモジュールのインターフェースが先に処理されている必要があります。

そのため、ビルドシステムはモジュール間の依存関係を理解し、正しい順序でコンパイルしなければなりません。

従来のヘッダー方式よりも、ビルドシステムとの連携が重要になります。

コンパイラごとの差がある

C++モジュールは標準化された機能ですが、コンパイラごとの実装状況や使い方には差があります。

特に、モジュールインターフェースのファイル拡張子、ビルドオプション、標準ライブラリモジュールの対応、ヘッダーユニットの扱いなどは環境によって異なります。

そのため、実務で導入する場合は、使用するコンパイラとビルド環境でどの程度対応しているかを確認する必要があります。

既存コードとの混在が必要になる

多くのC++ライブラリや既存プロジェクトは、今もヘッダー方式を前提にしています。

そのため、モジュールを導入しても、すべてのヘッダーをすぐに置き換えられるわけではありません。

現実的には、新規コードや内部ライブラリから段階的にモジュール化し、既存ヘッダーとはしばらく共存させる形になります。

ツールチェーン全体の対応が必要

モジュールを本格的に使うには、コンパイラだけでなく、IDE、LSP、静的解析ツール、ビルドシステム、CI環境なども対応している必要があります。

特にチーム開発では、開発者ごとの環境差やCI環境での再現性も重要です。

言語仕様としてモジュールが使えることと、プロジェクト全体で安定して運用できることは別問題です。

BMIとは何か

モジュール情報を保持する中間ファイル

多くのコンパイラでは、モジュールインターフェースを処理した結果として、利用側が参照する中間ファイルを生成します。

これは一般的にBMI、つまりBinary Module Interfaceと呼ばれます。

利用側は、このBMIのような情報を通じて、モジュールの公開インターフェースを利用します。

標準化された共通形式ではない

BMIの形式はC++標準で統一されていません。

そのため、あるコンパイラで生成したモジュール情報を、別のコンパイラでそのまま使うことは基本的にできません。

また、同じコンパイラでもバージョンが変わると互換性に注意が必要な場合があります。

この点は、モジュールをライブラリ配布に使う際に重要です。

ファイル拡張子について

標準では拡張子は決まっていない

C++標準は、モジュールインターフェースファイルの拡張子を規定していません。

そのため、実務では環境や慣習によって複数の拡張子が使われます。

代表的には、.cppm.ixx.mpp.ccm などが使われます。

プロジェクト内では統一するべき

拡張子自体は標準で決まっていませんが、プロジェクト内では統一した方がよいです。

ファイル名や拡張子がバラバラだと、ビルド設定やチーム内の理解が複雑になります。

また、モジュール名とファイル名も必ず一致する必要はありませんが、実務では対応させた方が分かりやすくなります。

includeは不要になるのか

すぐに完全不要にはならない

C++モジュールが導入されたからといって、#include がすぐに不要になるわけではありません。

既存のライブラリ、C言語由来のAPI、OSのヘッダー、マクロを多用するライブラリなどは、今後もしばらくヘッダー方式で利用されることが多いでしょう。

そのため、モジュールとヘッダーは当面共存すると考えるのが現実的です。

新規コードから段階的に導入するのが現実的

実務でモジュールを導入するなら、いきなりプロジェクト全体を置き換えるよりも、新規コードや依存の少ない内部ライブラリから始めるのが安全です。

たとえば、数学処理、文字列処理、設定管理、ログ出力など、比較的独立した小さな機能からモジュール化すると導入しやすくなります。

C++モジュールが向いているケース

新規開発のライブラリ

新しく作るライブラリでは、最初からモジュールを前提に設計しやすいため、導入しやすいです。

公開APIを明確にし、内部実装を整理した状態で開発を進められます。

依存関係が整理された内部コンポーネント

依存関係が少なく、外部ライブラリへの依存も限定的な内部コンポーネントは、モジュール化に向いています。

既存ヘッダーやマクロへの依存が少ないほど、モジュールのメリットを得やすくなります。

コンパイル時間が問題になっているコード

巨大なヘッダーやテンプレートを多用していて、コンパイル時間が問題になっているプロジェクトでは、モジュール化によって改善できる可能性があります。

ただし、実際の効果は測定しながら判断する必要があります。

C++モジュールの導入に慎重になるべきケース

古いコンパイラをサポートする必要がある場合

複数の古いコンパイラや環境をサポートしなければならないプロジェクトでは、モジュール導入のハードルが高くなります。

C++20に対応していても、モジュール機能の対応が十分でない場合があります。

マクロを多用しているコード

マクロに強く依存しているコードは、モジュール化が難しい場合があります。

設定切り替え、プラットフォーム依存処理、条件付きコンパイルが大量にあるコードでは、従来のヘッダー方式の方が扱いやすいこともあります。

ヘッダーオンリーで配布したいライブラリ

ヘッダーオンリーライブラリは、利用者がヘッダーを読み込むだけで使える点がメリットです。

モジュール化すると、ビルドシステムやコンパイラの対応が必要になるため、配布や利用のハードルが上がる可能性があります。

実務での導入方針

小さなモジュールから試す

最初は、依存関係の少ない小さな機能からモジュール化するのがおすすめです。

いきなり全体をモジュール化すると、ビルド設定や既存コードとの相性で問題が発生しやすくなります。

小さな範囲で試し、ビルド時間や開発体験、IDE対応を確認しながら広げていく方が安全です。

公開APIを明確にする

モジュールを導入する際は、どの機能を外部に公開し、どの機能を内部に隠すのかを整理することが重要です。

単にヘッダーをモジュールに置き換えるだけでは、モジュールの利点を十分に活かせません。

公開APIの設計を見直す機会として捉えると、より効果的です。

既存ヘッダーとの共存を前提にする

実務では、しばらくモジュールとヘッダーが混在する状態になります。

標準ライブラリや外部ライブラリについては、無理にすべてをモジュール化しようとせず、安定している方法を選ぶことが重要です。

自作コードはモジュール、既存ライブラリはヘッダー、というように使い分けるのが現実的です。

ビルド環境を先に確認する

モジュール導入では、ビルド環境の確認が非常に重要です。

使用しているコンパイラ、CMakeなどのビルドシステム、CI環境、IDEがモジュールに対応しているかを事前に確認する必要があります。

特にチーム開発では、個人の環境だけで動いても不十分です。

全員の開発環境とCIで安定して動くことが重要です。

よくある誤解

モジュールを使えばリンクが不要になるわけではない

モジュールは、コンパイル時の依存関係や公開範囲を改善する仕組みです。

関数やクラスの実体が別の翻訳単位にある場合、最終的には従来通りリンクが必要です。

モジュールはリンク処理をなくす機能ではありません。

importはincludeの別名ではない

インポートは、インクルードの単なる別名ではありません。

インクルードはテキスト展開ですが、インポートはモジュールとして提供されたインターフェースを利用する仕組みです。

この違いが、コンパイル時間、依存関係、マクロの扱いに大きく関係します。

モジュール名とファイル名は必ず一致しない

C++標準では、モジュール名とファイル名が一致しなければならないとは決められていません。

ただし、実務では一致させた方が分かりやすく、ビルド設定や保守もしやすくなります。

モジュールを使えばすべての実装を隠せるわけではない

通常の関数実装は隠しやすくなりますが、テンプレートやインライン関数などは、利用側に必要な情報がインターフェースに現れることがあります。

そのため、モジュールを使えばあらゆる実装詳細を完全に隠せる、という理解は正確ではありません。

今すぐ全プロジェクトをモジュール化すべきではない

モジュールは有用な機能ですが、すべてのプロジェクトにすぐ導入すべきとは限りません。

既存コードの規模、外部依存、対応コンパイラ、ビルド環境、チームの習熟度を考慮し、段階的に導入するのが現実的です。

前回内容で特に注意すべき点

標準ライブラリのインポートは環境依存

標準ライブラリヘッダーをインポートする例や、標準ライブラリ全体をモジュールとして使う例は、環境依存が大きいです。

そのため、入門記事や実務向けの説明では、自作モジュールのインポートと、標準ライブラリの扱いを分けて説明するのが安全です。

特に初心者向けには、標準ライブラリは従来通りインクルードする前提で説明した方が混乱を避けられます。

既存ヘッダーを使う場合はグローバルモジュールフラグメントを意識する

モジュール内で既存ヘッダーを使う場合、どこで読み込むかが重要になります。

従来の感覚でモジュール宣言後にヘッダーを読み込むと、環境やヘッダーの内容によって問題が起こる場合があります。

既存ヘッダーを使う場合は、グローバルモジュールフラグメントを使う構成を検討するのが安全です。

コンパイル時間改善は保証ではない

モジュールはコンパイル時間改善が期待できる機能ですが、必ず速くなるとは限りません。

モジュール分割が細かすぎる場合、依存スキャンのコストが大きい場合、ビルド環境の対応が未成熟な場合などは、期待したほどの効果が出ないこともあります。

導入時は、実際のプロジェクトでビルド時間を測定しながら判断することが重要です。

まとめ

C++のモジュール機能は、従来のヘッダー方式が抱えていた問題を改善するために導入された重要な機能です。

主なメリットは、公開APIと内部実装を分けやすいこと、マクロ汚染を抑えやすいこと、依存関係を明確にしやすいこと、コンパイル時間の改善が期待できることです。

一方で、実務導入には注意点もあります。

コンパイラやビルドシステムの対応状況、標準ライブラリモジュールの環境依存、既存ヘッダーとの混在、ツールチェーン全体の成熟度を確認する必要があります。

C++モジュールは、ヘッダーを今すぐ完全に置き換えるものではありません。

まずは新規コードや小さな内部ライブラリから段階的に導入し、ビルド環境や開発体験を確認しながら広げていくのが現実的です。

特に重要なのは、モジュールを「includeの新しい書き方」と考えないことです。

モジュールは、C++におけるコードの公開範囲、依存関係、ビルド構造をより明確にするための仕組みです。

上手に使えば、大規模なC++開発の保守性とビルド効率を改善する強力な手段になります。

以上、C++のモジュール機能についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次