こんにちは。田原です。
今回から応用講座をスタートします。案内にも記載したようにテンプレートを中心に解説していきます。
C++のテンプレートはびっくりするほど様々な機能を持っています。語り始めると長くなりすぎますので早速基本から解説します。
1.テンプレートの種類
テンプレートの種類は意外に沢山あります
関数型 | グローバル関数 | 関数テンプレート |
メンバ関数 | メンバ・テンプレート | |
クラス型 | クラス(他のクラスに含まれないクラス) | クラス・テンプレート |
他のクラスの中で定義されたクラス | メンバ・テンプレート | |
その他 | typedef(usingキーワードを使う) | エイリアス(別名)テンプレート |
さて、多くのテンプレート解説は関数テンプレートからの導入が多いです。しかし、関数テンプレートを使う時、多くは「型推論」と呼ばれる機能を一緒に使います。「型推論」は実はかなり高度な機能なのでイキナリ出てくると混乱しそうです。
そこで、今はまだ「型推論」が使えないクラス・テンプレートから解説し、その後で順次解説していきます。
2.テンプレートの重要な概念
テンプレートには様々な概念がありますが、中でも特に重要な概念をリストします。
暗黙的実体化 | コンパイラが必要なマシン語コードを生成すること |
明示的実体化 | |
明示的特殊化 | プログラマが特定の型に対して別定義すること |
部分的特殊化 |
更に上記を分類するための用語として「実体化」、「特殊化」があります。ただし、「部分的特殊化」は「特殊化」ではないなど、なかなか理解し辛いです。恐らくテンプレートの理解を阻む要因の1つになっていると思います。このややこし「特殊化」については後日解説したいと思います。
3.クラス・テンプレートの基本
まずは、基本的な機能から解説します。
3-1.サンプルの対象クラス
サンプルは、第34回目→第35回目→第36回目で使ったRAIIパターンのクラス Bar を使います。
今回の例でムーブは使いませんが、気持ち悪いのでムーブもフルに実装したバージョンです。また、この後の解説しやすいよう他の部分も少し修正しています。
#include <iostream> class Foo { int mData; public: Foo(int iData) : mData(iData) { } ~Foo() { } int data() const { return mData; } }; class Bar { Foo* mData; public: Bar() : mData(nullptr) { } Bar(Foo* iData) : mData(iData) { } ~Bar() { delete mData; } Foo* get() { return mData; } // ムーブ・コンストラクタ Bar(Bar&& iRhs) : mData(iRhs.mData) { iRhs.mData = nullptr; } // ムーブ代入演算子 Bar& operator=(Bar&& iRhs) { if (this != &iRhs) { delete mData; mData = iRhs.mData; iRhs.mData = nullptr; } return *this; } }; int main() { Bar bar(new Foo(123)); std::cout << "bar.get()->data()=" << bar.get()->data() << "\n"; }
さて、この Barクラスは Fooクラスのインスタンスを管理します。Barクラスが破棄される時は、管理していたFooクラスのインスタンスを自動的に解放します。便利ですね。ですので、このような機能は結構使いたい場合があります。しかし、上記のような定義しかできない場合、その管理したいクラスや型毎に異なるBarクラスを定義してないといけません。
中身は同じなのに一々定義するの嫌ですね。そして、機能を追加したくなった時、それらを全部同じように改造するの嫌ですよね。
3-2.マクロを使ってその問題に対処できないの?
実はマクロでできます。ちょっと長いですが、次のように記述するとDEFINE_UNIQUE(Foo);と書くこと(これはテンプレートの「明示的実体化」に近い)で、UniqueFooクラスが定義されます。
これを使えば、Fooクラスや他の型のインスタンスを管理するクラスを容易に生成できます。
#include <iostream> class Foo { int mData; public: Foo(int iData) : mData(iData) { } ~Foo() { } int data() const { return mData; } }; #define DEFINE_UNIQUE(dType) \ class Unique_##dType \ { \ dType* mData; \ public: \ Unique_##dType() : mData(nullptr) { } \ Unique_##dType(dType* iData) : mData(iData) \ { } \ ~Unique_##dType() { delete mData; } \ dType* get() { return mData; } \ \ /* ムーブ・コンストラクタ */ \ Unique_##dType(Unique_##dType&& iRhs) : mData(iRhs.mData)\ { \ iRhs.mData = nullptr; \ } \ /* ムーブ代入演算子 */ \ Unique_##dType& operator=(Unique_##dType&& iRhs) \ { \ if (this != &iRhs) \ { \ delete mData; \ mData = iRhs.mData; \ iRhs.mData = nullptr; \ } \ return *this; \ } \ } // 明示的実体化のようなもの DEFINE_UNIQUE(Foo); DEFINE_UNIQUE(double); int main() { Unique_Foo aUnique_Foo(new Foo(123)); Unique_double aUnique_double(new double(456.789)); std::cout << "aUnique_Foo.get()->data() = " << aUnique_Foo.get()->data() << "\n"; std::cout << "*aUnique_double.get() = " << *aUnique_double.get() << "\n"; }
これでも十分使えるのですが、マクロの制約で行の終わりに 継続行マーク ‘\’ を書かないといけないのでちょっと面倒ですね。C++言語を作った人は、当然マクロの文法を変更することもできた筈ですが、マクロではなくテンプレートを採用しました。何故なのでしょう?
真意は把握していませんが、マクロに比べるとテンプレートはかなり強力にできたからと思います。(テンプレートではできないことをマクロができることもありますが、その逆の方が遥かに多いです。)この例のように単純な場合はマクロでもできますが、今後解説するような複雑なことはとても手がでません。今回はその複雑な話に繋げるために基本的な機能を説明します。
3-3.クラス・テンプレートにする
上記のDEFINE_UNIQUEはパラメータとしてdTypeを受け取るようにしました。そして、下記操作をしました。
1. マクロ引数としてdTypeを指定
2. FooをdTypeへ書き換え
3. BarをUnique_##dTypeへ置き換え
1.と2.により、指定の型を管理するクラスが生成されるようになりました。
3.により、多重定義になると使えないため、dTypeごとに異なる名前になるよう管理したい型の名前にUnique_
というプリフィックスを付けたクラス名にしました。
これと同様なことをテンプレートでも行うため、次のように記述します。
1. class定義の前にテンプレート・パラメータ(上記のdTypeにあたるもの)を定義するため、template<typename tType>
と書く
2. FooをtTypeへ書き換え(マクロの場合と同じ)
3. BarをUniqueへ置き換え(クラス名に型名を含めなくて良い)
なお、template<typename tType>
の typename は次に書いているテンプレート・パラメータの tType が型名であることをしてしています。また、typename の変わりに class と書いても同じ意味になります。つまり、template<class tType>
は、template<typename tType>
と同じ意味です。
更に、テンプレート・パラメータは複数並べることもできます。例えば、template<typename tType, class tAllocator>
のように,で区切って記述します。
template<typename tType> class Unique { tType* mData; public: Unique() : mData(nullptr) { } Unique(tType* iData) : mData(iData) { } ~Unique() { delete mData; } tType* get() { return mData; } // ムーブ・コンストラクタ Unique(Unique&& iRhs) : mData(iRhs.mData) { iRhs.mData = nullptr; } // ムーブ代入演算子 Unique& operator=(Unique&& iRhs) { if (this != &iRhs) { delete mData; mData = iRhs.mData; iRhs.mData = nullptr; } return *this; } };
使う時は下記のように記述します。
int main() { Unique<Foo> aUnique_Foo(new Foo(123)); Unique<double> aUnique_double(new double(456.789)); std::cout << "aUnique_Foo.get()->data() = " << aUnique_Foo.get()->data() << "\n"; std::cout << "*aUnique_double.get() = " << *aUnique_double.get() << "\n"; }
使う時について、マクロとの相違点は以下の2点です。
- 実体化するための
DEFINE_UNIQUE(Foo);
やDEFINE_UNIQUE(double);
に対応する記述を省略可能
後日解説しますが、実体化するために明示的に記述することもできます。テンプレートでは「明示的実体化」と呼ばれ、ちょっと特殊な場合に使います。
多くの場合は、コンパイラのサポートにより必要に応じて自動的に実体化(暗黙的実体化)されるため、明示的な実体化を記述する必要はありません。今回のサンプルのUnique<Foo>
とUnique<double>
は「暗黙的実体化」されています。 -
クラス名は
Unique<Foo>
のように記述
マクロを定義する時は型名をクラス名に結合しましたが、テンプレートを定義する時はそれは不要でした。C++のコンパイラが直接解釈するので、クラス名に続けて<>
で囲んで型名を書けば、別のクラスとなり多重定義にならないのです。
4.まとめ
今回はテンプレートの一番基本的な部分について解説しました。一番基礎的な部分ですし、マクロでもほぼ同じものが書ける部分ですから、今回は特に難しくは無かったと思います。
この基本的なテンプレートは「プライマリー・テンプレート」と呼ばれます。「明示的特殊化」や「部分的特殊化」とセットになる用語です。そして、「明示的特殊化」や「部分的特殊化」により、テンプレートは非常に高度なことができます。しかし、それらの概念は結構ハードですので、まずは基本をしっかりと解説していきたいと思います。
ところで、入門講座の時よりボリュームが少ないです。応用講座はなかな難しい概念が多く、私自身もまとめるのに時間がかかりますし、皆さんも理解するがハードになると思いますので、応用講座は量少なめで進めます。
さて、次回は関数テンプレートの基礎を解説します。関数テンプレートの基本はクラス・テンプレートと大差ありませんが、「型推論」のお陰で使い勝手がたいへんよいです。(クラス・テンプレートの型推論が来たら、C++11から乗り換えるかも。) その基本部分も含めて解説します。お楽しみに。