C++編で扱っている C++ は 2003年に登場した C++03
という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14
-> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++
の学習を始めるのなら、C++11
よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい
C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。
この章の概要です。
テンプレートでは、テンプレート仮引数に当てはめる具体的な型に応じて、普通のクラスや関数をインスタンス化します。こうして生成されるクラスや関数のことを、特殊化と呼びます。
ところで、あるクラステンプレートの設計を考えたとき、そのテンプレート仮引数に当てはめられる型の種類によっては、実装上、都合が悪いケースがあります。よくあるのは、次のような処理です。
template <typename T>
class DataStore {
:
pubilc
// 無関係のメンバは省略
inline bool operator==(const DataStore& rhs) const
{
return mValue == rhs.mValue;
}
private:
;
T mValue};
DataStore<>::operator== は、メンバ変数 mValue の値を ==演算子を使って比較します。基本的に間違っていませんが、もし、テンプレート仮引数 T の具体的な型が const char*型のように、C言語の文字列表現だったらどうでしょうか? この場合、std::strcmp関数によって比較される方が適切かもしれません。
この例のように、テンプレート仮引数の具体的な型によって、処理内容を変更したいことがあります。これを可能にするための仕組みとして、完全特殊化(明示的特殊化)や部分特殊化が使えます。
完全特殊化や部分特殊化は、テンプレート仮引数に特定の型が当てはめられたときにだけ使われる、特別版を定義する機能です。
それではまず、完全特殊化の例を見ていきましょう。クラステンプレートの場合と、関数テンプレートの場合とがあるので、それぞれ個別に解説します。
先ほどの DataStoreクラステンプレートの例を解決してみます。
#include <iostream>
#include <cstring>
template <typename T>
class DataStore {
public:
explicit DataStore(const T& value) :
(value)
mValue{}
inline bool operator==(const DataStore& rhs) const
{
return mValue == rhs.mValue;
}
private:
;
T mValue};
template <>
class DataStore<const char*> {
public:
explicit DataStore(const char* value) :
(value)
mValue{}
inline bool operator==(const DataStore& rhs) const
{
return std::strcmp(mValue, rhs.mValue) == 0;
}
private:
const char* mValue;
};
int main()
{
const char s1[] = "abc";
const char s2[] = "abc";
<const char*> ds1(s1);
DataStore<const char*> ds2(s2);
DataStore
std::cout << std::boolalpha
<< (ds1 == ds2)
<< std::endl;
}
実行結果:
true
完全特殊化を行う際には、まず、通常のテンプレートの定義を用意しておきます。これはこれまでの章とまったく同じように作ればいいです。こうして用意したテンプレートのことを、1次テンプレート(プライマリテンプレート)と呼びます。
完全特殊化のためのクラス定義を別途用意します。これは、1次テンプレートが見える位置に書きます。
1次テンプレートと同じ名前のクラスを定義しますが、このとき冒頭に「template <>」を付けることで、完全特殊化を行っていることを表す必要があります。また、クラス名の後ろに、テンプレート仮引数に当てはめられる具体的な型名を指定します。これを記述することで、この型がテンプレート仮引数に当てはめられたときにだけ、この完全特殊化されたクラスが使用されるようになります。
あとは、1次テンプレートと同じ意味合いの内容になるように、メンバを書いていきます。感覚として意識して欲しいのは、1次テンプレート側と違い、完全特殊化のためのクラスは、具体的な型を使って記述されるという点です。テンプレート仮引数に当てはめられる型は、すでに決定しているのですから、T のようなテンプレート仮引数は登場しません。
テンプレートを使用する側は、特に何も意識する必要がありません。テンプレート実引数に const char*型を指定した場合にだけ、完全特殊化された DataStoreクラスが使用され、それ以外の型を指定した場合には、DataStoreクラステンプレートをその型を使ってインスタンス化したクラスが使用されます。
実行結果を見ると分かるように、内容は同一だがメモリアドレスが異なる2つの文字列の比較で、true という結果になっています。これは、==演算子ではなく std::strcmp関数が使用されているからです。試しに、完全特殊化のコードだけをコメントアウトして試すと、結果は false に変わります。
今度は、関数テンプレートの完全特殊化の例を挙げます。
#include <iostream>
#include <string>
#include <cstring>
template <typename T>
inline std::size_t Length(const T* str)
{
return str->length();
}
template <>
inline std::size_t Length<char>(const char* str)
{
return std::strlen(str);
}
int main()
{
const std::string s1("aaa");
const char s2[] = "xxxx";
std::cout << Length(&s1) << "\n"
<< Length(s2) << std::endl;
}
実行結果:
3
4
このサンプルプログラムは、文字列の長さを調べる Length関数テンプレートを定義しています。実引数に指定したポインタ経由で、lengthメンバ関数を呼び出して結果を得ようとしていますが、const char*型で表現された文字列の場合には、当然ながらメンバ関数がありません。そこで、完全特殊化を利用して、const char*型の場合にだけ、std::strlen関数を使わせようとしています。
関数テンプレートの場合の完全特殊化は、クラステンプレートの場合とほぼ同様で、1次テンプレートとなる関数テンプレートを定義し、これが見える位置に、完全特殊化のための関数を定義します。関数名を同じにすることや、冒頭に「template <>」が必要である点も、クラステンプレートの場合と同様です。
完全特殊化のための定義の方には、関数名の後ろに、テンプレート仮引数に当てはめる具体的な型名を指定します。これも、クラステンプレートの場合と同様ではありますが、関数テンプレートの場合は、実引数から型を推測しますから(第9章)、それによってコンパイラが判断可能であれば省略できます。省略する場合は、次のようになります。
template <>
inline std::size_t Length(const char* str)
{
return std::strlen(str);
}
省略しなかった場合の指定は「<const char*>」ではなくて「<char>」であることに注目してください。1次テンプレートの方の仮引数の型は「const T*」ですが、ここに const char*型の実引数を与えると、テンプレート仮引数 T は「char」と判断されます。「const」も「*」もすでに T の外側に付いていますから、T に当てはめられる型はあくまで「char」なのです。したがって、完全特殊化の側のテンプレート実引数は「char」が正解です。
ところで、Length関数テンプレートの仮引数がポインタなのが少し気にならないでしょうか? 恐らく、参照の方がシンプルで安全だと思われます。しかし、仮に以下のように参照に直すと、コンパイルが通らなくなります(呼び出し側も修正したとしても)。
template <typename T>
inline std::size_t Length(const T& str)
{
return str.length();
}
template <>
inline std::size_t Length<char>(const char* str)
{
return std::strlen(str);
}
これは、完全特殊化の方ではポインタが使われているため、引数の型が一致しないためです。完全特殊化はあくまでも、テンプレート仮引数の部分を具体化するだけですから、それ以外の部分が変わってはいけません。
このケースでは、完全特殊化ではなくて、単に関数をオーバーロードすればいいでしょう。
template <typename T>
inline std::size_t Length(const T& str)
{
return str.length();
}
inline std::size_t Length(const char* str)
{
return std::strlen(str);
}
これなら問題ありません。関数テンプレートと通常の関数があり、どちらにも適合するときには、通常の関数の方が優先される(第9章)ので、きちんと動作します。
少し分かりづらいのですが、部分特殊化にはいくつかのパターンがありますが、どれも一言で言えば、「テンプレート仮引数の一部だけを特殊化する」ということです。
テンプレート仮引数のうちの一部分だけしか特殊化されないので、言い換えると、特殊化されていないテンプレート仮引数が残っているということです。そのため、完全特殊化する場合と違い、部分特殊化によって作られるものはクラスではなく、クラステンプレートのままです。
1つ目のパターンは、テンプレート仮引数の型の具体性を高めるような特殊化で、たとえば、T型のテンプレート仮引数に対して、T*型で特殊化するというものです。この場合、指定されたテンプレート実引数がポインタだった場合にだけ、特殊化されるということになります。
2つ目のパターンは、2個以上あるテンプレート仮引数のうちの1個以上に関して、具体的な型を想定するもので、たとえば、T型と U型のテンプレート仮引数に対して、T は何でも構わないが、U が double のときには特殊化するというものです。
3つ目のパターンは、テンプレート仮引数の個数自体を変えてしまうもので、たとえば、T型のテンプレート仮引数に対して、T[N]型で特殊化するとき、N を新たなノンタイプテンプレート仮引数(第22章)に追加できます。
なお、部分特殊化は、クラステンプレートにのみ可能であり、関数テンプレートに対しては行えません。関数テンプレートの場合の一部は、オーバーロードを使うことで代替できます。
【上級】関数の戻り値の型を指定するためにテンプレート仮引数を使うような関数テンプレートの場合、オーバーロードでは引数の型や個数でしか区別を付けられないので、対応できません。この場合は、クラステンプレートを用意して、そちらで部分特殊化し、そのメンバ関数を呼ぶように実装すれば対応可能です。
それでは、部分特殊化の例を見ていきましょう。
まず、1つ目のパターンの例として、T型を T*型で部分特殊化します。
#include <iostream>
// 1次テンプレート
template <typename T>
class DataStore {
public:
explicit DataStore(const T& value) :
(value)
mValue{}
void Print() const;
private:
;
T mValue};
template <typename T>
void DataStore<T>::Print() const
{
std::cout << mValue << std::endl;
}
// 部分特殊化
template <typename T>
class DataStore<T*> {
public:
explicit DataStore(T* value) :
(value)
mValue{}
void Print() const;
private:
* mValue;
T};
template <typename T>
void DataStore<T*>::Print() const
{
std::cout << *mValue << std::endl;
}
int main()
{
int num = 20;
<int> ds1(10);
DataStore<int*> ds2(&num);
DataStore
.Print();
ds1.Print();
ds2}
実行結果:
10
20
DataStore<>::Printメンバ関数は、保持している値を出力しますが、ポインタ型を保持しているときには、指し示す先の値を出力します。
前述したとおり、部分特殊化が作り出すのはクラステンプレートなので、classキーワードの手前の「template <typename T>」はそのまま必要です。「template <>」だと、完全特殊化を意味するので注意してください。
次の例は、2つあるテンプレート仮引数のうちの1つだけを特殊化するものです。
#include <iostream>
#include <cassert>
// 1次テンプレート
template <typename T, std::size_t CAPACITY>
class Stack {
public:
static const std::size_t CAPACITY = CAPACITY;
public:
();
Stack~Stack();
void Push(const T& data);
void Pop();
inline const T Top() const
{
return mData[mSP - 1];
}
inline std::size_t GetSize() const
{
return mSP;
}
private:
[CAPACITY];
T mDataint mSP;
};
template <typename T, std::size_t CAPACITY>
<T, CAPACITY>::Stack() :
Stack(0)
mSP{
}
template <typename T, std::size_t CAPACITY>
<T, CAPACITY>::~Stack()
Stack{
}
template <typename T, std::size_t CAPACITY>
void Stack<T, CAPACITY>::Push(const T& data)
{
assert(static_cast<std::size_t>(mSP) < CAPACITY);
[mSP] = data;
mData++;
mSP}
template <typename T, std::size_t CAPACITY>
void Stack<T, CAPACITY>::Pop()
{
assert(mSP > 0);
--;
mSP}
// 部分特殊化
template <std::size_t CAPACITY>
class Stack<bool, CAPACITY> {
public:
static const std::size_t CAPACITY = CAPACITY;
public:
();
Stack~Stack();
void Push(const bool& data);
void Pop();
inline const bool Top() const
{
return (mData[GetDataIndex(mSP - 1)] & (1 << GetDataBit(mSP - 1))) != 0;
}
inline std::size_t GetSize() const
{
return mSP;
}
private:
typedef unsigned int data_t;
static const unsigned int DATA_BITS = sizeof(data_t) * 8;
static const std::size_t DATA_ARRAY_SIZE = (CAPACITY / DATA_BITS) + 1;
inline std::size_t GetDataIndex(int sp) const
{
return sp / DATA_BITS;
}
inline std::size_t GetDataBit(int sp) const
{
return sp % DATA_BITS;
}
private:
data_t mData[DATA_ARRAY_SIZE];
int mSP;
};
template <std::size_t CAPACITY>
<bool, CAPACITY>::Stack() :
Stack(0)
mSP{
}
template <std::size_t CAPACITY>
<bool, CAPACITY>::~Stack()
Stack{
}
template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Push(const bool& data)
{
assert(static_cast<std::size_t>(mSP) < CAPACITY);
if (data) {
[GetDataIndex(mSP)] |= (1 << GetDataBit(mSP));
mData}
else {
[GetDataIndex(mSP)] &= ~(1 << GetDataBit(mSP));
mData}
++;
mSP}
template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Pop()
{
assert(mSP > 0);
--;
mSP}
int main()
{
static const int SIZE = 5;
typedef Stack<int, SIZE> IntStack;
typedef Stack<bool, SIZE> BoolStack;
;
IntStack iStack;
BoolStack bStack
for (std::size_t i = 0; i < SIZE; ++i) {
.Push(static_cast<int>(i));
iStack.Push((i & 1) ? true : false);
bStack}
for (std::size_t i = 0; i < SIZE; ++i) {
std::cout << iStack.Top() << std::endl;
.Pop();
iStackstd::cout << std::boolalpha << bStack.Top() << std::endl;
.Pop();
bStack}
}
実行結果:
4
false
3
true
2
false
1
true
0
false
要素数の上限が固定化された Stackクラステンプレートです。
クラステンプレートは2つあり、1つは要素の型、もう1つは要素数の上限値です。ここで、要素の型を表すテンプレート仮引数を bool型と指定した場合に、内部配列の管理を工夫して、1ビットに 1要素の情報を持つように特殊化しています。
標準ライブラリの vector が、vector<bool> の場合にしていることと、考え方は同じです(【標準ライブラリ】第5章)。
次の例は、テンプレート仮引数の個数が変わるパターンです。
#include <iostream>
// 1次テンプレート
template <typename T>
class DataStoreArray {
public:
explicit DataStoreArray(std::size_t size) :
(new T[size])
mValueArray{}
~DataStoreArray()
{
delete [] mValueArray;
}
inline T operator[](std::size_t index) const
{
return mValueArray[index];
}
inline T& operator[](std::size_t index)
{
return mValueArray[index];
}
private:
* mValueArray;
T};
// 部分特殊化
template <typename T, std::size_t SIZE>
class DataStoreArray<T[SIZE]> {
public:
()
DataStoreArray{}
~DataStoreArray()
{}
inline T operator[](std::size_t index) const
{
return mValueArray[index];
}
inline T& operator[](std::size_t index)
{
return mValueArray[index];
}
private:
[SIZE];
T mValueArray};
int main()
{
static const int SIZE = 5;
<int> iStoreArray(SIZE);
DataStoreArray<int[SIZE]> iStoreArray2;
DataStoreArray
for (int i = 0; i < SIZE; ++i) {
[i] = i * 10;
iStoreArray[i] = i * 10;
iStoreArray2}
for (int i = 0; i < SIZE; ++i) {
std::cout << iStoreArray[i] << " " << iStoreArray2[i] << "\n";
}
std::cout << std::endl;
}
実行結果:
0 0
10 10
20 20
30 30
40 40
複数の要素を配列管理するクラステンプレートです。通常は、指定された型の要素を動的な配列を使って管理しますが、要素数が分かっている配列型を指定した場合には、静的な配列を使った実装を使用します。
標準ライブラリには、STL (Standard Template Library) と呼ばれる、テンプレートを活用した機能群があります。この章までに、関数テンプレートやクラステンプレートに関する基本的な知識を得られたので、STL を少しずつ理解していける段階に来たはずです。
まずは、コンテナ(STLコンテナ)について学んでみましょう。【標準ライブラリ】第4章に進み、概要を知ったら、第5章~第13章まで進めてください。
続けて、第14章に進み、イテレータという概念について理解してください。
なお、【標準ライブラリ】編では、ほぼ全機能を網羅するリファレンスのようなページ作りをしているので、すべてを理解するのではなく、ざっくりとした概要を学ぶ程度で良いでしょう。特に、【言語解説】編の本章までに解説されていない概念が登場することもあるので、その場合は、単に読み飛ばして良いです。
問題① 部分特殊化の1つ目のパターン「T型を T*型で部分特殊化する」において、ポインタであるかそうでないかによって、printメンバ関数の実装を変えられることを示しました。同様のことを、関数テンプレートで行うとすれば、どう実装しますか?
問題② 標準ライブラリに含まれる STLコンテナ、「vector」「list」「deque」「set」「map」は、それぞれどんなデータ構造を提供しますか?
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |