typedef テンプレート
Robert Schmidt
Microsoft Corporation
2000 年 8 月 3 日
typedef(型定義)を使うと、特殊化されたクラス テンプレートなど、任意に定義した複雑な(名前なしも可)型の名前付きエイリアスを作成できます。たとえば、次のエイリアス
typedef std::vector<int> bag_of_int;
を使うと、次のようにコードを記述できます。
std::vector<int> x1; // vector of ints
bag_of_int x2; // ditto
ここで、
std::vector<int>
という型は、2 つのパラメータを持つテンプレートが特殊化された、引数を 1 つ持つテンプレートです。
template<typename T, typename Allocator = std::allocator<T>>
class vector
{
// ...
};
ただし第 2 パラメータは既定の引数に結合されています。したがって、特殊化されたテンプレートは
std::vector<int>
または
std::vector<int, std::allocator<int>>
のどちらの形式でも記述でき、どちらも同じ効果を持ちます。
プログラマのほとんどは第 2 パラメータを無視し、標準ライブラリの標準のアロケータを使用します。しかしこのコラムの熱心な読者ならば、独自の巧妙なアロケータを使いたいと思うでしょう。
typedef std::vector<int, my_allocator<int>> bag_of_int;
bag_of_int x; // OK, uses my_allocator
さらに、int の vector だけでなく、すべての型の vector に対して自分のアロケータを使うようにすることもできます。この考え方を抽象化する分かりやすい方法の 1 つは次のとおりです。
template<typename T>
typedef std::vector<T, my_allocator<T>> bag_of<T>;
bag_of<int> x;
この構文は、次の部分特殊化構文から導き出されたものです。
template<typename T>
class vector<T, my_allocator<T>>
{
// ...
}
実際、bag_of は部分的に特殊化された同じテンプレートを参照します。というよりは、typedef が許されているのならば、おそらく参照するでしょう。しかし悲しいことに、C++ の規格では typedef のテンプレートは許されていません。クラス テンプレートは OK、関数テンプレートも OK ですが、typedef テンプレートはだめです。
ここでは期待に応えてテンプレートの型定義の欠如を補ういくつかの解決方法を紹介します。気づいていると思いますが、どのソリューションも理想的とは言い難いものです。しかし、規格そのものでも STL(Standard Template Library)の内部でこれらのソリューションの 1 つが使われています。
マクロ
おそらく、もっとも分かりやすいソリューションは typedef をマクロに置き換える方法です。
#define
bag_of(T) std::vector<T, my_allocator<T>>
bag_of(int) x; // OK
この bag_of は、どのマクロにも適用される標準的かつ習慣的な制限が課されます。また、つぎはぎ構文も悩みの種であり、テンプレート名のように機能すると想定されている名前に、() が付いています。確かに bag_of(int) という表記は、一見オブジェクト生成または関数の呼び出しに見えます。
また、マクロ引数はテンプレート引数とは別な方法で解析されるため、次のような複雑な特殊化テンプレート
std::vector<X<int, long>, my_allocator<X<int, long>>
では、次のように記述されても、コンパイルできません。
bag_of(X<int, long>) // error
一方で、bag_of は簡潔で単刀直入だという利点があります。bag_of は層としてはもっとも薄く、std::vector のインターフェイスや実装に変更を加える必要がありません。
派生
構文を表記においても機能においても正しくするために、bag_of を std::vector から派生した本物のクラス テンプレートとして定義することができます。
template<typename T>
class bag_of : public std::vector<T, my_allocator<T>>
{
// ...
};
bag_of<int> x; // OK
bag_of は vector であるため、vector を必要とするほとんどすべて場所で使用できます。
template<typename T>
void inspect(std::vector<T, my_allocator<T>> const &);
bag_of<int> x;
inspect(x); // OK
仮に bag_of の動作を vector とまったく同じにしたいとするならば、23.2.4.1 節で要求されている 4 つの vector コンストラクタと同じ bag_of コンストラクタを次のように定義する必要があります。
template<typename T, typename Allocator = my_allocator<T>>
class bag_of : public std::vector<T, Allocator>
{
public:
bag_of(Allocator const & = Allocator());
bag_of(bag_of<T, Allocator> const &);
explicit bag_of(bag_of<T>::size_type, T const & = T(),
Allocator const & = Allocator());
template<class InputIterator>
bag_of(InputIterator, InputIterator,
Allocator const & = Allocator());
// ...
};
既定の引数によって、これら 4 つのコンストラクタは 8 個の実際のメンバ インターフェイスを提供しています。これらのインターフェイスを最大 8 個までの個別のオーバーロードとして実装できます。
template<typename T, typename Allocator = my_allocator<T>>
class bag_of : public std::vector<T, Allocator>
{
public:
bag_of();
bag_of(Allocator const &);
bag_of(bag_of<T, Allocator> const &);
explicit bag_of(bag_of<T>::size_type);
explicit bag_of(bag_of<T>::size_type, T const &);
explicit bag_of(bag_of<T>::size_type, T const &, Allocator const &);
template<class InputIterator>
bag_of(InputIterator, InputIterator);
template<class InputIterator>
bag_of(InputIterator, InputIterator, Allocator const &);
// ...
};
警告: 規格では、標準ライブラリのコンテナから派生が行えるという保証はありません。コンテナは基本クラスとなるよう設計されてはいません。virtual と宣言されているコンテナ メンバ、あるいはデストラクタはありません。したがって、コンテナをポリモーフィックとして扱うことはできません。
私の議論では、標準の Allocator を破棄して(その少しあとで typedef テンプレートを追加する)ために、継承をしようとしているものと仮定しています。これ以外の方法でコンテナを変更したい場合には、継承ではなく集成または包含を使ったほうがよいでしょう。
(場合によっては、このような派生の結果ほかの制限事項や必要条件が発生するかどうかについて、ライブラリのベンダに問い合わせる必要があるかもしれません)
typedef メンバ
bag_of のコンテキストの中では、Allocator はテンプレート引数 my_allocator<T> に結合されたテンプレート パラメータ名です。この結合の効果は私達が必要としている typedef テンプレートのそれと大変よく似ています。その場合の Allocator は my_allocator<T> へのエイリアスが付けられた "typedef" です。結果として、任意のメンバに対して
my_allocator<T>::member
という表現は、bag_of のスコープ内において
Allocator::member
という表現と等価です。
my_allocator<T> は自分自身の型を認識しており、その型を typedef としてエイリアス化でき、その typedef をメンバとして公開できます。
template<typename T>
class my_allocator
{
public:
typedef my_allocator<T> type;
// ...
};
typedef my_allocator<int> X;
X x;
X::type y; // y has type my_allocator<int>
このことは、再び bag_of のコンテキストの中においてですが、エイリアス名 Allocator もまた同じメンバ type を公開できることを暗に意味しています。さらに、bag_of から自身のインターフェイスの一部として Allocator::type へのエイリアスを公開できます。
template<typename T, typename Allocator = my_allocator<T>>
class bag_of : public std::vector<T, Allocator>
{
public:
typedef Allocator::type allocator_type;
// ...
};
typedef bag_of<int> X;
X x;
X::allocator_type y; // y has type my_allocator<int>
このように、my_allocator<T> が my_allocator<T> のためのエイリアスを公開します。my_allocator<T> から代わりに my_allocator<U> のためのエイリアス(U は任意の型)を公開するようにすれば、このパターンを拡張できます。つまり、T と U とを異なる型の引数に結合できるわけです。これには、入れ子のクラスの中で引数獲得のための同じしくみを使います。
template<typename T>
class my_allocator
{
public:
template<typename U>
class rebind
{
public:
typedef my_allocator<U> type;
};
// ...
};
typedef my_allocator<int> X;
X x;
X::rebind<long>::type; // y has type my_allocator<long>
新しいパターンを bag_of に適用すると、次のようになります。
template<typename T, typename Allocator = my_allocator<T>>
class bag_of : public std::vector<T, Allocator>
{
public:
template<typename U>
class rebind
{
public:
typedef Allocator::rebind<U>::type allocator_type;
};
// ...
};
typedef bag_of<int> X;
X x;
X::rebind<long>::allocator_type y; // y has type my_allocator<long>
typedef テンプレート
my_allocator::rebind および bag_of::rebind は、typedef テンプレートとほぼ同等の効果を持っています。これらは次のような手続きによって、既存の特殊化されたテンプレートから新しい特殊化されたテンプレートのファミリを作成できます。
特殊化されたテンプレートのテンプレート名と値型名の両方を獲得する。
それらの名前をもとに、新しい特殊化されたテンプレートを作成する。
作成した実体をメンバ型として公開する。
bag_of および my_allocator が人為的な例だと思われないように、Standard の 20.1.5/3 節を熟読するようお勧めします。この節では標準ライブラリ アロケータの必要条件について次のように説明されています。
上の表のテンプレート クラス メンバ rebind は実際のところテンプレート typedef である。名前 Allocator が SomeAllocator<T> に結合されているならば、Allocator::rebind<U>::other は SomeAllocator<U> と同じ型である。
今回私は my_allocator と type を使っていますが、標準規格では SomeAllocator と other を使っています。それ以外の面では、効果は同じです。同様にして、STL 準拠のアロケータはすべて次のものと等価なメンバを定義する必要があります。
template<typename U>
class rebind
{
public:
typedef Allocator<U> other;
}
my_allocator が STL 準拠であるとすれば、次の記述を許す必要があります。
typedef my_allocator<int> X;
X x;
x::rebind<long>::other y; // y is of type my_allocator<long>
なぜ規格ではアロケータのための typedef テンプレートが必要なのでしょう。それは、任意の T というコンテナについて、そのコンテナのアロケータが T 以外の型のオブジェクトを管理しなければならない場合があるためです。
たとえば、標準のコンテナである list<T, Allocator> は、ふつう一連の二重リンク付きのノードとして実装されます。この場合、各ノードには型 T の list 要素が 1 つ含まれます。Allocator は、list 要素(T)だけでなく、それらの要素を保持するためのノードも作成する必要があります。Allocator::rebind<T>::other のしくみを通じて Allocator を T からノードに再結合することで、list は同じ Allocator を使って両方の型のオブジェクトを管理できるようになります。
ところで、C++ 委員会のコア言語作業部会(Core-language Working Group、CWG)は、先ほど引用した節について現在も有効な不具合レポート(DR 103)を提出しています。
20.1.5 節の 3 段落目で、" テンプレート クラス メンバ rebind" を " メンバ クラス テンプレート rebind" に、" テンプレート typedef" を "typedef テンプレート " に、それぞれ変更のこと。
ベクトルへの応用
私の本来の目的は std::vector を typedef テンプレートのように機能させることでした。vector を上のパターンに従って定義すると、次のようになります。
template<typename T, typename Allocator = std::allocator<T>>
class vector
{
public:
template<typename U>
class rebind
{
public:
typedef vector<U, Allocator> other;
}
// ...
};
typedef std::vector<int> X;
X x;
X::rebind<long>::other y; // y is of type std::vector<long>
このコードは正しく動作しないかもしれません。これは、規格において、vector やほかのコンテナが rebind のしくみをサポートすることを要求していないためです(繰り返しますが、このしくみを必要とするのはアロケータだけです)。コンテナに対して rebind をこのように定義している標準ライブラリの実装があるかどうか、実際のところ私にはわかりません。コンテナで rebind をサポートするようにしたい場合は、おそらく既に私がテンプレート bag_of の実装で行ったような方法で、独自に派生させる必要があるでしょう。
おわりに
今回の話題は C++ のニュースグループで最大の話題になったものです。彼らの FAQ になぜ現れなかったのか、ちょっと驚いています。
C++ 委員会がなぜ typedef テンプレートを言語に直接追加しなかったのか、その理由を私はまったく知りませんが、おそらく次の 2 つではないかと思います。
堅牢性を保ちながら実現する方法を見出すことができなかった。
rebind<T>::other の方法で十分であると判断した(未完成の構文であるにもかかわらず)。
typedef テンプレートを使えば、特殊化されたテンプレートをその汎用のルート テンプレートに変換し、さらにその汎用テンプレートを再結合して新しい特殊化されたテンプレートを生成できるとます。これは、強力なテクニックなので、皆さんのテンプレート クラスでも実装するとよいのではないでしょうか。
Robert Schmidt は MSDN のテクニカル ライターです。このほかに彼が寄稿している雑誌に、C/C++ Users Journal があります。そこでは、彼は編集補助およびコラムニストとして活躍しています。これまでのキャリアで、ラジオ DJ、野生生物飼育係、天文学者、ビリヤード場管理者、探偵、新聞配達夫、そして大学講師を経験しています