2021年4月11日日曜日

18: C++におけるヘッダーファイルの趣旨と使用法

<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>

なぜC++は、作成し保守するのが煩わしいと思われるかもしれないヘッダーファイルを必要とするのか、Java等は必要としないのに。どのようにそれらを使うべきなのか、もしも使うべきだとしたら?

話題


About: C++

この記事の目次


開始コンテキスト


  • 読者は、C++の基本的知識を持っている。

ターゲットコンテキスト



  • 読者は、なぜヘッダーファイルがC++で使われるのか、およびヘッダーファイルに何を入れるべきで何を入れるべきでないかを知る。

オリエンテーション


'テンプレート'に関わる込み入った事情が、ある記事にて論じられています。

クラススタティックインテジャーフィールドを定義するための新たに適切な(異論があるかもしれないが)方法がある記事にて紹介されています。

全てのクラスの全てのスタティックフィールドの定義を単一のソースファイルに入れるストラテジーが以後の記事にて紹介される予定です。


本体

ト書き
Hypothesizer 7が独白する。


1: ヘッダーファイルを作成し保守するのは煩わしくありませんか?


Hypothesizer 7
C++は、最も手間の省けるプログラミング言語であるとは言えない。

クラスを定義する(本当は、'宣言する'がより正確だ、後ほど論じられるとおり)際に私が感じる苛立ちは、2個のファイルを作成しなければならないことだ、ヘッダーファイルとソースファイルを、以下のように。

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/ClassA.hpp

@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_ClassA_hpp__
	#define __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_ClassA_hpp__
	
	#include <iostream>
	#include <string>
	
	using namespace ::std;
	
	namespace theBiasPlanet {
		namespace coreUtilitiesTests {
			namespace usingHeaderFilesTest1 {
				class ClassA {
					private:
						static string s_string;
						string i_string {"another string"};
					public:
						static string getStatically ();
						static string getStaticallyInLine () {
							return s_string;
						}
						ClassA ();
						virtual ~ClassA ();
						ClassA (ClassA const & a_copiedObject);
						virtual ClassA & operator = (ClassA const & a_assignedFromObject);
						virtual string getInstanceWise ();
						virtual string getInstanceWiseInLine () {
							return i_string;
						}
						friend ostream & operator << (ostream & a_outputStream, ClassA const & a_classA);
				};
			}
		}
	}
#endif

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/ClassA.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/ClassA.hpp"

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace usingHeaderFilesTest1 {
			string ClassA::s_string {"a string"};
			
			string ClassA::getStatically () {
				return s_string;
			}
			
			ClassA::ClassA () {
			}
			
			ClassA::~ClassA () {
			}
			
			ClassA::ClassA (ClassA const & a_copiedObject): i_string (a_copiedObject.i_string) {
			}
			
			ClassA & ClassA::operator = (ClassA const & a_assignedFromObject) {
				i_string = a_assignedFromObject.i_string;
				return *this;
			}
			
			string ClassA::getInstanceWise () {
				return i_string;
			}
			
			ostream & operator << (ostream & a_outputStream, ClassA const & a_classA) {
				a_outputStream << a_classA.i_string;
				return a_outputStream;
			}
		}
	}
}

確かに、一部のメソッドを'インライン'にすることもできる(私は無節操にはそうしない)が、少なくとも、スタティックフィールドはソースファイルを必要とする(初期化の順序をコントロールするために、私は大抵、全てのクラスの全てのフィールド(スタティックメソッドは別)の定義を単一のソースファイルに入れるが)。

それは煩わしくありませんか?ただ2ファイルを作成しなければならないだけでなく、'#include'命令も書き、「namespace theBiasPlanet {~」というネームスペース部分も再び書き、「string ClassA::getStatically ()」のようなメソッドシグネチャーも再び書かなければならない。そして、クラスを変更する際には、それら2ファイルを開き、整合性が保たれるようにそれらを変更しなければならない。

えーと、ある種のIDEがその痛みをある程度和らげてくれるかもしれないが、私は何のIDEも使わない、いくつかの理由のため(重さ、不満足なテキストエディターを使用するように強要される、コントロール不能に自動生成されるコード、ベンダーロックイン、等)。


2: なぜ、C++ではヘッダーファイルが必要とされるのか、そもそも、本当に必要とされているのか?


Hypothesizer 7
それは、C++が原始的なプログラミング言語だからなのか? . . . えーと、その側面を私は完全には否定しない(C++は、何の足かせもなく新たにデザインされたプログラミング言語ではなく、Cへの建て増しであり、古いものであり、過去の(必ずしも良くない)決定との互換性を保つ義理を負っている)が、C++がヘッダーファイルを使用することには、ある特定の理由がある: C++は、(完全な)分割コンパイルを行なう。

Javaだって分割コンパイルをする、'javac'は単一ソースファイルを取れるから? . . . いいえ、しません: 'javac'は、単一ソースファイルを個別にコンパイルすることはできず、そのソースファイルが依存するクラスファイルたちの中を見なければいけません、もしも、それらクラスファイルたちのいくつかがまだ作成されていなければ必要なソースファイル群をコンパイルした後で。

例えば、2個のパブリッククラスソースファイル、'ClassA.java'および'ClassB.java'が書かれ、'ClassA'が'ClassB'に依存しているとき、'ClassA.java'を、'ClassB.java'が一緒にコンパイルされることなくコンパイルすることはできない。

C++ソースファイルがコンパイルされる際は、他のどのソースファイルもどのライブラリの中も見られることはなく、それが'分割コンパイル'だ。

それがなぜヘッダーファイルを必要とするのか?えーと、それは、必ずしもヘッダーファイルを必要としないが、少なくとも、いくらかの情報がソースファイルへ注入されることを必要とする。

例えば、もしも、当該ソースファイルが、別のソースファイル内で定義されているあるファンクションをコールするのであれば、少なくとも、そのファンクションのシグネチャーがコンパイルされるソースファイルに注入されなければならない。

そうした情報は、実際には、コンパイルされるソースファイル内に直接に書かれても構わないのだが、同一の情報を複数のソースファイルへ直接に繰り返し書くのは、非効率だろうし、誤りを誘うので、そこで、私たちは通常、その情報を一度だけあるヘッダーファイルに書き、そのヘッダーファイルを複数のソースファイルへインポートするのだ。

してみると、ヘッダーファイルは、省力化と誤り防止のテクニックであって、プログラマーたちを苛立たせる悪意ではなく、分割コンパイルを行なうというC++の特性から、必要性を持って起源したものなのだ。

えーと、分割コンパイル自体が今の時代に本当にグッドアイデアであるのかという疑問があるかもしれないが、その疑問には私には答えられない。


3: '#include'が行なうこと


Hypothesizer 7
'#include'がプリプロセッサ命令であることを意識することは有益だろう。

それは、その命令は、ヘッダーファイルのコンテンツを当該ソースファイルへ、本来のコンパイラーが起動される前に、挿入するということである: コンパイラーは、ヘッダーファイルの中を見ることはなく、プリプロセスされたソースファイルのみの中を見るのだ。

ヘッダーファイルをインクルードするのは、単に、ソースファイルへ当該情報を書くための、手間を省く1つのやり方であるに過ぎないことを思い起こそう。

それに対比して、Javaの'import'は、全く別のことである: それは、C++における'using namespace'により近い。


4: ヘッダーファイルが効果を持つ時


Hypothesizer 7
これは当然のことであるのだが、とにかく、自分に思い起こさせておこう。

ヘッダーファイルを書くことは、それ自体では、何もしない。

例えば、あるクラス宣言を含むヘッダーファイルを書いたとき、私はそのクラスを作成しなかった(あるクラスを含むJavaソースファイルを書いたときは、私は、そのクラスを作成したと考える)。

つまり、ヘッダーファイルは、それがあるソースファイルへインクルードされて初めて効果を持つのだ。

その違いは、微妙なものに思えるかもしれないが、引き続くセクション群を考えるために重要である。


5: ヘッダーファイルへ入れられるものが意味すること


Hypothesizer 7
何を、ヘッダーファイルへ入れることができ、また、入れるべきなのか?何を、ソースファイルへ入れることができ、また、入れるべきなのか?

えーと、ヘッダーファイルは、本当には、単に、インクルードされることによって、何らかの物たちをソースファイルへ入れるための1つの方法にすぎないのだが、ヘッダーファイルの要点は、複数のソースファイルへインクルードされるよう意図されているというこだ。確かに、特定の1個のソースファイルへのみインクルードされるよう意図されたヘッダーファイルを作成することはできるが、そのような手法は、ヘッダーファイルの目的に沿っていない(なぜ、そのヘッダーファイルのコンテンツはそのソースファイルに直接に書かれないのか?)。

したがって、ヘッダーファイルへ入れられるものが意味することは、それらは、複数のソースファイルの中にいてもオーケーだということだ。

例えば、任意のグローバル変数の定義は、ソースファイル内にいるべきだ、ヘッダーファイル内ではなく、なぜなら、それは単一の実体であって、単一の実体は1度だけ定義できるからだ。実際、以下はリンクエラーを引き起こす。

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/GlobalVariables.hpp

@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_GlobalVariables_hpp__
	#define __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_GlobalVariables_hpp__
	
	namespace theBiasPlanet {
		namespace coreUtilitiesTests {
			namespace usingHeaderFilesTest1 {
				int g_integer {1};
			}
		}
	}
#endif

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/SourceFileA.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/GlobalVariables.hpp"

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace usingHeaderFilesTest1 {
		}
	}
}

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/SourceFileB.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/GlobalVariables.hpp"

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace usingHeaderFilesTest1 {
		}
	}
}

状況は、ファイルスコープのものでない任意のファンクション他でも同じだ: その重複は許されないし望まれてもいない。

「ファイルスコープ」?えーと、例えば、あるファイル内'static'変数の定義はヘッダーファイル内に許される、以下のように。

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/InFileStaticVariables.cpp

@C++ ソースコード
#ifndef __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_InFileStaticVariables_hpp__
	#define __theBiasPlanet_coreUtilitiesTests_usingHeaderFilesTest1_InFileStaticVariables_hpp__
	
	namespace theBiasPlanet {
		namespace coreUtilitiesTests {
			namespace usingHeaderFilesTest1 {
				static int f_integer {1};
			}
		}
	}
#endif

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/SourceFileA.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/GlobalVariables.hpp"
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/InFileStaticVariables.hpp"

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace usingHeaderFilesTest1 {
		}
	}
}

theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/SourceFileB.cpp

@C++ ソースコード
#include "theBiasPlanet/coreUtilitiesTests/usingHeaderFilesTest1/InFileStaticVariables.hpp"

namespace theBiasPlanet {
	namespace coreUtilitiesTests {
		namespace usingHeaderFilesTest1 {
		}
	}
}

典型的には、ヘッダーファイルへ入れられるものは定義ではなく宣言であるが、宣言は、複数のソースファイル内で複数回書くことができる。

定義と宣言の違いは何だ? . . . それを次セクションで考えよう。


6: 定義と宣言の違い


Hypothesizer 7
定義とは、オブジェクト的オブジェクトを生成するもののことだ。

「オブジェクト的オブジェクト」? . . . えーと、私がそう言ったのは、それを、ソースファイル中にのみ存在するものと区別するためだ。

例えば、こう反駁する人がいるかもしれない、「どの宣言もオブジェクトだ、だって、ソースファイルのまさにここに存在しているじゃないか!」と。

はい、ソースファイル的には存在しています、しかし、それはランタイムに何のオブジェクトも生成しません。

しばしば、定義はメモリ領域をアロケートするものとして説明される。 . . . その説明は多分不正確でないが、私には確信がない。

他方で、宣言は何らのオブジェクト的オブジェクトも生成せず、何かがどこかに存在するという断言か青写真である。

例えば、'extern int g_integer'は、そのタイプおよびその名前のグローバル変数がどこか他の所に存在する、と断言している、それ自体がその変数を生成することなしに。

「青写真」とは何だ? . . . えーと、青写真は、それ自体では何らのオブジェクト的オブジェクトでもないが、それを使ってオブジェクト的オブジェクトたちを生成できるテンプレートである(私が「テンプレート」でなく「青写真」を使ったのは、C++固有の'テンプレート'との混同を避けるためである)。

実際には、私はクラス宣言のことを考えている: クラス宣言は、そのクラスがどこか他の所に存在しているという断言ではない(それはどこにも存在しない!)。

しかし、クラス宣言はクラスを生成するのではないのか(クラスをオブジェクト的オブジェクトとして生成する)?

いいえ、しません、そこが要点です。実際、もしも、そうしたら、それは問題でしょう、同一クラスの複数の重複物を複数オブジェクトファイル内に生成するすることになってしまって。

Javaクラスはオブジェクト的オブジェクトであり、それが、'ClassA.class'のようなことができる理由であり、それをC++ではできないでしょう?


7: クラスの実態


Hypothesizer 7
C++におけるクラスは、オブジェクトではなく青写真であり、それが意味するのは、クラスを宣言すること自体は何らのオブジェクトも生成せず、その青写真を用いてクラスインスタンスを生成する('new ClassA ()'のようなことをして)際に初めて、その青写真にしたがって当該クラスインスタンスが生成されるということだ。

したがって、クラスはオブジェクト的には存在しない、クラスインスタンスは存在するかもしれないが。

それが、クラス宣言をヘッダーファイルへ入れて、それらヘッダーファイルを複数のソースファイルへインクルードできる理由だ。

クラス非インラインメソッドはソースファイル内に定義しなければならないが、その理由は、それらは(本来の意味での)ファンクションであって、任意の(本来の意味での)ファンクションはオブジェクト的オブジェクトだからだ。

任意のインラインメソッドは定義されるものでなく宣言されるものであるが、その理由は、それは、オブジェクト的オブジェクトとして存在せず、まさに、メソッドコールに手を加えるための青写真であるからであり、それが、ヘッダーファイル内で実装されたメソッドが自動的にインラインになる理由だ。

スタティックフィールドがソースファイル内で定義されなければならない理由は、今や明らかだ: クラス宣言は何も生成せず、クラスインスタンス生成もそのスタティックフィールドを生成せず(クラスインスタンス生成はそのインスタンスについてのものだから)、そのスタティックフィールドはどこかで生成されなければならない。他方で、インスタンスフィールドは、クラスインスタンス生成時に生成される。


参考資料


<このシリーズの前の記事 | このシリーズの目次 | このシリーズの次の記事>