Edited at

初心者向け:C++のテンプレートの基本

More than 1 year has passed since last update.


想定読者

C++は初めてだが、プログラミング自体は初めてではない


この記事について

C++のテンプレートの基本を、よく遭遇しそうな状況を挙げながら説明していく記事です。


基本

以下のように、< >で囲まれた中にclass なにか(若しくはtypename なにか)やint なにかなどと書かれているものが、テンプレートを表します。クラス定義や関数定義の手前に付けることで、それがテンプレートに指定されたものを求められることを表します。

template <class TYPE>

class MyClass{
private:
TYPE value; // valueは実際には、TYPEに指定された型の変数になる
// ...
// ...
// ...
};
// こういうクラスが定義されていたら、

int main(){
// 例えばこのように使う
MyClass<int> foo; // fooのvalueはint型となる
MyClass<double> bar; // barのvalueはdouble型となる
}


テンプレートを使って書かれているライブラリを使う

C++では、標準ライブラリでもテンプレートを利用しているものが多数あります(STL(Standard Template Library、標準テンプレートライブラリ)と呼ばれます)。多くの方は、C++で最初にテンプレートに出会うのはSTLなのではないかと思ってます。

例えばstd::vectorクラス(可変長配列)のリファレンスを見ると、vector - cpprefjp C++日本語リファレンスでは

namespace std {

template <class T, class Allocator = allocator<T>>
class vector;
}

のように書いてありますから、std::vectorを使いたいときはstd::vector<なにか, なにか>と指定すればよいことがわかります。さらに2つ目については「= allocator<T>」という形でデフォルト値を定めているので、これは省略できます。実際、以下のように利用できます。

#include <vector>

#include <iostream>

int main(){
std::vector<int> v_int; // int値を格納するvector
std::vector<double> v_double; // double値を格納するvector
// ...
// ...
// ...
}


テンプレート付きのクラス・関数の簡単な例

#include <iostream>

#include <string>

// ----------
// テンプレート付きのクラスの例(構造体でも同様)
// ----------
template <class TYPE>
class MyClass{
private:
TYPE value;
public:
MyClass(const TYPE & val) : value(val) {}
void display_twice(){ // 2回表示する
std::cout << value << std::endl << value << std::endl;
}
};
// MyClass<double> とか MyClass<std::string> のように使う

// 複数のテンプレート引数を指定することもできる
template <size_t SIZE, class TYPE>
struct MyClass2{
TYPE values[SIZE];
};
// MyClass2<5, double> のように使う

// ----------
// テンプレート付きの関数の例
// ----------
template <class TYPE>
TYPE MyFunction(const TYPE & value1, const TYPE & value2){
return value1 + value2;
}
// MyFunction<double>(1.8, 2.357) のように使う。
// MyFunction(1.8, 2.357) のように書いてもよい(型が引数から判別できるなら)

int main(void){
MyClass<double> m0(12.34);
MyClass<std::string> m1("foo");
m0.display_twice(); // 12.34 を2回表示
m1.display_twice(); // foo を2回表示

MyClass2<5, double> m2;
std::cout << sizeof(m2) << std::endl; // doubleが8バイトの環境であれば40と表示

std::string x("foo"), y("bar");
std::cout << MyFunction(1.8, 2.357) << std::endl; // 4.157 と表示
std::cout << MyFunction(x, y) << std::endl; // foobar と表示
}


補足

最後のMyFunction(1.8, 2.357)MyFunction(x, y)の例では、+演算子の挙動がdoublestd::stringの場合で違いはするものの、それぞれの挙動(前者は数値の和、後者は文字列の連結)をしてくれています


  • コード中で呼ぼうとしている関数・メソッド(この場合は+演算子)が、テンプレートで指定したクラスを受け付けられるのであれば、どんなクラスも指定でき、それぞれのクラスによって異なる挙動を示す。

  • もし、そのクラスを受け付けられないのであれば、コンパイルエラーになる。例えば以下のコードを上記main関数内に入れるとコンパイルエラーになる。

    std::vector<int> v1 = {1, 3, 6};

std::vector<int> v2 = {2, 4, 5};
std::vector<int> v3 = MyFunction(v1, v2);
// vectorには+演算子が定義されていないため、コンパイルエラー。


テンプレートに指定させることができるものは?

テンプレートでは、


  • classと書いて(typenameと書いてもよい)、型を指定させるようにする

  • 型を書いて、その型の値を指定させるようにする

の二つが使えます。

型を書いて値を指定させるテンプレートでは、整数型は指定できますが、文字列定数や浮動小数点型は指定できません。(文字列の場合は一度static char []に代入したものは可。参考。浮動小数点数についてはやや複雑になりますが、対応策はあります。例1例2。)

関数ポインタ等も所定の要件を満たせば指定できます。ここにもっと詳しい解説があります。【C++】非型テンプレート・パラメータ【用途別の使い方】 | MaryCore


例:テンプレートの付いてないクラスをテンプレート対応にする

例として、二次元座標上の点を表すクラスを考えます。座標値はdouble型で表現しています。また、このクラスのメソッドには、「別の点を引数として与えると、二つの点の中点を計算して返す」というものがあるとします。

#include <iostream>


class Point{
private:
double x_, y_;
public:
// コンストラクタ
Point(double x, double y) : x_(x), y_(y) {}
// ゲッタ
double x() const{ return x_; }
double y() const{ return y_; }
// 中点の計算
Point midpoint(const Point & other) const{
return Point((x_ + other.x_) / 2, (y_ + other.y_) / 2);
}
};

int main(void){
Point p1(1.0, 2.0), p2(4.0, 7.0);
Point mid = p1.midpoint(p2);
std::cout << "Midpoint: " << mid.x() << ", " << mid.y() << std::endl;
// 2.5, 4.5 が答えとなる
return 0;
}

※コードを実際に動かしてみる → https://wandbox.org/permlink/8h3SZOQ7JSu2oNB0

その後、これを整数や分数など別の数値のクラスにも適用したくなったとします。この場合、クラスにtemplate <class TYPE>を付け、doubleTYPEで置き換えればよいわけです。

#include <iostream>


template <class TYPE> class Point{
private:
TYPE x_, y_;
public:
// コンストラクタ
Point(const TYPE & x, const TYPE & y) : x_(x), y_(y) {}
// ゲッタ
const TYPE & x() const{ return x_; }
const TYPE & y() const{ return y_; }
// 中点の計算
Point midpoint(const Point & other) const{
return Point((x_ + other.x_) / 2, (y_ + other.y_) / 2);
}
};

int main(void){
// doubleを与える場合
Point<double> p1(1.0, 2.0), p2(4.0, 7.0);
Point<double> mid12 = p1.midpoint(p2);
std::cout << "Midpoint: " << mid12.x() << ", " << mid12.y() << std::endl;

// intを与える場合
Point<int> p3(123, 456), p4(78, 9);
Point<int> mid34 = p3.midpoint(p4);
std::cout << "Midpoint: " << mid34.x() << ", " << mid34.y() << std::endl;

return 0;
}

※コードを実際に動かしてみる → https://wandbox.org/permlink/viLkit5eSAZVpJ8x

なお、コンストラクタの引数やゲッタの返り値を、単なる型(単にTYPEと指定する)ではなくconst TYPE &にしたのは、TYPEがメモリ量を多く使う型であった場合に、無用な値のコピーが行われないようにしたためです。(メンバ変数やmidpoint関数の部分については、実際にメモリを確保する必要がある場所なので、const TYPE &ではなくTYPEで置き換えています。)templateclassと書いて型を指定させる場合、どのような型が入るかわからないということを考慮して対応を行うのがよいです。