世界を動かす技術を、日本語で。

C++モジュールの扱い方は?

概要

  • 本記事はC++モジュールに対する筆者個人の見解
  • C++モジュールの主目的は ビルド速度の劇的向上
  • 現状、期待された速度向上が実現されていない現状分析
  • 標準化プロセスや実装困難性の課題指摘
  • プロジェクト管理と設計アプローチの失敗例を解説

C++モジュールの現状と課題

  • 本記事は個人の意見 であり、Mesonや他組織の方針ではないことを強調
  • C++モジュールの開発者達の努力を否定する意図はなく、 現状を批判的に分析
  • C++モジュールが 5倍以上のコンパイル速度向上 (理想は10倍)を複数OSSで示せなければ、標準から除外すべきという立場
  • 現状のままでは、 リソース投入はサンクコストの罠 に陥る危険性
  • モジュール導入当初の主目的は ビルド高速化 であり、ヘッダーインクルード方式のO(N²)問題解消が狙い
  • しかし、次第に ビルドの分離性(バグ回避) が議論の中心となり、パフォーマンス向上の話題は後退
  • マクロ漏洩等のバグは 発生頻度が低く、多くの開発者にとっては深刻な問題ではない現実
  • 一方で、 ビルドの遅さは全てのC++開発者の大きな問題

標準化と実装の失敗

  • C++20でモジュールが標準化 されたが、5年以上経ってもまともに動作しない現状
  • 実装困難性を指摘した声もあったが、「絶対に必要だから」と 強引に標準化
  • 複数の関係者による証言からも、 進捗の遅さや実装の困難さ が浮き彫り
  • ビルドシステムとコンパイラの密結合 が求められるが、ISO標準はファイルの存在すら想定していない
  • 各関係者が 自分の得意な部分だけ を実装し、全体最適がなされていない
  • コンパイラ開発者との議論でも、「 コンパイラをビルドシステムにしたくない」との理由で全案却下される事例
  • Mesonへのモジュール対応も、 一時ファイルや追加フラグ管理の複雑さ で断念

プロジェクト管理と設計の問題

  • 複数組織横断の大規模プロジェクト には、強力なプロダクトオーナーが不可欠
    • 全体を統括し、各チームに指示を出せる権限者 の不在
    • 理論上も、そのような人物が存在し得ない構造
  • ソフトウェア設計の鉄則「 既存実装の標準化」を無視し、 壮大な設計先行 で進めた失敗例
  • 実装やプロトタイプを持たず、「必要だから今すぐ」と標準化を強行
  • 小規模な実験から始め、 段階的に拡張・性能測定・改善 を繰り返すべきだった

import stdによるモジュール活用とその限界

  • import std は、標準ライブラリに限定したモジュール適用事例
    • 依存関係がなく一括生成できる点で導入が容易
    • C++開発の遅さの多くは標準ライブラリのインクルードが原因
  • 実装は プリコンパイルドヘッダー(PCH)と本質的に同じ
    • 実際、GCCではモジュールファイルがPCHの流用であるとの情報
    • 速度向上もPCHと同程度(Visual Studioで10-20%、Clang/GCCで数%)
  • 抜本的な性能向上には至らない という見通し

5倍高速化の根拠と現状

  • 既存技術を上回る速度向上」が新機能の合理的条件
  • 筆者が独自設計した高速コンパイル向け標準ライブラリで 4倍の速度向上 を実現した事例
  • 既に4倍が達成可能なら、 5倍要求は妥当な基準
  • 速度向上が実証できないなら、 設計の根本的な見直し が必要

次の論点:C++モジュール設計と今後の展望

  • 既存のC++コンパイラ改修は非常に困難 であり、開発参入障壁が高い
  • モジュール開発を試すための Python製“モジュールプレイグラウンド” の紹介
  • 本質的な性能・運用上の課題が解決されない限り、 C++モジュールの将来は厳しい

Hackerたちの意見

90年代に、C++コンパイラ(Symantec C++)のためにプリコンパイルヘッダーを実装したんだ。モジュールみたいな感じだったよ。動作モードは2つあって、1つ目は全ての.hファイルをコンパイルして、一気にバイナリとして出力する方法。2つ目は、各.hファイルがそれぞれ独自のプリコンパイルヘッダーを作成する方法。モジュールみたいだよね?とにかく、たくさん学んだことがあって、特にC++に意味的な改善がないと、コンパイルは速くなるけど壊れやすいってことが分かった。この経験がDモジュールの設計に活かされたんだ。Dモジュールは本当に素晴らしい。モジュールに求めていたことが全て詰まってる。特に、モジュールの意味はどこからインポートされるかに完全に依存しないんだ。まあ、C++もDのモジュール設計を取り入れたらいいと思う。C++は25年の使用実績があるモジュールを手に入れることができて、かなり満足できるはず。そう、Cのプリプロセッサマクロが問題だってことは理解してる。俺のおすすめは、プリプロセッサを置き換える言語的な解決策を見つけること。C++はほぼそこまで来てるから、あとは仕上げてプリプロセッサをゴミ箱に捨てればいいんだ。

C++のテンプレートがこれを機能させるためには、何を変えなきゃいけないの?モジュール内でテンプレートを定義して、他の場所でインスタンス化したり特殊化したりするのは特に難しそうだね。

モジュールの標準化プロセス中に、誰かから意見を求められたことはある?Dは最も明白な先行技術のように見えるけど、モジュールの標準化プロセスは特に呪われてた感じがする。

仕事を終わらせて、プリプロセッサをゴミ箱に捨てちゃえばいいんだよね。そう、これがC++の問題の核心だと思う。スタンダード委員会が引いた線が悪くて、モジュールのエンコーディングがほぼ不可能になってる。良いモジュールシステムと速いインクリメンタルビルドを持つ他の言語では、プリプロセッサスタイルのクレイジーなことは、かなり厳しい境界がないと許可されない。少し間違った形になった言語(例えば、Rustのprocマクロみたいな)でも、そういうメタプログラミングがどこでどう行われるかに制約がある。プリプロセッサがゴミ箱に捨てられなくても、モジュールシステムからは除外されるべきだよ。メタプログラミングは、明確なインターフェースと相互作用を持つ言語の機能であるべき。例えば、Javaではアノテーションプロセッサが最終的にコード生成機能を引き起こす。アノテーションがなければ、メタプログラミングもなし。完璧ではないけど、C/C++のフリーなマクロシステムよりはずっとマシだよ。もう一つの選択肢はGoのやり方。コンパイラにコードを生成させるんじゃなくて、ビルドシステムにコード生成を担当させる(コードジェネレーターを呼び出す)。それなら、必要なときに開発者がその遅延を選べるから、ずっと良くなると思う。

自分が関わったC++プロジェクトで、Cヘッダーをインクルードして、普通に動くことに頼らなかったものは思いつかないな。C++の「モジュラー」からCマクロを禁止する方法ってあるのかな?(Cの依存関係をすべて調べて、何らかのラッパーを書く/生成しなきゃならないのは、多くの人には受け入れられないだろうけど。)

C++モジュールはインポート時のマクロに影響されないし、マクロをエクスポートすることもないから、問題は何なんだろう?

コンパイルを5倍速くするための賢い方法は、ほぼ10年前に実装されて、驚くほどうまくいったのに、完全に無視されたんだ。スタンダード委員会から進展は期待してないよ。興味があればこちらをどうぞ: https://github.com/yrnkrn/zapcc 次にスタンダード委員会に完全に無視される大きな進展は、100%メモリ安全なC/C++コンパイラで、これも実装済みで驚くほどうまくいくよ: https://github.com/pizlonator/fil-c

これは何の魔法だ。何年もHNを読んでたけど、メモリ安全なC++を持ち出す人を初めて見たよ。なんでこれがヘッドラインに載ってないの?何か問題があるの、ビルド時間とか?それを手に入れるために家を売らなきゃいけないの? 編集: ああ、トレードオフが分かった: hollerithが2024年2月21日に投稿 | 前 | 次 [–] >Fil-Cは、私のテストによるとレガシーCより約200倍遅いです。

コンパイルを5倍速くするための賢い方法は、ほぼ10年前に実装されて、素晴らしく機能したのに、完全に無視された。スタンダード委員会から進展は期待してないよ。興味があれば、これだよ: https://github.com/yrnkrn/zapcc もちろん、完全に無視されたよ。スタンダード委員会がコンパイラでキャッシングを強制すると思った?それは彼らの仕事じゃないから。 > 次にスタンダード委員会に完全に無視される大きな進展は、100%メモリ安全なC/C++コンパイラで、これも実装されていて素晴らしく機能する: https://github.com/pizlonator/fil-c また—スタンダード委員会がこのコンパイラの使用を強制すると思ったの?スタンダード委員会はコンパイラを「標準化」しないんだよ…

なんで誰がzapccをccacheの代わりに使うべきなの?もしそれがコンパイラの内部データを全部保存するってことなら、確かに高くつきそうだよね。これらのコンパイラツールが言語の革新をもたらすわけじゃないってことは、知ってると思うけど。どちらも本番環境には向いてないし、正しく動かないとデバッグもめっちゃ難しそう。

コンパイルを5倍速くするための合理的な方法は、ほぼ10年前に実装されて、すごくうまくいったのに、完全に無視されてしまった。スタンダード委員会からの進展は期待してないよ。興味があればここにあるよ:https://github.com/yrnkrn/zapcc ccacheのようなツールは20年以上前から存在していて、導入するのは実行可能ファイルをインストールして環境フラグを設定するだけで済むんだ。zapccがccacheが提供していない何をもたらすと思う?

zapccは本当に期待できそうだね!残念ながら、メンテナンスされてないみたいで、5年間コミットが見当たらないんだ。

標準委員会によって完全に無視される次の大きな進展は、100%メモリ安全なC/C++コンパイラで、これも実装されていてすごくうまく動いてるんだ: https://github.com/pizlonator/fil-c > 安全でないコードにリンクすることすらできない。これはかなり理論的な話になるね。

モジュールは、実際の依存関係のあるプロジェクトでは使えないよ。それが変わらない限り、俺はモジュールを見ようとは思わない。

モジュールは一般的に依存関係とどう関係してるの?モジュールを使いながら、依存関係をインクルード経由で同時に使うことができるよ。これ、うまくいってる。

C++を10年以上やって、今は約4年Rustをやってる。全体的にRustの方がずっと好きだけど、ヘッダーファイルが本当に恋しい。モジュールはビルド時間がひどい。実装を変更すると(普通はヘッダーを編集しないようなこと)、起こる再ビルドの量がクレイジーなんだ。最低限の注意を払ってセットアップしたC++プロジェクトと比べてもね。

Dのモジュールはすごく速いよ。うちの多くの顧客は、DがC++よりずっと速くコンパイルできることに頼ってる。

ヘッダーについて何だったんだろう?インターフェースと実装を定義する際の再ビルドパフォーマンスに関して、rustcがそれを自動的に処理してくれるのは興味深いね。詳しくはここを見てみて:https://rust-lang.github.io/rust-project-goals/2025h2/relink...

プライベートモジュールの断片(ソースファイルに相当)を変更しても、インターフェースが変わらないから再ビルドは起こらないはずだよ。

モジュールはスピード以上のものを提供するよ。コンパイル時間のメリットは素晴らしいし、記事もビルド時間の膨張がすべての開発者の悩みだって言ってるけど、モジュールにはもっと深い目的があるんだ。明示的なサブユニットのカプセル化。本当の隔離。奇妙な前方宣言や、無限にネストされたifdefガード、狂ったヘッダー依存グラフはもう必要ない。物事はそれぞれ独立して存在して、原子性があって、決定論的で信頼できる。モジュールは多分見直しが必要だし、採用は遅れてるけど、一度モジュールを使い始めたら、二度と戻れないよ。明示的に宣言されたインターフェースの明確さと、ヘッダー地獄からの解放は、C++コードの整理の仕方を根本的に変える。信じられないなら、新しいプロジェクトをモジュールで始めてみて。試してみて。今の最大の障壁は、下流での使用なんだ。使われていないからサポートされてないし、サポートされていないから使われていない。けど、一度使い始めたら、毎回新しいコンパイラのリリースを楽しみに待つようになるよ。彼らが受けるべき注目を得られることを願ってね。

何年も経って、実際にコンパイラが機能させるだろうと期待して、あまり機能しない特徴を使うのは、災難の計画に見える。人々はモジュールを使おうとしたけど、一般的に失敗して捨てちゃった。今の形のモジュールがC++でレガシー機能以外の何かになる可能性は低いと思う。いつか新しい実装が現れるかもしれないけど、noexceptが失敗した「throws」宣言を置き換えたように。

なんでダウンボートされてるのかわからないけど、これだけで俺は切り替えるよ(まだQt mocのサポートを待ってるけど):> 変な前方宣言はもういらない。俺たちC++開発者は、2025年になってもこのハックが全然オッケーだってことを受け入れてるんだ、ビルド時間を改善するためにね。

つまり(間違ってたら指摘してね)、モジュールを使うと、すべてのコードがモジュールを使わなきゃいけないってことだよね(例えば、プロジェクト内で# includeとimportを混ぜることはできない)。これだと、依存したいサードパーティのコードがたくさん使えなくなるんだ。

C++委員会がやるべきだったのは、「import」を導入することだけだった。「これはincludeと同じだけど、コンテキストが漏れない」ってね。既存のコードベースを移行するのはすごく簡単だったはず(includeをimportに置き換えるだけで済むし)、コンパイラ側の初期実装も今あるものとほぼ同じにできて、最適化の余地も簡単に提供できたのに。代わりに、既存のプロジェクトに適合させるのが不可能な全く新しいものを作りたがったから、実質的に死産だよ。

C++0x以降のすべての追加機能について言えることだね。委員会は後方互換性を逆行して、完全に新しいやり方を導入するために微妙な変化を拒否してる。既存のやり方とは全然合わないから、誰も20年前のコードベースを直したくないんだよ。

「どのコンテキストも漏れない」ってどういう意味?遷移的なインポートをエクスポートしないってこと?つまり、#include#includeも実行するけど、import vectorはvectorだけをインポートして、もしvec.begin()を変数に代入したいならimport iteratorが必要になるってこと?それとも、インポートの順番がどうでもよくて、インポートファイルのプリプロセッサディレクティブがインポートされたファイルに影響を与えないってこと?

コンパイラの作者たちがコンパイラをビルドシステムにしたくない気持ちはわかるけど、実際にやってくれたらすごく助かるな。Makefileとか他のビルドアーティファクトを作るのは、ほとんどのアプリケーションにとってエネルギーの無駄だよね。

ビルドシステムは誰が思っているよりもずっと複雑だよ。もしあなたのビルドシステムが私たちがやる変なことをすべてカバーしていないなら、全く使えないよ。

「続ける前に、$beverageを一杯取っておいた方がいいよ。これ、ちょっと時間かかるから。」その代わりに、コンパイルを始めたよ。

ここでの標準化プロセスは、JavaScriptモジュールのことを思い出させるね。ES2015で導入されたけど、言語の標準には構文といくつかの不変条件しかなかった。モジュールの読み込み方や、どうやって配信されるのか、ルートにモジュールがあるプログラムがどうやって始まるのかについての概念がなかった。でも、「ES2015にはこれが必要だ」という緊急性は似てたね。Chromeチームに入った後、最初のプロジェクトの一つとしてそのギャップを埋めることにしたんだ。それについては[1]にドキュメントがあるよ。(この記事の「それを終わらせる唯一の本当の方法は、プロダクトオーナーを持つことだ...」っていうのを思い出す。)標準のJSモジュールが、AMDやCommonJSモジュールのハックされたソリューションと競争する様子を、C++モジュールがプリコンパイルヘッダーと競争するのに例えることもできるかも。ただ、C++モジュールの問題はJavaScriptのよりも厄介そうだね。技術的な設計が難しそうだし、JSのホスト/言語の分離はC++の仕様/コンパイラの分割よりもクリーンに見える。おそらく最も重要なのは、組織的に、すべてのブラウザが集まってJSモジュールのロードの標準に取り組むための明確な場所(WHATWG)があったことだね。C++コンパイラの開発者同士の協力のためのフレームワークはあまりないみたいだし。

彼は本当にいいポイントを指摘してるね。これは本当に悲劇だよ。import stdは素晴らしいことになって、もっと楽にできたはずなのに、委員会が欲張っちゃったんだ。