2020年7月26日日曜日

9: C++テンプレート: インスタンス化、明示的または暗黙的、インライン化

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

「unresolved external symbol」エラー?「テンプレートクラスまたはファンクション」などというものはありません。インスタンス化とインライン化を区別しましょう。

話題


About: C++

この記事の目次


開始コンテキスト


  • 読者は、C++の基本的な知識を持っている、もしも、その、広く誤って説明されている一部の要素を正確に理解していないとしても。

ターゲットコンテキスト



  • 読者は、テンプレートとは何であるか、およびその使い方を理解する。

オリエンテーション


Hypothesizer 7
Javaが私の第一プログラミング言語だった(今もそうであるが)ので、早まって、私は、C++のテンプレートはJavaのジェネリクスのようなものだと思いこんでしまった。

しかしながら、C++テンプレートのメカニズムは、Javaジェネリクスのそれとは本質的に異なっており、そのメカニズムの正確な理解なしには、全く見当違いのことをやってしまうだろう。

C++テンプレートが本質的に何であるかの正確な理解を獲得した後でようやく、それを私は自由に使いこなせるようになるだろう。

本記事は、GCCに基づいており、Visual C++の特殊性には全く触れない。


本体


1: クラステンプレートは、クラスではなく、テンプレートであり、ファンクションテンプレートも、ファンクションではなく、テンプレートである


Hypothesizer 7
私は、以下のようなクラステンプレートを自分のプロジェクトに追加した。

'theBiasPlanet/tests/templatesTest1/ClassA.hpp'

@C++ ソースコード
#ifndef __theBiasPlanet_tests_templatesTest1_ClassA_hpp__
#define __theBiasPlanet_tests_templatesTest1_ClassA_hpp__

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> class ClassA {
				private:
					T i_memberVariableA0;
				public:
				    ClassA ();
				    template <typename U> ClassA (U a_argument0);
					T methodA0 ();
					template <typename U> U methodA1 (U a_argument0);
					template <typename U, typename ... V> U methodA2 (U a_argument0, V ... a_remainingArguments);
			};
		}
	}
}

#endif

'theBiasPlanet/tests/templatesTest1/ClassA.cpp'

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

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> ClassA <T>::ClassA () {
			}
			
			template <typename T> template <typename U> ClassA <T>::ClassA (U a_argument0) {
			}
			
			template <typename T> T ClassA <T>::methodA0 () {
				return i_memberVariableA0;
			}
			
			template <typename T> template <typename U> U ClassA <T>::methodA1 (U a_argument0) {
				return a_argument0;
			}
			
			template <typename T> template <typename U, typename ... V> U ClassA <T>::methodA2 (U a_argument0, V ... a_remainingArguments) {
				return a_argument0;
			}
		}
	}
}

「そのコードは問題なくコンパイルされた」、と私は思った、なぜなら、コンパイルエラーが全然なかったから。

そこで、私は、そのテンプレートを以下のように使用したクラスを追加した。

'theBiasPlanet/tests/templatesTest1/ClassB.hpp'

@C++ ソースコード
#ifndef __theBiasPlanet_tests_templatesTest1_ClassB_hpp__
#define __theBiasPlanet_tests_templatesTest1_ClassB_hpp__

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			class ClassB {
				public:
					int methodB0 ();
			};
		}
	}
}

#endif

'theBiasPlanet/tests/templatesTest1/ClassB.cpp'

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassB.hpp"
#include "theBiasPlanet/tests/templatesTest1/ClassA.hpp"

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			int ClassB::methodB0 () {
				ClassA <int> l_classA;
				return l_classA.methodA0 ();
			}
		}
	}
}

そのコードはリンクしない、エラーは、「undefined reference to `theBiasPlanet::tests::templatesTest1::ClassA<int>::ClassA()'」および「undefined reference to `theBiasPlanet::tests::templatesTest1::ClassA<int>::methodA0()'」...。

Javaジェネリクスとの類推で、そのテンプレートはオブジェクトファイルに存在しており、オブジェクトファイル内のそのテンプレートが、その利用物にリンクされるものと私は思いこんでいたのだが、そうではなかった。

判明したことは、クラステンプレートは、テンプレートクラスではなく、クラスを作成するのに使用できるテンプレートであって、作成されたクラスたちがオブジェクトファイルに格納されるのであって、そのテンプレートがではないといういことだ。

結局のところ、上記エラーは、'ClassA <int>'というクラスは存在しない、なぜなら、定義されていないから、ということを意味している。

私は、'テンプレートからクラス、ファンクション、等を定義すること'を、そのテンプレートの'インスタンス化'と呼ぶ。

それでは、 どうすれば、テンプレートをインスタンス化して'ClassA <int>'というクラスを定義できるのだろうか?...実のところ、明示的に行なう方法と暗黙に行なう方法とがある。

そのクラスを明示的に定義するために、私は、'theBiasPlanet/tests/templatesTest1/ClassA.cpp'を'theBiasPlanet/tests/templatesTest1/ClassA.tpp'に改名し(実際には、改名は必須ではないが、私はそうする、なぜなら、そのファイルはコンパイルされるものではなく、インクルードされるものであることが、明確になるから)、以下のようなソースファイルを作成する。

'theBiasPlanet/tests/templatesTest1/TemplatesInstantiator.cpp'

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassA.tpp"

using namespace ::theBiasPlanet::tests::templatesTest1;

template class ClassA <int>;

それだけ?...そう: これで、プロジェクトはリンクするはずだ。

そのクラスを暗黙に定義するには、私は、'theBiasPlanet/tests/templatesTest1/ClassA.cpp'を'theBiasPlanet/tests/templatesTest1/ClassA.tpp'に改名し(先程と同じく)、'ClassA <int>'を'theBiasPlanet/tests/templatesTest1/ClassB.cpp'(または任意のソースファイル)内で  普通に使用するのだが、その際、'theBiasPlanet/tests/templatesTest1/ClassB.cpp'(またはその任意のソースファイル)に'theBiasPlanet/tests/templatesTest1/ClassA.tpp'をインクルードさせる。

'tpp'ファイルをインクルードさせる必要があるのは、さもなければ、コンパイラは、クラスを定義するための完全な情報を知ることができないから。


2: どこでテンプレートをインスタンス化すべきか?


Hypothesizer 7
あるライブラリプロジェクトがあるテンプレートを持っていて、そのライブラリを使用する物たちは、いくつかの、そのテンプレートのインスタンス化された作成物(クラス群か、ファンクション群か、その両方)を使用するように想定されている。そのテンプレートはどこでインスタンス化すべきだろうか?...

1つのオプションは、使用物たちにそのテンプレートをインスタンス化させることで、それが意味するのは、そのライブラリのバイナリファイルには、インスタンス化された作成物もそのテンプレートそのものも格納されず、そのテンプレートは、'tpp'ファイルとして公開される、ということだ。

1つの良い点は、任意の使用物が自由にそのテンプレートをインスタンス化できることで、1つの悪い点(少なくとも、一部のケースにおいて)は、任意の使用物が自由にそのテンプレートをインスタンス化できることだ。...つまり、C++テンプレートではタイプパラメータ値に制約をかけられなので(Javaジェネリクスでは、'<T extends Comparable <T>>'のような指定(いわゆる、’バウンデッドタイプパラメータ’)によってそれができる)、そのテンプレートはある特定のタイプパラメータ値群のみでインスタンス化されると想定できない。

別の悪い点は、そのテンプレートの詳細が全ユーザーに露わにされることだ。つまり、全ての詳細を隠そうとは私は意図しないが、'tpp'内の、一部の情報か、一部のアルゴリズムか、その両方が、ユーザに露わにされるように意図されていないかもしれない。

別の悪い点は、任意のユーザーが'tpp'ファイルを変更して変更されたバージョンを使用することさえできてしまうということだ。つまり、もしも、そのユーザが自らのプログラムを作成しているのであれば、それは私の関心事ではないが、もしも、そのユーザが私が関係しているプロジェクトのメンバーだったら、それは問題だ。

別のオプションは、そのライブラリが、意図されるインスタンス化された作成物を格納し、'tpp'ファイルは、ユーザに公開しないことだ。

1つの良い点は、ユーザが自由に(みだりに)はそのテンプレートをインスタンス化できないことであり、1つの悪い点は、ユーザが自由にはそのテンプレートをインスタンス化できないことである。...つまり、もしも、許可するインスタンス化が容易に一覧化できるのであれば、そのライブラリがその全てのインスタンス化を行なえばよいだけだ、だが、常にそういうケースとは限らない。...もしも、そのライブラリがある特定のプロジェクト用のものであれば、それは、大きな問題ではない(または、全く問題でない)だろう、なぜなら、ユーザは、ライブラリ管理者に、あるインスタンス化を追加するようにただ頼めばよいだけだからだ、その一方、そのライブラリが一般大衆のためのものであれば、それは、致命的な問題だろう。

別の良い点は、予想できることだが、そのテンプレートの詳細がユーザに露わにされないことだ。

別の良い点は、これも予想できることだが、ユーザーが'tpp'ファイルを変更して変更されたバージョンを使うということが防げることだ。


3: テンプレートのインスタンス化は、明示的に行なうべきか、暗黙に行なうべきか?


Hypothesizer 7
テンプレートのインスタンス化を明示的に行なうべきか暗黙に行なうべきかは、テンプレートがどこでインスタンス化されるかによって完全に決定されるものではないが、テンプレートがどこでインスタンス化されるかからある程度の示唆を受ける。

実際、テンプレートがライブラリ内でインスタンス化される場合、明示的なインスタンス化がリーズナブルだ、なぜなら、何がインスタンス化された作成物であるかが明確になるからだ。しかし、暗黙のインスタンス化も可能ではある、ライブラリが、意図されたタイプパラメータ値群でテンプレートを使用するダミーソースファイルを含むことによって。

他方、テンプレートがライブラリの使用物によってインスタンス化される場合は、暗黙のインスタンス化は自然である、なぜなら、手間が省けるから。しかし、明示的なインスタンス化も可能である、各使用物が自身のモジュール内で明示的にそのテンプレートをインスタンス化することによって。

私は基本的に、ライブラリ内でテンプレートをインスタンス化することを好むので、テンプレートを明示的にインスタンス化することを好み、具体的にどのようにして明示的に任意のテンプレートをインスタンス化できるかを、次セクションで見よう。


4: 具体的にどのようにして明示的に任意のテンプレートをインスタンス化できるか


Hypothesizer 7
任意のクラステンプレートをインスタンス化する方法は既に上で学んだが、ファンクションテンプレート(クラスメソッドテンプレートを含む)もある。

任意のテンプレートを明示的にインスタンス化する方法を学ぼう。

第1に、復習として、任意のクラステンプレートを以下のようにして明示的にインスタンス化できる。

'theBiasPlanet/tests/templatesTest1/TemplatesInstantiator.cpp'

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassA.tpp"

using namespace ::theBiasPlanet::tests::templatesTest1;

template class ClassA <int>;

第2に、あるファンクションテンプレートを以下のようにして明示的にインスタンス化できる(先のコードに追加して)。

@C++ ソースコード
template double ClassA <int>::methodA1 <double> (double a_argument0);

それは容易だ、しかし、可変数引数群メソッドである'ClassA <T>::methodA2 <U a_argument0, V ... a_remainingArguments>'はどうだろうか?

実のところ、以下でできる。

@C++ ソースコード
template double ClassA <int>::methodA2 <double, float, char> (double a_argument0, float a_remainingArgument0, char a_remainingArgument1);

任意のクラスコンストラクターテンプレートは同様にインスタンス化できる(本記事はGCCに基づいている)。

@C++ ソースコード
template ClassA <int>::ClassA <double> (double a_argument0);


5: テンプレートのインスタンス化を、テンプレートが属するライブラリプロジェクト内でできないケース


Hypothesizer 7
ライブラリプロジェクト内にあるテンプレートのあるインスタンス化が、そのライブラリが認識しないパラメータタイプのためのものである場合、そのインスタンス化は、そのライブラリ内では行なえない。

例えば、2つのライブラリプロジェクト、'coreUtilities'(あらゆる種類のプロジェクトで共通に使用されると想定されるユーティリティコードを格納している)および'unoUtilities'(UNOプログラムプロジェクト群で共通に使用されると想定されるユーティリティコードを格納している)、を私は持っており、'coreUtilities'は、UNO特有の変数タイプを認識しない。'coreUtilities'はあるクラステンプレート、'<typename T, typename U, typename W = less <T>> ::theBiasPlanet::coreUtilities::collections::NavigableLinkedMap'、を持っているが、'coreUtilities'はクラス、'::theBiasPlanet::coreUtilities::collections::NavigableLinkedMap <::std::string, ::com::sun::star::beans::PropertyValue>'、を生成できない('::com::sun::star::beans::PropertyValue'は、UNOタイプ)。したがって、そのクラス、'::theBiasPlanet::coreUtilities::collections::NavigableLinkedMap <::std::string, ::com::sun::star::beans::PropertyValue>'、は、'unoUtilities'内で生成されなければならない、'tpp'ファイルを'unoUtilities'に公開して。


6: いわゆる「2段階ネームルックアップ」を原因とした謎めいたコンパイルエラーたち


Hypothesizer 7
以下のコードは、謎めいたコンパイルエラー、「there are no arguments to ‘methodC0’ that depend on a template parameter, so a declaration of ‘methodC0’ must be available [-fpermissive]」、を起こす...

'theBiasPlanet/tests/templatesTest1/ClassC.hpp'

@C++ ソースコード
#ifndef __theBiasPlanet_tests_templatesTest1_ClassC_hpp__
#define __theBiasPlanet_tests_templatesTest1_ClassC_hpp__

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> class ClassC {
				public:
		   			 int methodC0 (int a_argument0);
				};
		}
	}
}

#endif

'theBiasPlanet/tests/templatesTest1/ClassC.tpp'

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassC.hpp"

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> int ClassC <T>::methodC0 (int a_argument0) {
       		 	return a_argument0;
			}
		}
	}
}

'theBiasPlanet/tests/templatesTest1/ClassD.hpp'

@C++ ソースコード
#ifndef __theBiasPlanet_tests_templatesTest1_ClassD_hpp__
#define __theBiasPlanet_tests_templatesTest1_ClassD_hpp__

#include "theBiasPlanet/tests/templatesTest1/ClassC.hpp"

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> class ClassD : public ClassC <T> {
				public:
					int methodD0 (int a_argument0);
			};
		}
	}
}

#endif

'theBiasPlanet/tests/templatesTest1/ClassD.tpp'

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassD.hpp"

namespace theBiasPlanet {
	namespace tests {
		namespace templatesTest1 {
			template <typename T> int ClassD <T>::methodD0 (int a_argument0) {
				return methodC0 (a_argument0);
			}
		}
	}
}

'theBiasPlanet/tests/templatesTest1/TemplatesInstantiator.cpp'への追加

@C++ ソースコード
#include "theBiasPlanet/tests/templatesTest1/ClassD.tpp"

template class ClassD <int>;

そのメッセージをどのように理解するように私は想定されているのであろうか?...「‘methodC0’に、テンプレートパラメータに依存する引数がない」?そのとおり、しかし、「したがって、‘methodC0’の宣言にアクセスできなければならない」?...

第1に、「‘methodC0’に、テンプレートパラメータに依存する引数がな」かろうがあろうが、「‘methodC0’の宣言」は、アクセス可能でなければならないのではなかろうか?第2に、その宣言は存在しているとしか私には思えない、そのメソッドは問題なく宣言・定義されているのだから...

判明したのだが、'https://web.archive.org/web/20090212182538/http://www.redhat.com:80/docs/manuals/enterprise/RHEL-4-Manual/gcc/c---misunderstandings.html'(あるFirefoxアドオンがそのページに対して警告を発するので、ここでそのページをリンクするのは控える)というURLのページのセクション、'11.9.2.'、が説明をオファーしているが、それがまた謎めいている...

その説明が謎めいている1つの理由は、いくつかのことが読者に知られていると事前想定されていることだ、何の説明もなく。それらは常識なのだろうか?少なくとも、私は知らなかった...

第1の事前想定されている知識は、任意のテンプレートは、何らのインスタンス化にも関係なしに、前もって解析される、ということだ。その知識なしには、その説明全体が意味不明である。

第2の事前想定されている知識は、'タイプパラメータ値に依存しないコンテキスト'が何を意味しているかだ。'A::f <T>'の内部全体が 'T'というタイプに依存するコンテキストだ、と私は思っていた(だって、それは'T'でパラメータ化されたエリア内だから)し、「foo (1)」は依存コンテキスト内にいると思っていた、...しかし、そうではないようだ。「foo (1)」は、’T'に依存しないコンテキスト内にいるとみなされているようだ、ただ、その表現自体が'T'に関係していない(少なくとも、明示的には)という理由によって('明示的に'というのがキーポイントだ)。

第3の事前想定されている知識は、解析されるということと「looked into(中身を見られる)」ということの区別だ。解析されることは、「looked into」ということだ、と私は思っていた、だって、「looked into」されることなく、どのように解析されうるのだろうか?第1の事前知識により、'Base'テンプレートは解析されなければならないが、その説明は、「ベースクラスの中身を見ない、なぜなら、それは依存しているから」と主張している...。えーと、「looked into」というのは、'i'というシンボルの解決に、そのテンプレートの中身がルックアップされるという意味であるようだ。

結局、1つの解決策は、「return methodC0 ()」を「return this->methodC0 ()」に変更することであり、それが、'明示的に'というのがキーポイントである理由だ: 'methodC0'は初めから常に'this->methodC0'を意味してきたのだが、ただの'methodC0'は、明示性の欠如がゆえに、依存しているとはみなされない。


7: 結びとその先


Hypothesizer 7
これで、テンプレートとは本質的に何であるかおよびその使い方を理解した。

本記事はGCCに基づいているが、Visual C++はいくつかの特殊性を持ち込んでおり、それらは、将来の記事で論じられる(そこでは、テンプレートに関係しない特殊性も取り扱われるだろう)。


参考資料


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