モジュールはツールの機会ではない

C++ モジュールは標準化プロセスを経ており、現在の計画では、C++20 に間に合うように C++ 標準にマージされる予定です。これらは優れた言語機能であり、ヘッダーよりも多くの利点があります

  • よりモダンに感じます
  • 彼らは多い 解析が速い
  • マクロに対する保護を提供します
  • ODR 違反に対する保護を提供します。

コードベースでヘッダーをヘッダーに置き換えることができるようになるのが本当に待ちきれません. 」 設計に非常に複雑な機能を追加する機能は、決してレガシーになることはなく、短期的な利益のために長期的な問題の原因となります.私は間違っているかもしれませんが、間違っていることを願っています.

しかし、私が最も懸念しているのは、ツールとモジュールがどのように相互に統合されるかという問題です。この問題は、やや手放されたように感じます.論文 C++ Modules Are a Tooling Opportunity は、より優れたツールを求めています.著者に同意しないわけにはいきません.C++ ツールは、ほとんどの場合、過去に行き詰まっています.

ただし、モジュールが魔法のようにツールの改善につながると考えるのは非常に楽観的です。つまり、モジュールがより優れたビルド システムにつながることはほとんどありません。ビルド システムは、企業をリードする最終製品にとってあまり価値がありません。

  • 何十年にもわたって一連のスクリプトを有機的に成長させます。それらはほとんど機能しませんが、より良いソリューションにアップグレードするために数人のエンジニアに何か月も支払うことを誰も望んでいません
  • 既存のソリューションを使用して、より広いエコシステムの恩恵を受ける

このパターンは CMake への採用につながりました (ほとんど機能しないが、幅広いエコシステムの恩恵を受ける一連のスクリプト) は、多数の製品によって作成されています。また、メンテナーが信頼を失う前にクリティカル マスに到達できず、単に地下室で 3 人が放棄または使用しています。

新しいビルド システムの成長には何年もかかり、多額の投資が必要です。存在を望むことはできません。その約束された魔法のツールがどんなに欲しくても。

さらに重要なことは、モジュールをスムーズに処理するためにツール (ビルド システム、IDE、リファクタリング、インデックス作成など) が直面する課題は、ツールの古さや品質とは無関係です。問題は単純です。数百または数千のモジュールがあると想像してください。それ以上かもしれません。それほど多くのモジュールを用意するのに、大企業である必要はありません。 LLVM や chrome に小さな機能を追加したい場合や、vcpkg を使用する場合もあるでしょう。 多数の依存関係を処理します。この美しいコードがすべて存在するのに、なぜ車輪を再発明する必要があるのでしょうか?

新しいプロジェクト用に大量のファイルを作成します

//foo.cppm
export module cor3ntin.foo;
export import cor3ntin.foo.bar;
import google.chrome.net.http;

export namespace cor3ntin {
 inline void this_is_not_important() {}
}

//bar.cppm
export module cor3ntin.foo.bar;

//main.cpp
import cor3ntin.foo;
int main() {
 cor3ntin::this_is_not_important();
}

これらは多少主観的なものですが、実際にはかなりエレガントでモダンに見えます.いくつかの点に注意することが重要です.

  • 私のモジュールは cor3ntin.foo と呼ばれています :. 本質的な意味はありません:モジュールはそうではありません モジュール名の一部として組織名を使用することで、プロジェクトとその依存関係全体で一意性を確保できます。誰もあなたにそうするように強制することはありませんが、そうしてください?
  • 私が最初にすることは、モジュール名のような部分と呼ばれる名前空間を開くことです。モジュールは名前空間メカニズムではありません。 C++ の世界では、レガシーと、名前空間とモジュールのいくつかの違いにより、ある程度は理にかなっていますが、他の多くの言語で行われていることとは対照的であるため、多くの人を驚かせます (私も最初は驚きました)。

CMakeFile もあります。

add_executable(foo
 main.cpp
 foo.cppm
 bar.cppm
)
target_link_library(foo PUBLIC google-chrome::net)

そして、Cmake にビルドを実行するように依頼します。むしろ、ビルドを実行するさらに装備の整っていないツール用のスクリプトを生成することです。cmake は main.cpp 何にも依存していないため、それが依存関係グラフに最初に配置されます。

> compilator3000 main.cpp -o main.o
Error: no module named cor3ntin.foo

もちろん、この時点では、探しているモジュール バイナリ インターフェイスはまだプリコンパイルされていません。どうすれば修正できますか?

依存関係グラフを手動で表現する

明らかな解決策は、すべてのモジュールの依存関係グラフを手動で作成することです。

add_cpp_module(bar-module bar.cppm)
add_cpp_module(foo-module foo.cppm DEPENDS bar-module google-chrome::net-http-module)
add_executable(foo
 main.cpp
 foo-module
 bar-module
)
target_link_library(foo PUBLIC google-chrome::net)

これは現在有効ではありません CMake 各モジュールのターゲット (グラフ ノード) を明示的に作成します。また、cmake はモジュールをサポートしていませんが、依存関係グラフを手動で表現するこの種の方法は、モジュールが持つように見える方法です。モジュール TS をテストした企業によって使用されています。

その cmake を使用すると、正しい順序で処理を実行できます:

  • ビルド google-chrome::net-http-module google.chrome.net.http をインポートできるように BMI
  • ビルド bar-module cor3ntin.foo.bar をインポートできるように BMI
  • ビルド foo-module 現在存在する BMI cor3ntin.foo.bar をインポートする および google.chrome.net.http
  • main.cpp をビルドする
  • 実行可能ファイルをビルドする

それで、それはうまくいくでしょう。そして、モジュールがそのように使用されることが期待されるかもしれません.

私が生後 2 週間ほどのとき、母から重複を避けるように言われました。彼女は、それは優れたエンジニアリングの実践であると説明しました.それは完全に理にかなっており、それ以来、コードの重複を避けるよう努めています.他の人々もそう考えているようです.重複はありません。

業界として、私たちはコードの重複がコードの保守を難しくすることを知っており、私たちは親切な人であるため、コードを保守しやすいものにしたいと考えています。

モジュールも例外ではありません。再利用可能で共有可能な、適切に区切られた作業単位にコードを配置することは、コードの重複を避ける方法です。

なぜ私はあなたにそのすべてを話しているのですか?さて、私たちのプロジェクトを見てみましょう。

ファイル foo.cppm があります . cor3ntin.foo を宣言します モジュール。 foo-module によって構築されたもの target?これは同じことを 3 回言っています。異なる名前で。そして、ことわざにあるように、コンピューター サイエンスで最も困難な 3 つの問題は名前付けと一貫性です。

さらに重要なのは、モジュールの依存関係を複製してしまったことです。add_cpp_module(... DEPENDS bar-module) ビルド スクリプトでは、import cor3ntin.foo.bar; とまったく同じ情報をエンコードします。 つまり、ファイルにモジュールを追加または削除するたびに、ビルド スクリプトを編集する必要があります。

(個々のモジュールのビルド フラグを指定していないことにも注意してください。ただし、これも追加する必要があるため、重複や複雑さが増す可能性があります)

何百ものモジュールがある場合、または依存関係のビルド スクリプトを書き直す必要がある場合、このスキームは実際には維持できません。 modules になります やや魅力的ではありません.ビルド スクリプトを追加する必要はありません.

依存関係グラフの自動構築

代わりに、私たちが本当に望んでいるのは、最初の CMakeFiles の単純さに戻ることです。

add_executable(foo
 main.cpp
 foo.cppm
 bar.cppm
)
target_link_library(foo PUBLIC google-chrome::net)

そして、 cmake になります 頭いい。難しい注文ですが、ご了承ください。Cmake はすべてのファイルを開き、それらを lex して、すべてのモジュールの依存関係のリストを抽出します。

Main:モジュール宣言ではありませんが、cor3ntin.foo に依存します foo.cppm :これは cor3ntin.foo というモジュールです 、それは cor3ntin.foo.bar に依存します と google.chrome.net.http . main.cpp の依存関係に追加します bar.cppm :これは cor3ntin.foo.bar というモジュールです . foo.cppm の依存関係に追加します

CMake は、google.chrome.net.http を宣言するファイルを見つけるために、Chrome のコード ベース全体を解析する必要もあります。 .

そのためには、各ファイルを開き、マクロを含む可能性のある「プリアンブル」を前処理し、ディレクティブを含める必要があります。条件付きでコードなどをインポートするので、時間がかかります。また、解析は正確でなければならないため、実際の依存関係を取得するには本格的なコンパイラに任せる必要があり、これは遅い ベンダーは、プロセスを開かなくても依存関係を解決するためのライブラリを提供できるかもしれません。確かに期待できます!あるいは、import を支持する P1299 かもしれません。 宣言 どこでも その場合、cmake は常にすべての c++ を前処理して lex する必要があります。

google.chrome.net.http の依存関係だけを気にしている場合でも、しばらくすると、CMake は chrome コードベースと私たちのすべてのモジュールの依存関係グラフをメモリに保持します。 .これはキャッシュする必要があるため、ビルド システムはステートフルである必要があります。これは問題の原因ではないと思いますが、指摘する価値はあります。

この時点で、依存関係グラフが作成され、ビルドを開始し、スケールに興味がある場合はノードをビルドするためにディスパッチできます。はっきり言って、多くの企業がそうしなければなりません。 Google のコード ベースが妥当な時間内に私のラップトップに組み込まれるとは思えません。

foo.cppm を変更するとしましょう .ビルド システムはそれを確認し、必要なものをすべて再構築する必要があります。別として、2 種類のビルド システムについて説明します。

  • コードベースの変更時に、これらの変更を適用するようにアーティファクトを更新するための最小限かつ十分な一連のタスクを常に実行するシステムを構築する
  • 無駄なシステムを構築する。より多くのツールを期待してください!

しかし、多くのことが起こった可能性があります:

  • モジュールの名前を変更しました (export module cor3ntin.foo を変更しました) export module cor3ntin.gadget へ )
  • インポートを追加しました

そして、あなたはあらゆるにそれをしたかもしれません 変更されたファイル

したがって、ビルド ツールは、変更したすべてのファイルを再度 lex する必要があります。そして、ディペンデンシー グラフを再構築します。 cmake の世界では、これは cmake を再度実行することを意味します。ジェネレーターは単純にそれを処理できません

ソース コードを変更すると、あらゆる方法で依存関係グラフが変更されます。これは非常に新しいものです。これが機能すると、翻訳単位やシステムの構築よりもコーダーに集中できるので、これも非常に優れていると思います。

しかし、反対に、コンパイルするたびに、変更されたファイルのフル スキャンを実行する必要があります。コンピューター上、ビルド ファーム上、どこでも.これには 5 秒かかることもあれば、数分かかることもあります.そして、コードが完全にモジュール化されている場合 (数年以内には実現されることを願っています)そのスキャンが完了するまで実行してください。

ビルド システムの話はこれで十分です。IDE について話しましょう。

main.cpp を変更することにしました であるため、IDE でプロジェクトを開きます。たぶん、Qt Creator、VS、VSCode、emacs など、あなたの空想をくすぐるものなら何でも。また、それが IDE の目的です。そのため、IDE は、インポートされたすべてのモジュール内のすべてのシンボルのリストを探します。モジュールは移植可能ではないため、IDE はそのソース ファイルを読み取ろうとします。モジュール cor3ntin.foo をインポートしたことがわかります。 そのため、適切なモジュールを宣言するファイルが見つかるまで、プロジェクトとその依存関係のすべてのファイルを必死にレックスし始めます。すべての輸入申告に対してそれを行う必要があります。あなたの MacBook は非常に熱くなっており、物質の新しい状態を発見しています。そして、うまくいけば、数分後に、使用可能なシンボル インデックスが作成されます

または、IDE が clangd などの外部シンボル サーバーに従う可能性があります。 .コンパイルデータベースが必要です。ソースが変更されるたびに再構築する必要があります。

実際、シンボルのインデックス作成や静的解析の実行などを行う必要があるツールは、すべてのインポートのコンパイル済み BMI にアクセスできるか、モジュール名をファイル名にマップできる必要があります。

ツールの問題に対する考えられる解決策

モジュール マップ

もはや追求されていないモジュールの clang 提案には、モジュール名をファイル名にマップする「モジュール マップ」ファイルがあります。多くの重複と、同期していないことのリスク

モジュール マッピング プロトコル

P1184 は、コンパイラがビルド システムにクエリを実行し、特定の名前に一致する BMI の場所を問い合わせることができるようなプロトコルを提案しています。その後、ビルド システムが BMI が利用可能であることを通知するまで、各コンパイルはおそらくアイドル状態になります。コンパイラをビルド システムに変更しないように非常に注意しており、サーバーに変更することを検討しています。

何が問題になる可能性がありますか?

このようなシステムは特に、cmake などのメタ ビルド システムでは機能しません。個人的には、メタ ビルド システムが嫌いなので気にしませんが、覚えておく価値はあります。

宣言するファイルの名前にモジュールの名前を入れます

これは私のお気に入りのソリューションです。議論され、却下されたと思います。

アイデアは単純です。ファイル foo.cppm を使用する代わりに 、ファイルがモジュール名 cor3ntin.foo.cppm をエンコードする必要があります .そして .cppm にします モジュールの必須拡張機能。たとえば、

  • ビルド システムは、どのファイルがモジュールで、どのファイルがそうでないかを想定できます。
  • import cor3ntin.foo に遭遇したとき 、次にスキャンするファイルがすぐにわかります。

これは、ビルド システム以外のツールにとって特に望ましいことですが、ビルド システムが依存関係グラフを整然と構築するのにも役立ちます。ただし、そのファイルに対応する 1 つのノードにのみ頂点を追加または削除します。

パフォーマンスの観点からは、ディレクトリのスキャンは c++ の字句解析よりもはるかに高速です。ファイルのスキャンは、他のほとんどの主流の OS よりも通常 10 倍遅くなる Windows でのパフォーマンスは依然として懸念事項ですが.

重複の問題は解決されますが、ほとんどの言語では堅牢性のために、ソース ファイルとファイル名の両方に情報を含めることを選択しています。

この提案の欠点

  • パスまたはファイル名でエンコードする必要があるかどうかについては、多少の議論が予想されますが、モジュールには意味論的な階層の概念がないため、これは問題ではありません。
  • 名前付けファイルは言語の範囲外にあるため、wg21 の範囲外と見なされる可能性があります。意味的に意味のある完全なファイル名を持つ言語を無視する場合を除いて、それは本当だと思います:
    • Java
    • パイソン
    • ハスケル
    • アーラン
    • DA 確かに他にもいくつかあります。

標準化へのウッドストックのアプローチ

多くの人は、モジュール インターフェイスを宣言するファイルの名前またはパスに何らかの構造を課すことの利点を認識しているようです。しかし、彼らは、それはベンダーに任せるべきだと考えています。ツールが集まって、同様の理由で同様の解決策に同意するでしょう。花の力で、私は推測します.これは素晴らしいことですが、C++ は標準ではありません。 .普遍的な依存関係マネージャーの夢​​は、共通の言語を話す場合にのみ実現できます。

標準では、ファイルについて言及する必要さえありません。 「モジュール名 X」の行に沿って何かを推測します リソース X.cppm によって宣言された一意のモジュールを識別します

モジュールに関するその他の問題

これはモジュールの主要な問題だと思いますが、それだけではありません.たとえば、レガシーヘッダーがビルドシステムレベルでどのようにツール化できるかを誰も知らないと思います.モジュール形式もまったく制限されていません.ビルド システムの動作が特定のコンパイラに依存する可能性があることを意味します。たとえば、Microsoft BMI は Clang よりも最適化されているため、clang はより多くの再構築をトリガーする可能性があります。

そこからどこへ行く?

モジュールについては、サンディエゴで議論されます。そして、彼らは素晴らしいです。はるかに優れている可能性があります。

しかし、ビルド システムやツールとの統合をよりよく把握し、小規模プロジェクトと大規模プロジェクトの両方でビルド時間が約束どおりに短縮されるという確信が得られるまでは、慎重に悲観的なままです。

参考資料

  • FORTRANを思い出してください
  • 暗黙のモジュール パーティション ルックアップ
  • マージされたモジュールとツール
  • P1156への対応
  • モジュールのプリアンブルは不要
  • C++ ツール エコシステムに対するモジュール TS の影響
  • C++ モジュールはツールの機会です
  • ビルド モジュール - YouTube
  • C++ モジュールの進歩 - Youtube

No