😺

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

9 min read

緒言

こんにちは。最近読んだ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型やその他色々な型に対応した関数を作成できるということです。

add関数をちょっとだけデコレートして実戦で使えるようにする

業務で見かけるコードはこれにちょっとだけ手を入れて下記のように書きます。

template <class T>
T add(const T& a, const T& b) {
    return a + b;
}

これはテンプレートとは関係ありませんがadd関数の引数にconstキーワードが付いたのと参照を示すキーワード「&」がついています。こうすることで、addの引数a, bは値渡しではなく、参照渡しとなり関数呼び出し時の引数のコピーコストを削減することができます(int型単体の場合は参照渡しよりもコピーの方がよいそうですが)。参照渡しということはこのadd関数は万が一にでも引数a, bを変更した場合、関数呼び出し元のa, bの値も変わってしまう可能性があるということです。これを防ぐためにconstをつけることでadd関数内では引数a, bを変更できないように設定しています。関数の引数に値を設定するときに値渡しかポインタ渡しか参照渡しかという議論が度々あがっているようですが、筆者はよく参照渡しを使います。これは無駄なコピーが発生する値渡しよりも参照渡しの方が大体の場面で優れているからです。参照渡しをする場合はconstをつけるべきかよく考えましょう。その引数が絶対に変更されるべきではない場合はconstをつけ、変更したい場合はconstをつけるべきではありません。どちらかわからない場合はとりあえずconstをつけ、変更する必要に迫られた場合のみconstを外すくらいの心構えでよいかと思われます。

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

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

#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という数字が追加されます。

デフォルトアクセス修飾子について

ちなみにメンバ変数vec_にはアクセス修飾子(private, public, protectedなど)がついていませんが、クラスの場合、内部に宣言したメンバ変数あるいはメンバ関数は暗黙的にprivateになります。この理由として、そもそもクラスとはデータや関数を外部から隠ぺいするための機構なので、privateをつけ忘れて外部からメンバ変数やメンバ関数にアクセスできてしまう、なんていうユーザーの凡ミスを防ぐためです。これとは逆に構造体「struct」ではデフォルトのアクセス修飾子はpublicになっています。

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

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関数をオーバーロード(多重定義)しなければなりません。つまり昇順用のpush_back関数、降順用のpush_back関数、その他の並び替えルールを持ったpush_back関数といった具合です。これではコピーコードが増えて修正や拡張といった保守作業がやりづらくなります。実をいうと今回の場合はオーバーロードすることができませんが詳細については後述します。

さて、ここで困ったことがあります。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

ログインするとコメントできます