C++ 20 のモジュールって実際どうなの?と思ったので調査。
現時点ではそこまでおすすめできないけど、思ったよりはちゃんと使えるし、新規の小さなプロジェクトで使ってみるくらいはしても良いんじゃないかなと思います。
モジュールについて
ざっくり言えば、クラスや関数を他のソースから使えるようにするための新しいやり方。ヘッダーと異なり、マクロやプリプロセッサ (#include を含む) が利用側に漏れないので
-
ヘッダーの展開が減りコンパイル時間を減らせる
-
ヘッダーのインクルード順でコンパイル結果が変わる問題が起こらない
-
意図せぬマクロによる汚染が起こらない
といったメリットがある。
構文や仕様は cpprefjp や cppreference、 Clang のドキュメント などにあるので、この記事では解説を省く。
使用状況
Are We Modules Yet? というサイトがある。これは vcpkg に公開されている OSS のうち、モジュール対応できているものを示したもの。
トップページには、モジュール対応の推移を示したチャートがある。これを見ると着実に進んでいるように見える…?
片対数グラフ…!
縦軸が対数なので見た目と割合が一致しない。この記事の執筆時点 (2026年8月) では、モジュールに対応しているプロジェクトは 4.2%。
ここから Browse All Projects というページに移動すると、モジュール対応できているプロジェクトがリスト表示される。
左端に ✅ があるものはモジュール対応が済んでいるプロジェクト。imgui, fmt, nlohmann-json などの有名ライブラリもある。これらの多くは従来通りヘッダーを提供しつつ、それをラップする形でモジュールを提供していると思われる。imguiのように、ラッパーを第三者が公開しているものもある。
検索フィルターとして Modules Natives にチェックを入れると、モジュールネイティブのプロジェクトが表示される。これは内部実装も含めてモジュールを使っているプロジェクトであることを意味する。
この記事の執筆時点ではモジュールネイティブなプロジェクトは 17 個。
モジュールはまだ浸透してないし、ライブラリとしては C++20 以前もサポートしたいだろうから、この状況なのは仕方ない。
C++20 の標準化から6年になるが、モジュールを使うのはまだアーリーアダプターだと思う。モジュールの目的は理解できるけど、コルーチン等と違いできることが増えるわけではないので、使う必要性もそこまで無い。多くのライブラリがモジュールに置き換わる、という未来は当分は来なさそう。
参考情報
先に挙げた cppreference や Clang の資料のほか、以下の記事が参考になる。
これは言語仕様の解説ではなく、モジュールを使ってプログラムを書くユーザー視点のアドバイスという位置づけ。
また、先に挙げた Module Native なプロジェクトも実際の例として参考になる。
技術的なメモ
個人的に気になった点を書いていく。
ツールは執筆時点 (2026年8月) の最新版を使用した。
- MSVC: ツールセット 14.51 (Visual Studio 2026 18.9.0)
- CMake: 4.4.2
- Ninja: 1.13.2
- clangd: 22.1.8
モジュールの分割
モジュール名は my_app.utils のようにドット (.) を使える。ドットはただの名前の一部なので、構造上は my_app と my_app.utils の間に上下関係はないが、ソースコードを置くディレクトリの構造や名前空間に合わせるなど、階層構造を表現するのに使える。 my_app.utils.foo.bar のように、ドットはいくつでも繋ぐことができる。
パーティションを使うと、モジュールを複数の翻訳単位に分けることができる。パーティション名は、my_app:utils のように、モジュール名の後にコロン (:) を付けて表記する。モジュールと同様、 my_app:utils.foo.bar のように名前にドットを使える。
利用側はモジュール単位でインポートすることになるので、その際の使い勝手を考慮してモジュールとパーティションを使い分けると良さそう。例えば my_app.utils:foo, my_app.utils:bar の形だと、利用側は import my_app.utils; の形で取り込むので、 :foo や :bar を意識せずに済む。逆に import my_app.utils.foo; のように細かい粒度でインポートさせたい場合は、モジュールを分ける。
利用側はモジュールのインターフェース変更の影響を受けるため、細かく分けた方が再コンパイルの範囲を狭めやすい。例えば utils:foo, utils:bar のようなパーティションだと、利用側は import utils; として取り込むので、 :foo にある関数しか使っていない場合でも :bar の変更の影響を受ける。 utils.foo のように別モジュールであれば、utils.bar の影響は受けない。
ライブラリの場合、import my_lib; で公開APIを全て取り込めるようにしておくと使いやすいと思う。その場合、公開用のモジュール内で my_lib.sub などを再エクスポートする。小さいライブラリであれば、モジュールは1つだけにして、内部のコード分割はパーティションだけでも良いと思う。
拡張子
モジュールインターフェースファイルの拡張子として、MSVCでは .ixx, Clang では .cppm がよく使われる。GCCは特定の慣習は無い。
ヘッダーファイルだと拡張子の違い (.h, .hpp, .hh) は問題にならないが、MSVCでは注意が要る。MSVC は .ixx という拡張子を特別扱いするようで、他の名前のファイルをモジュールインターフェースとして扱う場合は /interface と /TP というオプションを付ける必要がある 1。その場合でも、コンパイルは通るが Intellisense や clangd によるコード解析がうまく動かないことがある。
MS文化圏の影響を受けたくない、より汎的な名前にしたい、ということで .cppm を使いたくなるが、MSVC が前提であれば .ixx が無難そう。
私自身は MSVC を使うことが多いが、この記事では .cppm で統一する。
インターフェースと実装を分ける
モジュールでもインターフェースと実装 (.cppm と cpp) を分けることができる。その場合、ヘッダーと同様に .cppm には宣言だけを書き、実装を .cpp に書く。テンプレートは .cppm に書くというのも同じ。
モジュールの場合、ヘッダーと違って何度も展開されないし、 ODR の問題も避けられるので、小規模であれば .cppm だけでも問題ない。
ヘッダーと同様、.cppm と .cpp を分けておくことで、宣言は変わらず実装だけ変わった場合に、利用側は再コンパイルせずに済む。
STL
C++ 23 では import std; で標準ライブラリの機能をまとめて取り込める。初回のビルドでは std モジュールのコンパイルが走るが、それ以降はヘッダー展開のコストなしに STL の機能を使える (もちろん、テンプレートの実体化のコストが減るわけではない)。
プロジェクトで使っている STL の機能が数個しかなく、初回だけとはいえ STL にあるもの全てをコンパイルするのが無駄だと感じる場合は、後述のヘッダーユニットを使うと良さそう。
名前付きモジュールとヘッダーユニット
ここまで書いた、 .cppm で宣言して import xxx; の形で取り込むものは、名前付きモジュール (named module) と呼ぶ。それに対し、ヘッダーファイルを import "foo.hpp"; のように取り込むものをヘッダーユニットと呼ぶ。
ヘッダー自体は従来通りなので、「foo.hpp はヘッダーユニットである」と表現するよりも、「foo.hpp をヘッダーユニットとして使う」のように表現するのが適切に思う。
マクロ
名前付きモジュールはプリプロセッサやマクロが外に漏れない。逆にいえば、マクロを利用者に提供するデザインのものは、名前付きモジュールにできない。
ヘッダーユニットは、 import の外にあるマクロは使われないが、ヘッダーの中にあるマクロは外に漏れるという性質がある。
// BAR の定義は foo.hpp に渡されない
#define BAR 1
import "foo.hpp";
// foo.hpp の中で定義されたマクロは
// これ以降のコードで使える
ヘッダーユニットでも、外部のマクロに依存するものは使えない。例えば <cassert> は、NDEBUG というマクロが定義されているかどうかで動作を切り替える仕組みのため、ヘッダーユニットとして使えない。
既存のヘッダーをヘッダーユニットにする
外部のマクロに依存するものや、 特定の順序でインクルードする必要のあるものは、それをラップしたヘッダーをヘッダーユニットとして使うと良さそう。これはプリコンパイルヘッダーに似た目的で使える。
// wrapper.hpp
// WIN32_LEAN_AND_MEAN の定義があると
// Windows.h の中身が小さくなる
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
// 先に atlbase.h をインクルードしてから atlcom.h を
// インクルードしないとエラーになる
#include <atlbase.h>
#include <atlcom.h>
// 利用側
import "wrapper.hpp";
// Windows 系のマクロもここで使える
wrapper.hpp はヘッダーユニットとしてコンパイルされるので、利用側は <Windows.h> や <atlbase.h> を展開せずに済む。複数のファイルが wrapper.hpp をインポートしていても、巨大なヘッダーが展開されるのは wrapper.hpp のコンパイル時のみ。
ただし、 ヘッダーユニットにできるファイル (importable header) の要件は実装定義となっている。MSVCの場合は自作ヘッダーでも import できたが、ツール側が「既知のヘッダーのみ許容する」といった制限を設けることも仕様上は許容されるので、必ずしも使えるとは限らない。
ヘッダーユニット用のファイルを作るときは、外部のマクロに依存せず自己完結させる。
ヘッダーとモジュールの両対応
既存のライブラリについて、ヘッダーの提供を維持しつつモジュール版も提供する場合、 Clang の Transitioning to modules にある ABI non-breaking styles を参考にすると良い。
やり方は上記を参照してもらうとして、ここでは、そもそも何でこうするの?という話を書く。これにはモジュールの所有権が関係する。cpprerence の Module Ownership の説明も読んでおくと良い。
モジュールには以下のルールがある。
-
宣言はいずれかのモジュールに紐づく
-
名前付きモジュールに紐づかない宣言は、グローバルモジュールに紐づく
- モジュール導入以前の書き方のコードは暗黙的にグローバルモジュールに紐づく
宣言が名前付きモジュールに紐づく場合、これは名前マングリングに影響し、従来とは異なるABIになることがある。 Clang の資料で ABI breaking style, ABI non-breaking style と書いているのはこのため。
ヘッダー版とモジュール版の両方を提供する場合、所有権の問題を避けるため、宣言をグローバルモジュールに紐づける工夫が要る。
例えば STL がこの対応をしておらず、宣言が std モジュールに置かれていたと仮定する。その場合、以下のようなコードが問題になる。
// 外部のヘッダー内で #include <string> しており、
// std::string がグローバルモジュール内で宣言される。
#include "third_party.hpp"
// 自分達のコードは C++23 を使っており、
// モジュール版の STL を使いたい。
//
// 実際のSTLでは起こらない仮定の話だが、
// ここでは std::string の宣言が std モジュールに
// 置かれるものとする。
import std;
// std::string の宣言が
// stdモジュールとグローバルモジュールの
// 両方に紐づくため不正になる
int main() {
std::string s = "Hello, World!";
}
「自分達のコードではモジュール版を使う」くらいなら徹底できるかもしれないが、このように外部のヘッダーでインクルードされる可能性も考えると、ヘッダー版との混在を避けるのは難しい。
ヘッダー版と互換性のあるモジュールを提供する方法として、Clang の資料では export-using style と export extern-C++ style の2つを紹介している。後者については、言語リンケージ (extern "C" や extern "C++") の中にある宣言はグローバルモジュールに紐づく、というルールを知っておく必要がある。
ヘッダー側に手を入れる必要はあるが、export extern-C++ style の方がメンテナンス性は良さそう。export-using style だと、ヘッダーに宣言が追加された際に .cppm 側も更新する (export using xxx を同期させる) 必要がある。
参考までに、GCC の STL 実装は export-using style, MSVC の STL 実装は export extern-C++ style のようである。GCC はこのファイル、MSVC はこの箇所やこの箇所が参考になる。
include と import の順序
モジュール内では、ヘッダーのインクルードはグローバルモジュールフラグメントに置く。これは各種資料に書いてあるので分かりやすい。
通常の実装ファイルではどうかというと、「言語仕様としての規則は無いが、コンパイラ側の制約があるためインクルードを先に書いた方が良い」といえる。これは、ヘッダーとモジュールの両方に同じ宣言がある場合の動作が関係する。
言語仕様上は以下のどちらも認められるが、後者は現在の主要コンパイラではエラーになる。
// include が先
#include <vector>
import std;
// include が後
//
// 言語仕様上は問題ないが、コンパイラによっては
// 「std::vectorが多重定義されている」といったエラーが出る
import std;
#include <vector>
関連する情報として、MSVC の Developer Communityの投稿 や STL の issue 4666, Clang の issue 61465, GCCの issue 99000 などがある。
MSVC のドキュメントでは「import は include の後でなくてはならない」と書いているが、この文面だけだと言語仕様上の規則なのかコンパイラの制約なのかが分からない。Clang の issue では「どちらも受け入れるべきだが、コンパイラの実装により制約がある」という書き方をしている。
言語仕様上は規則がないものの、この制約が事実上あるものと考え、include は常に import よりも先に書くのが良いだろう。
内部パーティション (実装パーティション)
同じモジュール内からのみ参照でき、そのモジュールのインターフェースに寄与しないパーティションを内部パーティション (internal partition) と呼ぶ。資料によってはこれを実装パーティション (implementation partition) と呼んでいる。
// 内部パーティション (export を付けない)
module my_app.utils:foo;
// このモジュール内にある型や関数は
// my_app.utils の内部実装にのみ利用できる
前述の宣言と実装を分ける方法だと、実装はモジュール単位になってしまい、「このパーティション用の実装ファイル」のようなことができない。この記事やこの記事では、パーティションの実装を別ファイルに切り出すためのテクニックとして内部パーティションを使っている。これはプロジェクトの規模が大きい場合に、コンパイル時間を抑えるために役立つ可能性がある。
しかし、気を付けるべき点も多いので、必要になるまでは使わなくて良いと感じた。
内部パーティションを使う場合は、以下のような注意がある。
-
内部パーティションをモジュールインターフェース内でインポートするのを避ける
-
処理系により convention が異なる
-
MSVC では
.cppを使う (.ixxは不可) -
Clangd では
.cppmを使う (慣習的,.cppも可)
-
-
ツール側の設定
-
MSVC では
/internalPartitionというフラグを付ける 4 -
CMake では
FILE_SET CXX_MODULESに含める
-
MSVC では .ixx はモジュールインターフェース専用であり、内部パーティションには使えない。内部パーティションには .cpp を使い、/internalPartition を付けて特別扱いする必要がある。一方、 CMake や Clang では内部パーティションはモジュールインターフェースに近い扱いをする。
処理系による違いが厄介に感じる。「モジュールインターフェースには .ixx または .cppm を使う」だけなら理解しやすいが、内部パーティションは「通常の実装ファイルに寄せる (.cpp) 」と「モジュールインターフェースに寄せる (.cppm)」 の二派があり、事情を知らないと混乱する。.cpp に寄せた場合、通常の実装ファイルと区別しづらい問題もある。同じ .cpp でも、内部パーティション用のファイルのみモジュール用の設定 (MSVCの /internalPartition フラグや CMake の CXX_MODULES) をするというのは、分かりづらいと感じた。
CMakeLists の書き方
以下のように書く。
add_library(my_lib STATIC)
target_sources(my_lib
PUBLIC
FILE_SET CXX_MODULES
FILES
lib.cppm
utils.cppm
...
)
他の target から利用するモジュールでなければ PUBLIC にする必要はない。
FILE_SET のところは、構文上は FILE_SET (任意の名前) TYPE CXX_MODULES のように書くが、TYPE と同じ名前の場合は上記のように省略できるというルールがある。ヘッダーを使う従来のライブラリでも、公開用ヘッダーを示す際に FILE_SET HEADERS のような形で使ったことがある人もいるかと思う。
clangd の設定
CMake + Ninja + clangd の構成を前提にする。
-
CMake の cache 変数で
CMAKE_EXPORT_COMPILE_COMMANDSを有効にしておき、configure 時にcompile_commands.jsonがビルドフォルダに作られるようにする -
clangd の起動時に
--experimental-modules-support引数を付ける
モジュールは experimental 扱いなので、起動時に上記のフラグが要る。VS Code の拡張機能から使う場合は.vscode/settings.json で以下のように設定する。
{
"clangd.arguments": [
"--experimental-modules-support"
]
}
Visual Studio の Intellisense も clangd も、モジュールの対応はまだ成熟しているとは言い難い。自分が試した範囲だと、関数やクラスにコメントを付けても利用側のコードではそれを認識せず、識別子をホバーしてもシグニチャしか表示されないという問題があった。
ビルドへの影響
モジュールを使う場合、コンパイルに順序関係が生じる。例えばモジュールAを利用するソースコードBをコンパイルするには、先にモジュールAをプリコンパイルし、その出力 (BMIという) を利用する必要がある。これはコンパイラやビルドシステムだけでなく、clangd のようなツールにも影響する 5。
コンパイルに順序が生じると、従来のような単純な並列化ができず、並列化の自由度が下がる。そのため、ジョブ数が多い場合のビルドが遅くなるという報告もある (Are modules fast? – P1441R1)。
この辺りはコードの規模や依存関係の複雑さ、ビルドマシンのスペックにもよると思う。Alibabaの事例では、モジュールへの置き換えによりビルド時間が 42% と大きく短縮した例があるので、うまく使えた場合の効果はありそう。
所感
触ってみるとそれなりには使えるし、試す価値も無いわけではないけど、現時点で使う必要はあまり無いと感じた。5年後の C++ 開発ではモジュールが中心になっている、とは思えない。
マクロやインクルードの問題を防げるなど、言語仕様としては良いと思うものの、ツールやエコシステムとしてはまだ課題があると感じる。
ツールの対応は少しずつ進んでいるので、興味があれば使ってみても良いと思う。ライブラリのように他の人に使ってもらいたい、古い C++ バージョンを考慮する必要があるといったものであれば、ヘッダーファイルにしておくのが無難だろう。
-
https://learn.microsoft.com/en-us/cpp/build/reference/interface ↩
-
https://clang.llvm.org/docs/StandardCPlusPlusModules.html#reachability-of-internal-partition-units ↩
-
https://cmake.org/cmake/help/git-stage/manual/cmake-cxxmodules.7.html#module-visibility ↩
-
https://learn.microsoft.com/en-us/cpp/build/reference/internal-partition ↩
-
https://chuanqixu9.github.io/c++/2025/12/03/Clangd-support-for-Modules.en.html ↩


