より良い C++ モジュールを目指して - パート 1:モジュール マッピング

このブログ投稿では、モジュール マッピングについて説明します。モジュール マッピングは、モジュール名がそのモジュールのインターフェースを定義するソース ファイルに関連付けられるメカニズムです。

モジュールは閉じており、自己完結型です。つまり、すべてのモジュール名に対して、そのモジュールを定義する唯一のソース ファイルが存在する必要があります。

モジュール マッピングは、コンパイラにとって有用ではありません。コンパイラが 08 に遭遇するまでに 宣言、モジュール binary インターフェイスが存在し、コンパイラに認識されている必要があります。

ただし、モジュール マッピングはツールにとって非常に重要です。 .依存関係グラフを構築するにはモジュール マッピングが必要であるため、これは特にビルド システムが常に実行する必要があるものです。

モジュールは依存関係グラフをより動的にし、特にファイルが変更されるたびに依存関係グラフを更新する必要があることに注意してください。

モジュール、どこにいますか?

1 つのモジュールを見つける前に、すべてのモジュールを見つける必要があります。ヘッダーがインクルード ワールドに存在するたびに、モジュールがインポート ユニバースに存在する可能性があります。

  • ヘッダーを使用して生成するのと同じように、コードはモジュールを消費して生成します。
  • STL はスノーフレーク モジュールである可能性が最も高く、それはそこにあるだけです
  • システム ライブラリが使用する可能性があります - 使用しないのはなぜですか?そのため、Debian のすべての開発パッケージにはモジュール インターフェースがある可能性があります。
  • システム以外のサードパーティ ライブラリ - おそらく、これらは git サブモジュール、Conan、Vcpkg、Dennis のオフィスの階下にあるそのコンピューターからマウントされた共有ドライブのどこかにあるフォルダーにあります。
  • お探しのモジュールでさえ、お使いのコンピュータにはまったくないかもしれません。 16 と書くと あなたのすばらしいビルドシステムは、インターネット上で対応するモジュールを取得します。それはまた別の機会に。

要するに、モジュールはパス管理に関連する問題に悩まされないため、ヘッダーよりもモジュールの方が収集しやすいという期待がいくつかありますが、ビルド ツールは、のリストを収集するために多くの場所を調べる必要があります。モジュールを宣言できるファイル .

モジュールを宣言する可能性のあるファイルを見つける可能性のある場所のリストを用意して、個々のモジュール宣言ファイルを収集する必要があります.これを行う簡単な方法は、各ファイルの拡張子を調べることです.28 モジュールを宣言しますか? 30 ? 47 ? 57 ? 65 ? 72 ? 87 ? 91 問題は、標準自体はファイル拡張子に関係がないため、ファイルをスキャンするビルドシステムは、モジュールを宣言する可能性のあるものを何でも突き刺さなければならないということです。ええ、それはおそらく既存のすべての 104 を意味します と 117 習慣から、誰も彼らに指示しないので、人々はこのスキームを使用するライブラリを書くでしょう.

モジュールを突く

特定のファイルで宣言されているモジュールの名前を取得するには、それを開いて前処理し、123 を取得するまで lex する必要があります。 宣言.これはファイルに何百行もある可能性があり、ファイルはビルドシステムが気にしないモジュールグローバルフラグメントも宣言する可能性がありますが、モジュールに非モジュラーコードを含めることができる必要があります。今のところ、ファイルからモジュールの名前を抽出することは自明ではなく、本格的なコンパイラが必要であると言えば十分です.

また、たとえば、翻訳単位がモジュール 136 に依存している場合 144 を宣言するファイルが見つかるまで、何百ものファイルを開く必要がある場合があります。 .一部のシステムでは、ファイルを開いてプロセスを起動するのにコストがかかる可能性があるため、モジュールをファイルにマッピングするのに時間がかかる場合があります。

依存関係の抽出にも同じ問題が存在すると主張するかもしれません。実際、ビルドの依存関係を抽出するには、ファイルを開いて前処理し、lexed する必要があります。

ただし、他にも考慮すべき使用例があります。たとえば、IDE は、単一の翻訳単位の補完を提供するために、クイック マッピングを実行できる必要があります。補完を提供するツール、依存関係に関する指標 (パッケージ マネージャーを含む)、などは、そのマッピングを提供する必要があります。

明確にするために、モジュール<->ファイル マッピングはモジュールの最大のツール可能性の問題ではありませんが、その 1 つです。

より簡単なマッピング

ツールが名前をファイルにマップしやすくするために、いくつかの解決策が提案されています。

ビルド システムでマッピングを手動で記述する

アイデアは、開発者がビルド システムでモジュールを直接記述できるようにすることです。たとえば、cmake を使用する場合、次のように記述できます。

 add_module(foo, foo.cppm)

ただし、これは cmake に関するものではありません。たとえば、 155 です。 まさにそれをサポートします

 mxx{foo}@./: cxx.module_name = foo

何百ものモジュールが存在する場合があるため、これは少し面倒です。また、情報が複製されます (モジュール名はソース ファイルとビルド システムでエンコードされます)。依存関係のそれぞれがどのモジュールを使用し、一般的にどのモジュールを使用するかを知る必要があります。あるビルド システムから別のビルド システムに移行するのは非常に困難です。たとえば、もともと Meson で書かれたライブラリを Bazel ビルドで使用することは非常に困難です。

標準的なモジュール マッピング ファイル

この考え方は、ビルド システムでマッピングを記述するのと少し似ていますが、マッピングを 160 に配置する代わりに または 177 、それを別のファイルに入れ、その構文はスタンディングドキュメントで指定されます(標準ではなくても業界標準にすることを期待して)。

概念的には、このファイルは非常に単純です:

foo: foo.cppm
bar: bar.mpp

これにより、ビルド システム間の移植性の問題が解決されます。しかし、もう 1 つの問題が残ります。モジュール名はまだ重複しています。これも興味深い課題をもたらします。たとえば、ビルド中に生成されたモジュールをどのように処理するかなどです。しかし、さらに重要なことは、これらのファイルはサード パーティのソース ツリー内のどこにあるのかということです。 Debian などのパッケージベースのシステムではどのように機能しますか?

標準レイアウト。

ある論文では、モジュール マッピングをファイル path の一部としてエンコードできることが提案されています。 どこで 187 196 にマップされます .その設計にはいくつかの問題があります

  • ファイルシステムは階層的であると理解されていますが、モジュールはそうではありません。 209にもかかわらず覚えておいてください モジュール識別子内の有効な文字であるため、意味的な意味はありません。219 223 のスーパーセットである必要はありません
  • そのシステムが外部およびシステム ライブラリとどのように連携するかは不明です
  • 強制することはできません
  • どのレイアウトが最適かについて人々が議論しても、どこにも行き着かないでしょう。それは実際にサンディエゴで起こったことです。モジュールに関係なく、標準のレイアウトが依存関係管理の点で利点があるとしても、人々はレイアウトを適応させたくありません。

モジュール名をファイル名の一部にする

これが最も単純で、より正気で、同意しやすいアプローチだと思います。

モジュール 237 242 という名前のファイルで宣言する必要があります 、モジュール 253 262 という名前のファイルで宣言する必要があります . それだけです - とても簡単です。

これにより、上記で明らかになった問題が解決されますが、制約はかなり小さくなります。コードのリファクタリングが容易になり、依存関係グラフの動的性がわずかに低下します (ファイルの名前を変更すると、274 式)

モジュール識別子で使用される文字がほとんどすべてのビルド システムでサポートされているもののサブセットであることを考えると、ファイル名とモジュール名の間には 1 対 1 の対応があります。同意する必要があるのは拡張子だけです。これは解決が必要な問題であると我々が同意すれば、これは実行可能と思われます.

それには優先順位があると私は主張することができます。結局、ディレクティブ 285 の間には 1 対 1 の対応があります。 およびファイル 298 .

このスキームは実際には 302 によって実装されています . build2 のドキュメントでは次のように説明されています。

大きなオーバーヘッドなしでこの解決を実行するために、実装はモジュール インターフェイス ユニットからの実際のモジュール名の抽出を遅らせます (利用可能なすべてのモジュール インターフェイスが必ずしもすべての変換ユニットによってインポートされるとは限らないため)。代わりに、実装は、インターフェイス ファイル パスに基づいて、インポートされる各モジュールを実装するインターフェイス ユニットを推測しようとします。または、より正確には、2 段階の解決プロセスが実行されます。最初に、目的のモジュール名とファイル パスの間の最適な一致が検索され、次に実際のモジュール名が抽出され、最初の推測の正確性が検証されます。

この実装の詳細の実際的な意味は、プロジェクトで使用されるすべてのモジュールを明確に解決するために、モジュール インターフェイス ファイルがモジュール名の一部、またはより正確には、十分な量の「モジュール名の末尾」を埋め込む必要があるということです。この当て推量は、直接モジュール インターフェイスの前提条件に対してのみ実行されることにも注意してください。ライブラリに由来するモジュールの場合、モジュール名は既知であるため、正確に一致します。

残念ながら、312 module<->ファイル マッピングはあいまいで、より脆弱です。ドキュメントでは次のように主張しています:

インターフェース ファイルをそれぞれ hello.core.mxx と hello.extra.mxx と呼ぶこともできますが、これは特に適切とは言えず、プロジェクトで使用されているファイル命名スキームに反している可能性があります。

しかし、この柔軟性は、追加された複雑さに見合うものでしょうか?私は本当にそうは思いません!

同じ簡単に実装可能なマッピングを適用することで、すべてのビルド システムが同様に動作することも保証されます。

C++ ビルド システムの設計は難しいものです。難しくしないようにしましょう。