1. ホーム
  2. C++

C++テンプレートテンプレート使用法まとめ

2022-02-16 08:25:11

<スパン はじめに

テンプレートとは、プログラミング設計言語C++において、型を引数に用いて汎用的なプログラミングを支援することを指し、C++の標準ライブラリには、STLやIO Streamなど、ほとんどがテンプレートの考え方を取り入れた便利な関数が多数用意されています。

<スパン 関数テンプレート

c++入門では、以下のようなswap(int&, int&)のような関数に出会う人が多いでしょう。

void swap(int&a , int& b) {
    int temp = a;
    a = b;
    b = temp;
}

しかし、long、string、カスタムクラスのスワップ関数に対応する場合、上記のコードと似ていますが、種類が異なるので、スワップ関数のテンプレートを定義して、異なる種類のスワップ関数のコードを再利用するようにします。

template <class identifier> function_declaration;
template <typename identifier> function_declaration;

swap関数テンプレートを宣言し、定義するコードは以下のとおりです。

//method.h
template<typename T> void swap(T& t1, T& t2);

#include "method.cpp"

//method.cpp

template<typename T> void swap(T& t1, T& t2) {
    T tmpT;
    tmpT = t1;
    t1 = t2;
    t2 = tmpT;
}

以上がテンプレートの宣言と定義ですが、テンプレートのインスタンス化はどのように行われるのでしょうか?テンプレートのインスタンス化は、プログラマーではなく、コンパイラが行うものです。

//main.cpp
#include <stdio.h>
#include "method.h"
int main() {
    // template method 
    int num1 = 1, num2 = 2;
    swap<int>(num1, num2);
    printf("num1:%d, num2:%d\n", num1, num2);  
    return 0;
}

ここではswap関数を使用していますが、swapの定義が含まれていないとコンパイルがエラーになります。そのため、method.hファイルの最終行に#include "method.cpp"を追加する必要があります。


クラステンプレート

int型、long型、string型などをサポートできるスタックの簡単なクラスを書くとします。クラステンプレートを使用しない場合、3つ以上のスタッククラスを書かなければならず、コードは基本的に同じになります。クラステンプレートを使えば、単純なスタックテンプレートを定義し、それを必要に応じて int スタック、long スタック、string スタックとしてインスタンス化することができます。

//statck.h
template <class T> class Stack {
    public:
        Stack();
        ~Stack();
        void push(T t);
        T pop();
        bool isEmpty();
    private:
        T *m_pT;        
        int m_maxSize;
        int m_size;
};

#include "stack.cpp"

//stack.cpp
template <class T> Stack<T>::Stack(){
   m_maxSize = 100;      
   m_size = 0;
   m_pT = new T[m_maxSize];
}
template <class T> Stack<T>::~Stack() {
   delete [] m_pT ;
}
        
template <class T> void Stack<T>::push(T t) {
    m_size++;
    m_pT[m_size - 1] = t;
    
}
template <class T> T Stack<T>::pop() {
    T t = m_pT[m_size - 1];
    m_size --;
    return t;
}
template <class T> bool Stack<T>::isEmpty() {
    return m_size == 0;
}

上記では、クラステンプレートであるスタックを定義していますが、これは単純で、クラステンプレートの使い方を説明するためだけのもので、スタック上の要素は最大100個までしかサポートできません。

//main.cpp
#include <stdio.h>
#include "stack.h"
int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    
    while (!intStack.isEmpty()) {
        printf("num:%d\n", intStack.pop());
    }
    return 0;
}

<スパン テンプレート・パラメータ
テンプレートは,通常の型パラメータintの他に,以下のようなデフォルトのテンプレートパラメータを持つことができます.

template<class T, T def_val> class Stack{...}

上記のクラステンプレートのスタックは、最大100個の要素しかサポートできないという制限があります。このスタックの最大要素数は、テンプレートパラメータを使用して設定することができますが、そうでない場合は、デフォルトの最大値を100に設定します。

//statck.h
template <class T,int maxsize = 100> class Stack {
    public:
        Stack();
        ~Stack();
        void push(T t);
        T pop();
        bool isEmpty();
    private:
        T *m_pT;        
        int m_maxSize;
        int m_size;
};

#include "stack.cpp"

//stack.cpp
template <class T,int maxsize> Stack<T, maxsize>::Stack(){
   m_maxSize = maxsize;      
   m_size = 0;
   m_pT = new T[m_maxSize];
}
template <class T,int maxsize> Stack<T, maxsize>::~Stack() {
   delete [] m_pT ;
}
        
template <class T,int maxsize> void Stack<T, maxsize>::push(T t) {
    m_size++;
    m_pT[m_size - 1] = t;
    
}
template <class T,int maxsize> T Stack<T, maxsize>::pop() {
    T t = m_pT[m_size - 1];
    m_size --;
    return t;
}
template <class T,int maxsize> bool Stack<T, maxsize>::isEmpty() {
    return m_size == 0;
}

使用例は次のとおりです。

//main.cpp
#include <stdio.h>
#include "stack.h"
int main() {
    int maxsize = 1024;
    Stack<int,1024> intStack;
    for (int i = 0; i < maxsize; i++) {
        intStack.push(i);
    }
    while (!intStack.isEmpty()) {
        printf("num:%d\n", intStack.pop());
    }
    return 0;
}

<スパン テンプレート特化型

 あるテンプレートに対して別の実装を定義したい場合、テンプレートの特殊化を利用することができます。例えば、スタッククラスのテンプレートを定義すると、char*型のスタックであれば、charポインタを保持しているだけなので、charポインタが指すメモリは無効かもしれないし、スタックはスタック要素のcharポインタをポップアップして、既に無効かもしれないメモリを指すので、charのデータを全てスタッククラスにコピーできるようにしたいです。また、我々が定義したスワップ関数のテンプレートは、ベクトルまたはリストと他のコンテナ型は、コンテナが大きなオブジェクトを保存する場合、それは多くのメモリとパフォーマンスの低下を取るため、一時的に大きなオブジェクトを保存する生成するために、これらは解決するためのテンプレートの特殊化が必要です。

関数テンプレートの特化

  例えば、2つのベクトルに多数の要素<int>がある場合、本来のswap関数を使うと、tmpT = t1がt1の要素を全てコピーしなければならず、メモリを大量に消費してパフォーマンスが低下するので、我々のシステムでは、この問題をvector.swap関数で解決します、そのコードは以下の通りです。

//method.h
template<class T> void swap(T& t1, T& t2);

#include "method.cpp"

#include <vector>
using namespace std;
template<class T> void swap(T& t1, T& t2) {
    T tmpT;
    tmpT = t1;
    t1 = t2;
    t2 = tmpT;
}

template<> void swap(std::vector<int>& t1, std::vector<int>& t2) {
    t1.swap(t2);
}

template<>という接頭辞は、これがテンプレート・パラメータなしで記述される特殊化であることを示します。

//main.cpp
#include <stdio.h>
#include <vector>
#include <string>
#include "method.h"
int main() {
    using namespace std;
    // template method 
    string str1 = "1", str2 = "2";
    swap(str1, str2);
    printf("str1:%s, str2:%s\n", str1.c_str(), str2.c_str());  
    
    vector<int> v1, v2;
    v1.push_back(1);
    v2.push_back(2);
    swap(v1, v2);
    for (int i = 0; i < v1.size(); i++) {
        printf("v1[%d]:%d\n", i, v1[i]);
    }
    for (int i = 0; i < v2.size(); i++) {
        printf("v2[%d]:%d\n", i, v2[i]);
    }
    return 0;
}

vector<int> のスワップコードはまだかなり限定的なので、テンプレート特化ですべてのベクタースワップを解決したい場合、どうすればいいかというと、次のコードを入れるだけです。

template<> void swap(std::vector<int>& t1, std::vector<int>& t2) {
    t1.swap(t2);
}

Change to

template<class V> void swap(std::vector<V>& t1, std::vector<V>& t2) {
    t1.swap(t2);
}

//compare.h
template <class T>
 class compare
 {
  public:
  bool equal(T t1, T t2)
  {
       return t1 == t2;
  }
};

#include <iostream>
#include "compare.h"
 int main()
 {
  using namespace std;
  char str1[] = "Hello";
  char str2[] = "Hello";
  compare<int> c1;
  compare<char *> c2;   
  cout << c1.equal(1, 1) << endl; //compare two arguments of type int
  cout << c2.equal(str1, str2) << endl; //compare two arguments of type char *
  return 0;
 }

これだけです。残りのコードは変わりません。

クラステンプレートの特殊化

 以下の比較コードをご覧ください。

//compare.h
#include <string.h>
template <class T>
 class compare
 {
  public:
  bool equal(T t1, T t2)
  {
       return t1 == t2;
  }
};
   

template<>class compare<char *>  
{
public:
    bool equal(char* t1, char* t2)
    {
        return strcmp(t1, t2) == 0;
    }
};

//shape.h
class Shape {

};
class Circle : public Shape {
};

2つの整数を比較する場合、compareのequalメソッドは正しいのですが、compareのテンプレートパラメータがchar*の場合、このテンプレートは動作しないので、以下のように修正します。

//main.cpp
#include <stdio.h>
#include "stack.h"
#include "shape.h"
int main() {
    Stack<Circle*> pcircleStack;
    Stack<Shape*> pshapeStack;
    pcircleStack.push(new Circle);
    pshapeStack = pcircleStack;
    return 0;
}

main.cpp ファイルは変更されず、このコードは問題なく動作します。

テンプレート型変換

カスタムスタックテンプレートを思い出しながら、プログラムでShapeクラスとCircleクラスを以下のコードで定義したとします。

//statck.h
template <class T> class Stack {
    public:
        Stack();
        ~Stack();
        void push(T t);
        T pop();
        bool isEmpty();
        template<class T2> operator Stack<T2>();
    private:
        T *m_pT;        
        int m_maxSize;
        int m_size;
};

#include "stack.cpp"

そして、このような使い方ができるようにしたいのです。

template <class T> Stack<T>::Stack(){
   m_maxSize = 100;      
   m_size = 0;
   m_pT = new T[m_maxSize];
}
template <class T> Stack<T>::~Stack() {
   delete [] m_pT ;
}
        
template <class T> void Stack<T>::push(T t) {
    m_size++;
    m_pT[m_size - 1] = t;
    
}
template <class T> T Stack<T>::pop() {
    T t = m_pT[m_size - 1];
    m_size --;
    return t;
}
template <class T> bool Stack<T>::isEmpty() {
    return m_size == 0;
}

template <class T> template <class T2> Stack<T>::operator Stack<T2>() {
    Stack<T2> StackT2;
    for (int i = 0; i < m_size; i++) {
        StackT2.push((T2)m_pT[m_size - 1]);
    }
    return StackT2;
}

Stack<Shape*> は Stack<Circle*> の親ではないので、ここではコンパイルされません。しかし、そのように動作させたいので、変換演算子を定義しなければならず、Stackコードは次のようになります。

//main.cpp
#include <stdio.h>
#include "stack.h"
#include "shape.h"
int main() {
    Stack<Circle*> pcircleStack;
    Stack<Shape*> pshapeStack;
    pcircleStack.push(new Circle);
    pshapeStack = pcircleStack;
    return 0;
}

class Util {
    public:
        template <class T> bool equal(T t1, T t2) {
            return t1 == t2;
        }
};

int main() {
    Util util;
    int a = 1, b = 2;
    util.equal<int>(1, 2);
    return 0;
}

class Util {
    public:
         template <class T> static bool equal(T t1, T t2) {
            return t1 == t2;
        }
};

int main() {
    int a = 1, b = 2;
    Util::equal<int>(1, 2);
    return 0;
}

こうすることで、Stack<Circle>やStack<Circle*>をStack<Shape>やStack<Shape*>に自動的に変換できるのですが、変換の型が Stack<int> to Stack< Shape> だとコンパイラからエラーが報告されることに注意してください。

<スパン その他

以下のコードで、クラスがテンプレート引数を持たず、テンプレート引数を持つメンバー関数を持つことが可能です。

class Util {
    public:
        template <class T> bool equal(T t1, T t2) {
            return t1 == t2;
        }
};

int main() {
    Util util;
    int a = 1, b = 2;
    util.equal<int>(1, 2);
    return 0;
}

次のコードで、Utilのイコールを静的と宣言することもできます。

class Util {
    public:
         template <class T> static bool equal(T t1, T t2) {
            return t1 == t2;
        }
};

int main() {
    int a = 1, b = 2;
    Util::equal<int>(1, 2);
    return 0;
}