C++入門|クラステンプレート

クラスそのものを型に合わせて変化させる。クラステンプレートは、どんな修行データにも対応できる万能設計図です。

前回は、関数テンプレートを使って、型が違っても同じような処理を1つにまとめる方法を学びました。

関数テンプレートでは、たとえばunite(x, y)のように書くことで、Tにintを入れれば数値の合計、Tにstringを入れれば文字列の連結として使えました。つまり、関数の中で使う型をあとから決められるようにしたわけです。

今回学ぶクラステンプレートは、その考え方をクラス全体に広げたものです。

関数だけではなく、メンバ変数の型、メンバ関数の引数の型、戻り値の型まで、まとめてテンプレート化できます。

ドラゴンボール風にたとえるなら、関数テンプレートは「1つの技だけを型に合わせて変える仕組み」でした。一方、クラステンプレートは「修行装置そのものを、入れるデータの種類に合わせて作り替える仕組み」です。

数値の修行ポイントを扱うなら、数値用の修行装置になります。
技名の文字列を扱うなら、技名連結用の修行装置になります。

同じクラスの設計図を使いながら、int用にもstring用にも変化できる。
これがクラステンプレートの大きな特徴です。

学習する内容役割
クラステンプレートクラス全体を型に合わせて使い分ける
templateクラス内でTという型を使えるようにする
KiMixerint型のデータを扱うクラスとして使う
KiMixerstring型のデータを扱うクラスとして使う
inline短い関数を呼び出し元へ展開しやすくする
constメンバ関数がインスタンスの状態を変更しないことを示す

クラステンプレートとは何か

クラステンプレートとは、クラスの中で使う型をあとから指定できるようにしたものです。

普通のクラスでは、メンバ変数の型をあらかじめ決めておきます。

たとえば、修行ポイントを扱うクラスなら、メンバ変数をint型にするかもしれません。

int m_n1;
int m_n2;

この場合、そのクラスはint型専用になります。

一方、技名を扱うクラスなら、メンバ変数をstring型にしたくなります。

string m_n1;
string m_n2;

この場合は、string型専用になります。

しかし、処理の流れが同じなら、型ごとに別々のクラスを作るのは少しもったいないです。2つの値を保存し、それらを+でまとめて返す、という考え方はintでもstringでも共通しています。

そこで、型をTとしておき、使うときにTをintやstringに置き換えられるようにします。

template<typename T>
class KiMixer {
private:
    T m_n1;
    T m_n2;
};

このように書くと、KiMixerは特定の型専用のクラスではなく、型をあとから指定できるクラスになります。

図:クラステンプレートは型に合わせて変化する設計図

この図が示していること

この図では、1つのクラステンプレートKiMixerが、int型用にもstring型用にも変化できることを表しています。

KiMixerとして使えば、Tがintになり、修行ポイントの合計を扱うクラスになります。
KiMixerとして使えば、Tがstringになり、技名の連結を扱うクラスになります。

同じクラスの設計図を使いながら、型に応じて別のクラスのように使えることが分かります。

今回作成するサンプルプログラム

今回のサンプルでは、KiMixerというクラステンプレートを作ります。

KiMixerは、2つの値を保存し、その2つを+でまとめるクラスです。

Tがintなら、2つの数値を足し合わせます。
Tがstringなら、2つの文字列を連結します。

型指定クラスのイメージaddの結果
KiMixerint型の値を2つ保存するクラス数値の合計
KiMixerstring型の値を2つ保存するクラス文字列の連結

今回のプログラムでは、テンプレートクラスの定義をヘッダーファイルに書きます。

通常のクラスでは、クラスの宣言を.hに書き、メンバ関数の定義を.cppに書く形をよく使います。
しかし、クラステンプレートでは、テンプレートの定義をコンパイラが使える状態にしておく必要があるため、基本的にヘッダー内に実装まで書きます。

このサンプルでも、KiMixerクラスはki_mixer.hにまとめて書きます。

KiMixerクラスの定義

プロジェクト/ファイル名: Chap6_03/ki_mixer.h

#ifndef _KI_MIXER_H_
#define _KI_MIXER_H_

// クラステンプレート
template<typename T>
class KiMixer {
private:
    // 1つ目の修行データ
    T m_n1;

    // 2つ目の修行データ
    T m_n2;

public:
    // 2つの値を設定する
    inline void set(const T n1, const T n2) {
        m_n1 = n1;
        m_n2 = n2;
    }

    // 2つの値をまとめた結果を返す
    inline T add() const {
        return m_n1 + m_n2;
    }
};

#endif // _KI_MIXER_H_

このヘッダーファイルでは、KiMixerというクラステンプレートを定義しています。

最初に注目したいのは、次の部分です。

template<typename T>
class KiMixer {

templateは、このクラスの中でTという型を使うことを示しています。

そのため、クラス内のメンバ変数にもTを使えます。

T m_n1;
T m_n2;

Tは、KiMixerを使うときに指定される型に置き換わります。

たとえば、KiMixerとして使うと、m_n1とm_n2はint型になります。
KiMixerとして使うと、m_n1とm_n2はstring型になります。

main.cppでクラステンプレートを使う

次に、main.cppでKiMixerを使います。

プロジェクト/ファイル名: Chap6_03/main.cpp

#include <iostream>
#include <string>
#include "ki_mixer.h"

using namespace std;

int main(int argc, char** argv) {
    // int型を扱う修行データミキサー
    KiMixer<int> powerMixer;

    // string型を扱う技名ミキサー
    KiMixer<string> techniqueMixer;

    // 修行ポイントを設定する
    powerMixer.set(1, 2);

    // 技名を設定する
    techniqueMixer.set(string("かめ"), string("はめ波"));

    // それぞれの結果を表示する
    cout << powerMixer.add() << endl;
    cout << techniqueMixer.add() << endl;

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果

3
かめはめ波

このプログラムでは、同じKiMixerクラスを2種類の型で使っています。

KiMixer<int> powerMixer;
KiMixer<string> techniqueMixer;

powerMixerは、Tにintを指定したKiMixerです。
そのため、m_n1とm_n2はint型として扱われます。

techniqueMixerは、Tにstringを指定したKiMixerです。
そのため、m_n1とm_n2はstring型として扱われます。

同じKiMixerというクラスの設計図から、int用とstring用の2つの使い方ができているわけです。

KiMixerのイメージ

KiMixerと書くと、Tがintに置き換わります。

つまり、次のクラステンプレートは、

template<typename T>
class KiMixer {
private:
    T m_n1;
    T m_n2;

public:
    inline void set(const T n1, const T n2) {
        m_n1 = n1;
        m_n2 = n2;
    }

    inline T add() const {
        return m_n1 + m_n2;
    }
};

Tをintとして見ると、次のようなクラスのイメージになります。

class KiMixer_int {
private:
    int m_n1;
    int m_n2;

public:
    inline void set(const int n1, const int n2) {
        m_n1 = n1;
        m_n2 = n2;
    }

    inline int add() const {
        return m_n1 + m_n2;
    }
};

実際にこの名前のクラスを書くわけではありませんが、考え方としてはこのようにTがintに置き換わります。

そのため、次の処理では、1と2がint型の値として保存されます。

powerMixer.set(1, 2);

そして、addを呼び出すと、m_n1 + m_n2が計算されます。

cout << powerMixer.add() << endl;

結果は3になります。

KiMixerのイメージ

KiMixerと書くと、Tがstringに置き換わります。

そのため、メンバ変数m_n1とm_n2はstring型として扱われます。

イメージとしては、次のようなクラスになります。

class KiMixer_string {
private:
    string m_n1;
    string m_n2;

public:
    inline void set(const string n1, const string n2) {
        m_n1 = n1;
        m_n2 = n2;
    }

    inline string add() const {
        return m_n1 + m_n2;
    }
};

この場合、addの中で行われるm_n1 + m_n2は、数値の加算ではありません。
string型の+なので、文字列の連結になります。

main.cppでは、次のように値を設定しています。

techniqueMixer.set(string("かめ"), string("はめ波"));

そして、addを呼び出すと、2つの文字列が連結されます。

cout << techniqueMixer.add() << endl;

結果は、かめはめ波になります。

同じaddという名前でも、Tがintなら数値の合計になり、Tがstringなら文字列の連結になります。
ここに、クラステンプレートの面白さがあります。

図:Tがクラス全体の型を決める

この図が示していること

この図では、KiMixerのTが、クラス全体の型を決めていることを表しています。

Tがintになると、メンバ変数も引数も戻り値もintとして扱われます。
Tがstringになると、メンバ変数も引数も戻り値もstringとして扱われます。

関数テンプレートでは関数だけが型に合わせて変化しましたが、クラステンプレートではクラス全体が型に合わせて変化します。

クラステンプレートはヘッダーに書く

このサンプルでは、ki_mixer.cppを作っていません。

KiMixerの定義は、ki_mixer.hにまとめて書いています。

これは、クラステンプレートではとても大切なポイントです。

通常のクラスでは、ヘッダーにクラス宣言を書き、cppファイルにメンバ関数の定義を書くことがよくあります。

しかし、テンプレートでは、Tにどの型が入るかが、使われる場所で決まります。
そのため、コンパイラがKiMixerやKiMixerを作るときに、テンプレートの中身まで見える必要があります。

もしテンプレートの実装がcppファイル側に分かれていて、使う場所から見えない状態になると、必要な型に対するコードを生成できないことがあります。

そのため、クラステンプレートでは、基本的にヘッダー内に実装まで書きます。

通常のクラスクラステンプレート
宣言を.h、定義を.cppに分けることが多いヘッダー内に実装まで書くことが多い
型はあらかじめ決まっている型は使うときに決まる
cppで定義しても扱いやすいコンパイラがテンプレートの中身を見られる必要がある

クラステンプレートでは、ヘッダーが設計図であると同時に、型に応じたクラスを作るための材料にもなります。

ドラゴンボール風に言えば、KiMixerの設計図は、修行司令室からいつでも見える場所に置いておく必要があります。
int用の装置を作るときも、string用の装置を作るときも、その設計図が見えていなければ組み立てられないからです。

set関数の役割

KiMixerクラスには、set関数があります。

inline void set(const T n1, const T n2) {
    m_n1 = n1;
    m_n2 = n2;
}

set関数は、2つの値をメンバ変数に保存するための関数です。

Tがintなら、int型の値を2つ受け取ります。

powerMixer.set(1, 2);

Tがstringなら、string型の値を2つ受け取ります。

techniqueMixer.set(string("かめ"), string("はめ波"));

set関数では、引数n1とn2を受け取り、それぞれm_n1とm_n2に代入しています。

m_n1 = n1;
m_n2 = n2;

つまり、KiMixerは、まずsetで2つのデータを保存し、そのあとaddでその2つをまとめるクラスです。

修行世界でたとえるなら、setは修行装置に2つのエネルギーをセットする操作です。
int型なら修行ポイントをセットし、string型なら技名の前半と後半をセットします。

add関数の役割

add関数は、保存されている2つの値を+でまとめて返します。

inline T add() const {
    return m_n1 + m_n2;
}

戻り値の型はTです。

Tがintなら、addはintを返します。
Tがstringなら、addはstringを返します。

同じaddでも、Tに入る型によって意味が変わります。

Tの型m_n1m_n2addの結果
int123
stringかめはめ波かめはめ波

このように、クラステンプレートでは、同じメンバ関数でも、型によって動き方が変わります。

ただし、これは+演算子がその型で使える場合に限ります。
intやstringでは+が使えるので問題ありません。

もしTに+が使えない型を指定した場合、addの中のm_n1 + m_n2でコンパイルエラーになります。

テンプレートはどんな型でも必ず動くわけではありません。
その型に対して、クラス内で使っている操作が有効である必要があります。

const修飾子の意味

add関数の後ろには、constが付いています。

inline T add() const {
    return m_n1 + m_n2;
}

このconstは、add関数がメンバ変数の状態を変更しないことを表します。

addは、m_n1とm_n2を使って結果を返しますが、m_n1やm_n2の値を書き換える必要はありません。

そのため、関数の後ろにconstを付けて、「この関数はインスタンスの状態を変えません」と示しています。

関数メンバ変数を書き換えるかconstを付けるか
setm_n1とm_n2を書き換える付けない
addm_n1とm_n2を読むだけ付ける

setは値を設定する関数なので、メンバ変数を書き換えます。
そのため、setの後ろにはconstを付けません。

addは値を読むだけで、メンバ変数を書き換えません。
そのため、addの後ろにconstを付けるのが自然です。

ドラゴンボール風に言えば、setは修行装置にエネルギーを入れ替える操作です。
addは、今セットされているエネルギーを読み取って結果を表示するだけの操作です。

読み取るだけなら装置の状態は変わらないので、constを付けられるわけです。

inline修飾子の意味

今回のset関数とadd関数には、inlineが付いています。

inline void set(const T n1, const T n2) {
    m_n1 = n1;
    m_n2 = n2;
}
inline T add() const {
    return m_n1 + m_n2;
}

inline修飾子が付いた関数は、インライン関数と呼ばれます。

インライン関数は、コンパイル時に関数呼び出しの場所へ処理が展開されることを期待するための書き方です。

通常の関数では、関数を呼び出すときに、別の場所にある関数本体へ処理が移ります。
一方、インライン展開が行われると、関数の中身が呼び出し元へ埋め込まれるようなイメージになります。

小さくて短い関数では、関数呼び出しの手間が減り、処理が速くなることがあります。

ただし、inlineを付ければ必ず速くなるわけではありません。
実際にインライン展開するかどうかは、コンパイラが判断します。

また、長い処理にinlineを付けると、同じ処理が呼び出し元に何度も展開され、プログラムのサイズが大きくなることがあります。

関数の特徴inlineに向いているか
短い処理向いている
何度も呼ばれる小さな関数向いている場合がある
長い処理向いていない
複雑な処理効果が分かりにくい

今回のsetやaddは短い処理なので、inlineの例として分かりやすい形になっています。

図:inline関数は呼び出し元に処理が埋め込まれるイメージ

この図が示していること

この図では、通常の関数呼び出しとinline関数の違いを表しています。

通常の関数では、呼び出し元から関数本体へ処理が移るイメージです。
inline関数では、関数の処理が呼び出し元に埋め込まれるようなイメージになります。

ただし、inlineを付ければ必ずそうなるわけではなく、最終的にはコンパイラの判断に任されます。

inline関数を使うときの注意

inlineは便利ですが、何でも付ければよいわけではありません。

短い関数なら、呼び出しの手間を減らせる可能性があります。
しかし、処理が長い関数にinlineを付けると、呼び出し元のあちこちに処理が展開され、生成されるプログラムのサイズが大きくなることがあります。

ドラゴンボール風にたとえるなら、小さな気弾ならすぐにその場で出せますが、巨大な必殺技を毎回その場に埋め込むと、修行場全体が重くなってしまうようなものです。

inlineは、小さくて分かりやすい処理に使うのが基本です。

今回のsetやaddは処理が短いため、inlineの例として扱いやすい関数です。

クラステンプレートとポリモーフィズム

クラステンプレートも、広い意味ではポリモーフィズムの一種として考えられます。

これまで学んだポリモーフィズムには、オーバーロードやオーバーライドがありました。

オーバーロードは、同じ関数名を引数の違いで使い分ける仕組みでした。
オーバーライドは、親クラスの関数を子クラスで作り直す仕組みでした。

クラステンプレートは、それらとは少し違い、型をパラメータとして受け取り、型に応じてクラスの形を変えます。

種類内容
オーバーロード引数の型や数で関数を使い分けるadd()とadd(int, int)
オーバーライド子クラスで親クラスの関数を作り直すKameWarrior::shout()
クラステンプレート型を指定してクラスを作るKiMixer、KiMixer

クラステンプレートのように、型をパラメータとして使う多相性は、パラメータ多相と呼ばれることがあります。

難しい言葉に見えるかもしれませんが、考え方はシンプルです。
Tという型のパラメータを受け取り、Tにintを入れればint用、Tにstringを入れればstring用として使える、ということです。

ドラゴンボール風に言えば、同じ修行装置の設計図に対して、修行ポイント型、技名型、独自の戦士データ型など、どの型をセットするかで装置の姿が変わるようなものです。

クラステンプレートを使うメリット

クラステンプレートを使うと、型だけが違う似たクラスを何度も作らずに済みます。

たとえば、int型専用のIntMixer、string型専用のStringMixerを別々に作ることもできます。
しかし、それでは型が増えるたびにクラスが増えてしまいます。

KiMixerとして作っておけば、Tにintやstringを指定するだけで、それぞれの型に対応できます。

テンプレートを使わない場合テンプレートを使う場合
IntMixer、StringMixerなど型ごとにクラスを作るKiMixerを1つ作る
型が増えるとクラスも増える型を指定するだけで対応できる
共通処理の修正箇所が増えやすい共通処理を1か所で管理しやすい
コードが重複しやすい汎用性が高くなる

同じ構造を持つクラスを型ごとに作るのではなく、型を差し替えられるクラスとして作る。
これが、クラステンプレートの大きなメリットです。

クラステンプレートで押さえたい感覚

クラステンプレートでは、クラスの中に出てくる型をTとしてまとめます。

今回のKiMixerでは、メンバ変数、set関数の引数、add関数の戻り値にTを使いました。

T m_n1;
T m_n2;

inline void set(const T n1, const T n2)

inline T add() const

これにより、KiMixerならTがintになり、KiMixerならTがstringになります。

クラステンプレートを使うときは、「このクラスは型が違っても同じ構造で使えるか」を考えると理解しやすくなります。

今回のKiMixerは、2つの値を保存し、+でまとめて返すという構造を持っています。
この構造は、intでもstringでも共通して使えます。

だからこそ、クラステンプレートに向いています。

一方で、型によってまったく違う処理が必要な場合は、無理にクラステンプレートにまとめると分かりにくくなることもあります。
テンプレートは、共通化できる構造があるときに使うと効果的です。

C++のクラステンプレートは、関数テンプレートより少し大きな考え方になります。
関数だけでなく、クラス全体を型に合わせて変化させるからです。

ドラゴンボール風に言えば、KiMixerは万能修行装置の設計図です。
Tにintを入れれば修行ポイントを合計する装置になり、Tにstringを入れれば技名を連結する装置になります。

1つの設計図から、型に応じて複数のクラスのように使える。
それが、クラステンプレートの大きな力です。