😺

【c++】テンプレートの基本解説1【初心者向け】

2021/06/16に公開

緒言

こんにちは。最近読んだc++の書籍で「c++テンプレートテクニック」という有名な本があるのですが、読み進めているうちにこのテンプレートという機能を使えばどんなことができるのか色々とコードを書いて動かしてみようという気になりました。ということで基本的なテンプレートの知識をお伝えした後に色々と面白いなと思った実装を紹介したいと思います。vscodeやvisual studio, wandboxなどを利用されている方は実際にコードを書いて動かしてみてください。

テンプレートとは

関数テンプレートについて

まずテンプレート自体所見の人は下記を見てください。

template <class T>
T add(T a, T b) {
    return a + b; // a + bの結果を返す
}

これは関数テンプレートと呼ばれています。関数がテンプレート機能を利用しているから関数テンプレートです。分かりやすいですね(?)

この関数が何をしているかというと、関数名addの通り、引数のaとbを足し合わせた結果を返すというただそれだけの関数です。通常はTという型の代わりにintやdouble, floatなどを使うことと思います。察しのいい方は想像つくかと思いますが、この関数はTという型に任意の型を入れて使うことが可能です。

例えば下記のようにユーザーがこのadd関数を使うときにint型を指定すれば、それはint型の引数a, bの和演算の結果を返す関数になるということです。下記を見てください。

int main() {
    int sum = add(1, 2);
    std::cout << "sum = " << sum << std::endl;
}

上記コードではaddというテンプレート関数に1と2というint型の定数を渡すことで下記の関数を呼んだことと等しい関数になります。

// int型の変数aとbを受け取り、足し合わせて返却する。
int add(int a, int b) {
    return a + b;
}

つまり、Tという任意の型を持つテンプレート関数addを宣言しておけば、int型, double型, float型, long型やその他色々な型に対応した関数を作成できるということです。

クラステンプレートについて

次にテンプレートは関数だけではなく「クラス」や「構造体」にもつけることができます。下記を見てください。

#include <vector>

template <class T>
class MyVector {
    std::vector<T> vec_;
public:
    // vec_の末尾に引数valueを挿入
    void push_back(const T& value) {
        vec_.push_back(value);
    }

    // vec_の中身を表示するprint関数
    void print() const { // 関数名の後にconstをつけるとメンバ変数を変更できないようになる
        for(int i=0; i<vec_.size(); i++) {
	    std::cout << vec_.at(i) << " ";
	}
	std::cout << endl;
    }
};

上記クラスはprivateメンバ変数としてstd::vector<T>型のvec_という動的配列を持っています。また、publicなメンバ関数としてpush_back関数とprint関数を持っています。push_back関数はconst T&型の引数valueを受け取ると、内部で保持しているvec_配列の末尾に挿入します。例えばint型の1という数字をvalueに渡せばvec_配列に1という数字が追加されます。

このクラスを使うときは下記のようにします。

int main() {
    MyVector<int> myVector;
    myVector.push_back(3); // 3を挿入
    myVector.push_back(1); // 1を挿入
    myVector.push_back(2); // 2を挿入

    myVector.print(); // 3 1 2と出力される
}

テンプレートクラスのインスタンスを作成するときはクラス名の後に<任意の型>をつけます。
こうすることで、Tという型にはint型が入り、MyVector<int>というユーザー定義型を持ったクラスをインスタンス化することができます。

MyVectorクラスの拡張

さてこのクラスは今はただstd::vectorをラップしただけでprint関数で手軽にvec_配列の中身を出力できる以外にほとんど使い道はないのであまり面白みがないかもしれません。そこで例えば、「値を挿入したら自動的に昇順に並び替えてくれる魔法の配列」SortedVectorを作ってみましょう。値を挿入するにはpush_back関数を使っているので、挿入した後に標準ライブラリが提供しているsort関数を使って並び替えを行えば実現できそうです。実装は下記の通り。

#include <iostream>
#include <vector>
#include <algorithm> // sort関数を使うのに必要

// 値を挿入すると自動的に並べ替えてくれる配列クラス
template <class T>
class SortedVector {
    std::vector<T> vec_;
public:
    // vec_の末尾に引数valueを挿入
    void push_back(const T& value) {
        vec_.push_back(value);
	// 挿入した後にソート(並び替え)する
	std::sort(vec_.begin(), vec_.end(), std::less<T>());
    }

    // vec_の中身を表示するprint関数
    void print() const { // 関数名の後にconstをつけるとメンバ変数を変更できないようになる
        for(int i=0; i<vec_.size(); i++) {
	    std::cout << vec_.at(i) << " ";
	}
	std::cout << std::endl;
    }
};

新たに下記sort関数が追加されました。これによってvec_配列に値を挿入するたびに中身の要素が昇順にソートされるようになりました。

std::sort(vec_.begin(), vec_.end(), std::less<T>());

第3引数のstd::less<T>()という部分が配列の並び替えルールが昇順であることを示しています。やや難しいのですが、第3引数にはstd::less<T>型を持つクラステンプレート内のメンバ関数operator()を渡しています。つまりソート対象のvec配列の並び替えルールは第3引数に渡す値によって変えることができるということです。例えばvec配列を昇順ではなく降順に並び替えたい場合はstd::greater<T>()を指定すると降順ソートを実現できます。その他にもユーザーが任意の並び方でvec配列を並び替えたい場合はその並び替えのルールを作って第3引数に渡せばよいということです。(後述)

std::sort(vec_.begin(), vec_.end(), std::greater<T>());

さて、ここで困ったことがあります。SortedVectorクラスのメンバ関数push_backは現時点では昇順にしか並び替えられません。もしSortedVecotrクラスの設計者であるあなたが、昇順で並び替えることができればそれでいいし、それは未来永劫変わることはないという事情があれば現状のままでも問題はありません。しかしもっと柔軟な設計ができるようになるためには降順に並べ替えることもできるクラスにしておくにこしたことはないはずです。しかし単にpush_back関数をオーバーロードするだけではこのような柔軟な要望に応えるクラスを設計することはできません。下記のような具合です。

~~~~~~
public:
    // 昇順用のpush_back関数
    void push_back(const T& value) {
        vec_.push_back(value);
    	std::sort(vec_.begin(), vec_.end(), std::less<T>());
    }

    // 降順用のpush_back関数
    void push_back(const T& value) {
        vec_.push_back(value);
    	std::sort(vec_.begin(), vec_.end(), std::greater<T>());
    }
~~~~~~

これは下記の通りコンパイルエラーになります。なぜならpush_back関数は戻り値の型も引数の型も同じだからです。ソースの中でpush_back関数を呼ぶときに戻り値も引数の型も同じpush_back関数があるとコンパイラはどちらを呼び出したらいいのかわからなくなるためです。

main.cpp:17:10: error: 'void SortedVector<T>::push_back(const T&)' cannot be overloaded with 'void SortedVector<T>::push_back(const T&)'
     void push_back(const T& value) {
          ^~~~~~~~~
main.cpp:11:10: note: previous declaration 'void SortedVector<T>::push_back(const T&)'
     void push_back(const T& value) {
          ^~~~~~~~~

これを解決するにはテンプレートのさらなる力を解放する必要があります。それが「テンプレートテンプレートクラス」です。ちなみにふざけているわけではありません。それを言うならc++の+も一つ余計じゃないかと叫びたくなります。

いきなり結論を言いますが下記のようなコードになります。

#include <iostream>
#include <vector>
#include <algorithm>

// 値を挿入すると自動的に並べ替えてくれる配列クラス
template <class T, template <class> class ComparePolicy> // テンプレートテンプレートクラス
class SortedVector {
    std::vector<T> vec_;
public:
    // 昇順用のpush_back関数
    void push_back(const T& value) {
        vec_.push_back(value);

        // ソートルールをテンプレートクラスに変更
    	std::sort(vec_.begin(), vec_.end(), ComparePolicy<T>());
    }

    // vec_の中身を表示するprint関数
    void print() const { // 関数名の後にconstをつけるとメンバ変数を変更できないようになる
        for(int i=0; i<vec_.size(); i++) {
	        std::cout << vec_.at(i) << " ";
    	}
	    std::cout << std::endl;
    }
};

// 昇順に並び替えるためのソートルールクラス
template<class T>
struct LessPolicy {
    // ()演算子のオーバーロード
    constexpr bool operator()(const T& lhs, const T& rhs) {
        return lhs < rhs;
    }
};

本当にややこしい構文なのですが写経したりすれば覚えられると思います。いくつかポイントを解説したいと思います。

まずテンプレートテンプレートクラスの宣言は下記のようにします

template <class T, template <class> class ComparePolicy>
class SortedVector {

templateキーワードの中にもう一度templateと書きます。ここでComparePolicyと書いた部分がソートルールに当たるクラスになります。ここに入れるためのクラスが下記LessPolicyクラスになります。

// 昇順に並び替えるためのソートルールクラス
template<class T>
struct LessPolicy {
    // ()演算子のオーバーロード
    constexpr bool operator()(const T& lhs, const T& rhs) {
        return lhs < rhs;
    }
};

operator()というのは演算子オーバーロードです。標準ライブラリのsort関数の第3引数に渡すソートルールですが、ソートするとき、内部でソート対象の配列の要素と要素を比較しています。この比較に使うのが上記で定義しているLessPolicy::operator()です。第1引数のlhsはleft hand sideの略で左辺値を表し、第2引数のrhsはright hand sideの略で右辺値を表しています。挙動としてはlhsがrhsよりも小さい場合、trueを返却し、lhsがrhs以上の場合はfalseを返すという処理になっています。つまりLessPolicy(1, 2)とすればtrueが返ってきますし、LessPolicy(3, 1)などとすればfalseが返ってくるようになるというわけです。SortedVectorクラスにこのソートルールを適用するには下記のように使用します。

int main() {
    SortedVector<int, LessPolicy> vec;
}

SortedVectorクラスのインスタンス化の際に2番目のテンプレートパラメータに上記で定義したLessPolicyを渡しています。これにより、SortedVectorクラスのテンプレートテンプレートパラメータComparePolicyにはLessPolicyが適用されることになります。つまりvec配列をソートしている処理にはLessPolicyが適用されるということです。

public:
    // 昇順用のpush_back関数
    void push_back(const T& value) {
        vec_.push_back(value);

	// ComparePolicyにLessPolicy<T>::operator()の処理が適用される
    	std::sort(vec_.begin(), vec_.end(), ComparePolicy<T>());
    }

実際にソートされているか確認する場合はprint関数を呼んでやればよいでしょう。

int main() {
    SortedVector<int, LessPolicy> vec;
    vec.push_back(3); // 挿入してsort
    vec.push_back(1); // 挿入してsort
    vec.push_back(2); // 挿入してsort
    vec.print(); // 1 2 3
}

これで要素を挿入するごとにsort関数が内部で走り、かならず要素が昇順に並ぶようになりました。めでたしめでたし。今回の記事はこれで終わりたいと思いますが、次回はこれをさらに応用してint型配列だけでなくユーザー定義の構造体やクラスなども並べ替えられるように改造します。

Discussion