モジュールは、C++20 の 4 つの大きな機能 (概念、範囲、コルーチン、およびモジュール) の 1 つです。モジュールには、コンパイル時の改善、マクロの分離、ヘッダー ファイルの廃止、見苦しい回避策など、多くの約束があります。
なぜモジュールが必要なのですか?一歩下がって、実行可能ファイルを取得するための手順を説明したいと思います.
単純な実行可能ファイル
もちろん、「Hello World」から始めなければなりません。
// helloWorld.cpp #include <iostream> int main() { std::cout << "Hello World" << std::endl; }
プログラム helloWorld.cpp から実行可能な helloWorld を作成すると、そのサイズが 130 倍になります。
スクリーンショットの数字 100 と 12928 はバイト数を表します。
内部で何が起こっているかについて、基本的な理解が必要です。
従来のビルド プロセス
ビルド プロセスは、前処理、コンパイル、リンクの 3 つのステップで構成されます。
前処理
プリプロセッサは、#include
などのプリプロセッサ ディレクティブを処理します。 そして #define
.プリプロセッサは、#inlude ディレクティブを対応するヘッダー ファイルに置き換え、マクロ (#define) を置き換えます。 #if
などのディレクティブのおかげで 、 #else
、 #elif
、 #ifdef
、 #ifndef,
と #endif
ソース コードの一部を含めたり除外したりできます。
この単純なテキスト置換プロセスは、GCC/Clang ではコンパイラ フラグ -E、Windows では /E を使用して確認できます。
おお!!!前処理ステップの出力は、50 万バイトを超えています。私は GCC を責めたくありません。他のコンパイラも同様に冗長です:CompilerExplorer.
プリプロセッサの出力は、コンパイラの入力です。
コンパイル
コンパイルは、プリプロセッサの各出力に対して個別に実行されます。コンパイラは C++ ソース コードを解析し、アセンブリ コードに変換します。生成されたファイルはオブジェクト ファイルと呼ばれ、コンパイルされたコードがバイナリ形式で含まれています。オブジェクト ファイルは、定義のないシンボルを参照できます。オブジェクト ファイルは、後で再利用するためにアーカイブに入れることができます。これらのアーカイブは静的ライブラリと呼ばれます。
コンパイラが生成するオブジェクトまたは翻訳単位は、リンカーへの入力です。
リンク
リンカーの出力は、実行可能ファイル、静的ライブラリ、または共有ライブラリにすることができます。未定義シンボルへの参照を解決するのはリンカーの仕事です。シンボルは、オブジェクト ファイルまたはライブラリで定義されます。この状態の典型的なエラーは、シンボルが定義されていないか、2 回以上定義されていないことです。
この 3 つのステップからなるビルド プロセスは、C から継承されています。翻訳単位が 1 つしかない場合でも、十分に機能します。ただし、複数の翻訳単位がある場合、多くの問題が発生する可能性があります。
ビルド プロセスの問題
完全にしようとせずに、ここに従来のビルド プロセスの欠陥を示します。モジュールはこれらの問題を解決します。
ヘッダーの繰り返し置換
プリプロセッサは、#include ディレクティブを対応するヘッダー ファイルに置き換えます。最初の helloWorld.cpp プログラムを変更して、繰り返しが見えるようにしましょう。
プログラムをリファクタリングし、hello.cpp と world.cpp の 2 つのソース ファイルを追加しました。ソース ファイル hello.cpp は関数 hello を提供し、ソース ファイル world.cpp は関数 world を提供します。両方のソース ファイルには、対応するヘッダーが含まれています。リファクタリングとは、プログラムが以前のプログラム helloWorld.cpp と同じことを行うことを意味します。簡単に言えば、内部構造が変更されます。新しいファイルは次のとおりです:
- hello.cpp と hello.h
// hello.cpp #include "hello.h" void hello() { std::cout << "hello "; }
// hello.h #include <iostream> void hello();
- world.cpp と world.h
// world.cpp #include "world.h" void world() { std::cout << "world"; }
// world.h #include <iostream> void world();
- helloWorld2.cpp
// helloWorld2.cpp #include <iostream> #include "hello.h" #include "world.h" int main() { hello(); world(); std::cout << std::endl; }
プログラムのビルドと実行は期待どおりに機能します:
これが問題です。プリプロセッサは、各ソース ファイルで実行されます。これは、ヘッダー ファイル
これはコンパイル時間の無駄です。
対照的に、モジュールは 1 回だけインポートされ、文字通り無料です。
プリプロセッサ マクロからの分離
C++ コミュニティのコンセンサスが 1 つあるとすれば、それは次のようなものです。プリプロセッサ マクロを削除する必要があります。なんで?マクロの使用は、C++ のセマンティックを除いた単なるテキスト置換です。もちろん、これには多くのマイナスの結果があります。たとえば、マクロを含める順序によっては、アプリケーションで既に定義されているマクロや名前と衝突する可能性があります。
webcolors.h と productinfo.h をヘッダー化する必要があると想像してください。
// webcolors.h
#define RED 0xFF0000
// productinfo.h
#define RED 0
ソース ファイル client.cpp に両方のヘッダーが含まれている場合、マクロ RED の値は、ヘッダーが含まれている順序によって異なります。この依存関係は非常にエラーが発生しやすいです。
対照的に、モジュールをインポートする順序に違いはありません。
シンボルの複数定義
ODR は One Definition Rule の略で、関数の場合に言います。
- 関数は、どの翻訳単位でも複数の定義を持つことはできません。
- 関数は、プログラム内で複数の定義を持つことはできません。
- 外部リンケージを持つインライン関数は、複数の翻訳で定義できます。定義は、各定義が同じでなければならないという要件を満たす必要があります。
1 つの定義規則に違反するプログラムをリンクしようとしたときに、リンカーが何を言わなければならないかを見てみましょう。次のコード例には、header.h と header2.h の 2 つのヘッダー ファイルがあります。メイン プログラムはヘッダー ファイル header.h を 2 回インクルードします。したがって、func の 2 つの定義がインクルードされるため、1 つの定義ルールが破られます。
// header.h void func() {}
// header2.h #include "header.h"
// main.cpp #include "header.h"
#include "header2.h" int main() {}
リンカーは、func の複数の定義について不平を言っています:
私たちは、ヘッダーの周りにインクルード ガードを配置するなどの醜い回避策に慣れています。インクルード ガード FUNC_H をヘッダー ファイル header.h に追加すると、問題が解決します。
// header.h #ifndef FUNC_H #define FUNC_H void func(){} #endif
対照的に、モジュールと同一のシンボルはほとんどありません。
この投稿を終了する前に、モジュールの利点を要約したいと思います。
モジュールの利点
- モジュールは一度だけインポートされ、文字通り無料です。
- モジュールをインポートする順序に違いはありません。
- モジュールと同じシンボルはほとんどありません。
- モジュールを使用すると、コードの論理構造を表現できます。エクスポートするかどうかの名前を明示的に指定できます。さらに、いくつかのモジュールをより大きなモジュールにバンドルして、論理的なパッケージとして顧客に提供することもできます。
- モジュールのおかげで、ソース コードをインターフェース部分と実装部分に分ける必要はありません。
次は?
モジュールは多くのことを約束します。次回の投稿では、最初のモジュールを定義して使用します。