Zenn
🖥️

【C++言語入門】 第22回 クラステンプレート

2025/03/23に公開

https://youtu.be/z1_03DXD510

四国めたん
四国めたん:\textcolor{pink}{四国めたん: }教師役ですわ

ずんだもん
ずんだもん:\textcolor{lime}{ずんだもん: }生徒役なのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} こんにちは。四国めたんです

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ずんだもんなのだ。こんにちはなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 前回は関数に対する テンプレート についてお話ししましたわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そうだったのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 今回はクラスに対する テンプレート についてお話ししますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 待っていたのだ

クラステンプレート

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 以前にCircleと云うクラスを使ってプログラム例を作成していましたわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} うむ

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
class Circle {
  double diameter_;  // 直径

 public:
  Circle(double diameter) : diameter_(diameter) {
  }
  virtual ~Circle() {
  }

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual double Area() const {
    double radius = Diameter() / 2.0;
    double a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} このクラスでは、"diameter_"メンバ変数など、ほとんどの変数や引数はdoubleで処理していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} その通りなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} これをintなどの他の数値型での処理をおこなうクラスを追加することを考えてみますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 別のクラスを作成するのは面倒なのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} はい、クラス名を変えたり、異なる名前空間に置いたりする必要がありますわね

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} クラス自体のオーバーロードというのがないので、しょうがないのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} また、作成した別クラスの内部でdoubleとしている部分をintなどに変える必要がありますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 大きなクラスでは、大変な手間がかかるのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} そこで、関数と同じようにクラスにも テンプレート を適用することで、これらの問題を解決できますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} はい、 テンプレート を使ったクラスの定義は以下の通りですわ

template <class テンプレート引数1, 型 テンプレート引数2, ...>
class クラス名 {
  メンバーなど
}

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 通常のクラス定義の前にtemplateキーワードに続けて山括弧"<>"内に テンプレート引数 をリストアップしているのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 関数の テンプレート と同じく、 テンプレート引数 の名前の前にclassキーワードを付加したものを 型引数 と言いますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ちなみに最近ではclassキーワードの代わりにtypenameキーワードを付加することも増えていますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 違いは何なのだ?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ほとんど違いはありませんので、どちらを使ってもOKですわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そうなのか

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} そして テンプレート引数 はクラス定義内の処理中で使うことができますわ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} たとえばメンバ変数の型や初期値の指定やメソッドの引数の型などに使用できますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} なお テンプレート引数 は複数指定することが可能ですわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} そしてクラスのインスタンスを作成する際に 実際の型実際の値 を指定しますわ

クラス名<実際の型, ..., 実際の値, ...> インスタンス名(コンストラクタの引数...);    // (1)
クラス名<実際の型, ..., 実際の値, ...>* ポインタ名 = new クラス名<実際の型, ..., 実際の値, ...>(コンストラクタの引数...);    // (2)

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} (1)は テンプレート で定義されたクラスのインスタンスを直接作成する場合なのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} はい、宣言時にクラス名に続けて山括弧"<>"内に 実際の型実際の値 をリストアップしますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} (2)は テンプレート で定義されたクラスのインスタンスをnewにより作成する場合なのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} はい、newの後のクラス名に続けて山括弧"<>"内に 実際の型実際の値 をリストアップしますわ

クラステンプレートを使ったCircleクラス

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} では実際にCircleクラスを テンプレート を使って書き換えてみましょう

#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
template <class T = double, int init = 10>
class Circle {
  T diameter_;  // 直径

 public:
  Circle(T diameter = init) : diameter_(diameter) {}
  virtual ~Circle() {}

  T Diameter() const { return diameter_; }
  void Diameter(T diameter) { diameter_ = diameter; }

  virtual T Area() const {
    T radius = Diameter() / 2;
    T a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} まず クラステンプレート として 型引数 に"T"を、 非型引数 に"init"を指定していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ふむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} なお、 非型引数 の型は、 関数テンプレート の場合と同様に 整数型ポインタ型参照 などの限られた型のみとなりますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} また、どちらにも= デフォルト値としてデフォルトの値を指定していますわね

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 関数の引数のデフォルト値の指定と同じなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} クラス定義部分では、変更前にdoubleとしていた部分を"T"で置き換えていますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} また、コンストラクタの引数には"init"をデフォルト値として指定していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} このように テンプレート引数 はクラス定義の様々なところで使用することができますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

クラステンプレートを使ったクラスオブジェクトの生成

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} では クラステンプレート を使ったCircleクラスのインスタンスを作って使ってみましょう

hello_world.cpp
#include <iostream>
#include "circle.h"

int main(int argc, char* argv[]) {
  Circle<> c(5.0);
  Circle<double, 3> d;
  Circle<int>* pc = new Circle<int>;
  std::cout << "Circle<> c(5.0);の円の面積は" << c.Area() << "です。"
            << std::endl;
  std::cout << "Circle<double, 3.0> d;の円の面積は" << d.Area() << "です。"
            << std::endl;
  std::cout << "Circle<int>* pc = new Circle<int>;の円の面積は" << pc->Area() << "です。"
            << std::endl;
  delete pc;
  pc = nullptr;
  return 0;
}

クラステンプレート

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} プログラムの説明をお願いするのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} わかりましたわ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} まず最初のCircle<> c(5.0);では テンプレート引数 の値として両方ともにデフォルトの値を使用していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ふむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ただしコンストラクタの引数として5.0を指定しているので、"diameter_"は5.0で初期化していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほど...

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ちなみに テンプレート引数 の全てにデフォルト値が設定されている場合でも、山括弧"<>"は省略できませんわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そのあたりは関数の テンプレート とは違うのだな

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} そうですわね

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 次にCircle<double, 3> d;では テンプレート引数 の値として両方の テンプレート引数 を指定していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 通常の使い方と云う訳だな

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} コンストラクタの引数は指定していませんので、 テンプレート引数 の"init"の値3が"diameter_"の初期値として使用されますわね

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 最後にCircle<int>* pc = new Circle<int>;では テンプレート引数 の"T"としてintを指定していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ふむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} テンプレート引数 の"init"については指定されていませんので、デフォルト値の10が使用されますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} なお、インスタンスの生成はnewでおこなっているのでポインタとなっていますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほど...

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ちなみに変数の型指定とnewの型指定については、 テンプレート引数 も含めて同じでなければなりませんわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} Circle<int>の部分なのだ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ところで結構ワーニングが出ているのだが...

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} T a = radius * radius * kPI;の部分ですわね

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} "kPI"はdoubleなのですが、"T"がintなので、型の不一致に関するワーニングですわね

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 問題ないのか?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 今回は無視しても問題ありませんわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

メソッドに対するテンプレート

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} テンプレート を使ったクラス内のメソッドに対しても テンプレート を使うことができますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} メソッドに対して テンプレート を使った定義はこの通りですわ

template<class テンプレート引数1, ..., 型 テンプレート引数2, ...>
戻り値の型 メソッド名(引数...) { 処理 };

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} 概ね 関数テンプレート と同じなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 戻り値の型引数の型処理 内に クラステンプレートテンプレート引数 も使えるところが異なっていますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} CircleクラスのAreaメソッドに対して使ってみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
template <class T = double, int init = 10>
class Circle {
  T diameter_;  // 直径

 public:
  Circle(T diameter = init) : diameter_(diameter) {}
  virtual ~Circle() {}

  T Diameter() const { return diameter_; }
  void Diameter(T diameter) { diameter_ = diameter; }

  template <class U>
  U Area() const {
    U radius = Diameter() / 2;
    U a = radius * radius * kPI;
    return a;
  }
};

#endif  // CIRCLE_H
hello_world.cpp
#include <iostream>

#include "circle.h"

int main(int argc, char* argv[]) {
  Circle<> c(5.0);
  Circle<double, 3> d;
  Circle<int>* pc = new Circle<int>;
  std::cout << "Circle<> c(5.0);の円の面積は" << c.Area<double>() << "です。"
            << std::endl;
  std::cout << "Circle<double, 3.0> d;の円の面積は" << d.Area<double>() << "です。"
            << std::endl;
  std::cout << "Circle<int>* pc = new Circle<int>;の円の面積は" << pc->Area<double>()
            << "です。" << std::endl;
  delete pc;
  pc = nullptr;
  return 0;
}

メソッドのテンプレート

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} Areaメソッドの 戻り値 と変数の型をメソッドの テンプレート引数 に置き換えているのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} そしてメイン関数でのAreaメソッドの呼び出しでは、山括弧"<>"内に 実際の型 を指定していますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} ところでメソッドの テンプレート ではvirtualを指定していないのだが...

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} はい、メソッドの テンプレート の仮想化は禁止ですわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

ソースファイルでのメソッドの定義

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} さて、これで クラステンプレート を使ってクラスの定義をおこなうことができましたわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} うむ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ただCircleクラスではArea`メソッドの定義をヘッダーファイルのクラス定義内でおこなっていますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} たしかに...

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 簡単なメソッドでは、これでもOKなのですが、複雑で大きなメソッドでは定義をソースファイルに書いた方が効率的ですわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} その通りなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} では、クラスの定義に クラステンプレート を使った場合でも、ソースファイルにメソッドの定義をおこなう方が良いのでしょうか?

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} わからないのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 基本的には テンプレート を使った場合には、メソッドの定義はヘッダーファイルでおこなうことをお勧めしますわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} なぜなのだ?

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} ソースファイルにメソッドの定義をおこなうと、コンパイルに必要な追加の情報が必要になるので、かなり面倒なのですわ

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} それならしかたがないのだ

ソースファイルでのメソッドの定義方法

テンプレート を使った場合にソースファイルでメソッドを定義する場合の形式は以下の通りです

template<class テンプレート引数1, 型 テンプレート引数2...>
戻り値の型 クラス名<テンプレート引数1, テンプレート引数2, ...>::メソッド名(引数...) { 処理 }

まず冒頭に クラステンプレートtemplate以下を書きます

次に通常のメソッドと同様に、"戻り値の型"に続けて"クラス名"と"メソッド名"をスコープ演算子"::"を挟んで書きます

なお"クラス名"は山括弧"<>"内に テンプレート引数 を列挙します

その際classなどは付加しないので注意が必要です

ちなみにヘッダーファイルのクラス定義内のメソッドは宣言のみとなります

とりあえずCircleクラスを書き換えてみます

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;

/// @brief 円
template <class T = double, int init = 10>
class Circle {
  T diameter_;  // 直径

 public:
  Circle(T diameter = init) : diameter_(diameter) {}
  virtual ~Circle() {}

  T Diameter() const { return diameter_; }
  void Diameter(T diameter) { diameter_ = diameter; }

  T Area() const;
};

#endif  // CIRCLE_H
circle.cpp
#include "circle.h"

template <class T, int init>
T Circle<T, init>::Area() const {
  T radius = Diameter() / 2;
  T a = radius * radius * kPI;
  return a;
}
hello_world.cpp
#include <iostream>

#include "circle.h"

int main(int argc, char* argv[]) {
  Circle<> c(5.0);
  Circle<double, 3> d;
  Circle<int>* pc = new Circle<int>;
  std::cout << "Circle<> c(5.0);の円の面積は" << c.Area() << "です。"
            << std::endl;
  std::cout << "Circle<double, 3.0> d;の円の面積は" << d.Area() << "です。"
            << std::endl;
  std::cout << "Circle<int>* pc = new Circle<int>;の円の面積は" << pc->Area()
            << "です。" << std::endl;
  delete pc;
  pc = nullptr;
  return 0;
}

未解決のシンボル

実行すると 未解決のシンボル というエラーが出てしまいます

これは"circle.cpp"をコンパイルする際にはmain関数でどのような形で呼ばれるかが判らないので、 テンプレート から作成されるメソッドが確定しないことが原因です

その結果、 未解決のシンボル と云うエラーとなってしまいます

ですので、 テンプレート を使ったメソッドをソースファイルで定義する場合には、具体的な テンプレート引数 を明示しておく必要があります

明示の方法は以下の通りです

template 型 クラス名<実際の型, ..., 実際の値, ...>::メソッド名(引数...);

"circle.cpp"を書き直してみます

circle.cpp
#include "circle.h"

template <class T, int init>
T Circle<T, init>::Area() const {
  T radius = Diameter() / 2;
  T a = radius * radius * kPI;
  return a;
}

template double Circle<double>::Area() const;
template double Circle<double, 3>::Area() const;
template int Circle<int>::Area() const;

メイン関数では3種類のインスタンスを生成してAreaメソッドを呼び出しているので、 テンプレート引数 の明示についても3種類必要です

テンプレート引数 を明示しておくのは意外に面倒なので、 クラステンプレート のメソッドの定義は、ヘッダーファイルのクラス定義内に書くか、少なくともヘッダーファイル内に書く方が得策です

まとめ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} お疲れ様でした

ずんだもん:\footnotesize \textcolor{lime}{ずんだもん:} おつかれさまなのだ

四国めたん:\footnotesize \textcolor{pink}{四国めたん:} 以上で クラステンプレート を終わりますわ

Discussion

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