テンプレート

[C++] テンプレート引数を複数持つ関数やクラスの書き方を解説

C++では、テンプレート引数を複数持つ関数やクラスを定義する際、テンプレート宣言にカンマで区切って複数の型や値を指定します。

例えば、template<typename T, typename U>のように記述します。

関数の場合、テンプレート引数を使って汎用的な処理を記述でき、クラスでは異なる型や値を扱う汎用的なデータ構造を作成できます。

テンプレート引数を複数持つ基本的な書き方

C++では、関数やクラスに複数のテンプレート引数を持たせることができます。

これにより、より柔軟で再利用可能なコードを書くことが可能になります。

以下に、複数のテンプレート引数を持つ関数とクラスの基本的な書き方を示します。

複数のテンプレート引数を持つ関数の例

以下のコードは、2つの異なる型の引数を受け取る関数の例です。

この関数は、引数の合計を計算して返します。

#include <iostream>
template <typename T1, typename T2>
auto add(T1 a, T2 b) -> decltype(a + b) {
    return a + b; // aとbの合計を返す
}
int main() {
    int x = 5;
    double y = 3.2;
    std::cout << "合計: " << add(x, y) << std::endl; // intとdoubleの合計
    return 0;
}
合計: 8.2

この例では、add関数は2つのテンプレート引数T1T2を持ち、異なる型の引数を受け取ることができます。

decltypeを使用して、戻り値の型を自動的に推論しています。

複数のテンプレート引数を持つクラスの例

次に、複数のテンプレート引数を持つクラスの例を示します。

このクラスは、2つの異なる型のデータを保持します。

#include <iostream>
template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {} // コンストラクタ
    void display() const { // データを表示するメソッド
        std::cout << "First: " << first_ << ", Second: " << second_ << std::endl;
    }
private:
    T1 first_;  // 最初のデータ
    T2 second_; // 2番目のデータ
};
int main() {
    Pair<int, std::string> myPair(1, "C++"); // intとstringのペア
    myPair.display(); // データを表示
    return 0;
}
First: 1, Second: C++

この例では、Pairクラスは2つのテンプレート引数T1T2を持ち、異なる型のデータを保持することができます。

コンストラクタで初期化し、displayメソッドでデータを表示します。

複数のテンプレート引数を使用することで、より汎用的で再利用可能なコードを書くことができるため、C++プログラミングにおいて非常に重要な技術です。

複数のテンプレート引数を持つ関数の実装

複数のテンプレート引数を持つ関数を実装することで、異なる型の引数を受け取る柔軟な関数を作成できます。

ここでは、具体的な実装例をいくつか紹介します。

1. 異なる型の引数を受け取る関数

以下の例では、整数と浮動小数点数の引数を受け取り、その合計を計算する関数を示します。

#include <iostream>
template <typename T1, typename T2>
auto sum(T1 a, T2 b) -> decltype(a + b) {
    return a + b; // aとbの合計を返す
}
int main() {
    int intValue = 10;
    double doubleValue = 5.5;
    std::cout << "合計: " << sum(intValue, doubleValue) << std::endl; // intとdoubleの合計
    return 0;
}
合計: 15.5

この関数sumは、異なる型の引数を受け取ることができ、decltypeを使用して戻り値の型を自動的に推論しています。

2. 複数の引数を持つ関数

次に、複数の引数を持つ関数の例を示します。

この関数は、3つの異なる型の引数を受け取り、それらの合計を計算します。

#include <iostream>
template <typename T1, typename T2, typename T3>
auto total(T1 a, T2 b, T3 c) -> decltype(a + b + c) {
    return a + b + c; // a, b, cの合計を返す
}
int main() {
    int x = 1;
    double y = 2.5;
    float z = 3.0f;
    std::cout << "合計: " << total(x, y, z) << std::endl; // int, double, floatの合計
    return 0;
}
合計: 6.5

このtotal関数は、3つの異なる型の引数を受け取り、それらの合計を計算します。

テンプレート引数を使うことで、さまざまな型の引数を受け取ることができます。

3. テンプレート引数のデフォルト値

テンプレート引数にはデフォルト値を設定することも可能です。

以下の例では、デフォルト値を持つテンプレート引数を使用しています。

#include <iostream>
template <typename T1, typename T2 = int>
auto multiply(T1 a, T2 b = 2) -> decltype(a * b) {
    return a * b; // aとbの積を返す
}
int main() {
    double value = 3.5;
    std::cout << "積: " << multiply(value) << std::endl; // doubleとデフォルトのintの積
    std::cout << "積: " << multiply(value, 4) << std::endl; // doubleとintの積
    return 0;
}
積: 7
積: 14

このmultiply関数は、2つ目の引数にデフォルト値を持ち、引数を1つだけ指定した場合には自動的に2が使用されます。

これにより、関数の呼び出しが簡単になります。

複数のテンプレート引数を持つ関数を実装することで、さまざまな型や数の引数を柔軟に扱うことができ、C++のプログラミングにおいて非常に強力な機能となります。

複数のテンプレート引数を持つクラスの実装

複数のテンプレート引数を持つクラスを実装することで、異なる型のデータを柔軟に扱うことができます。

ここでは、具体的な実装例をいくつか紹介します。

1. 異なる型のデータを保持するクラス

以下の例では、2つの異なる型のデータを保持するContainerクラスを示します。

このクラスは、データを設定し、表示するメソッドを持っています。

#include <iostream>
template <typename T1, typename T2>
class Container {
public:
    Container(T1 first, T2 second) : first_(first), second_(second) {} // コンストラクタ
    void display() const { // データを表示するメソッド
        std::cout << "First: " << first_ << ", Second: " << second_ << std::endl;
    }
private:
    T1 first_;  // 最初のデータ
    T2 second_; // 2番目のデータ
};
int main() {
    Container<int, std::string> myContainer(42, "C++"); // intとstringのコンテナ
    myContainer.display(); // データを表示
    return 0;
}
First: 42, Second: C++

このContainerクラスは、2つのテンプレート引数T1T2を持ち、異なる型のデータを保持します。

コンストラクタで初期化し、displayメソッドでデータを表示します。

2. 複数のテンプレート引数を持つスタッククラス

次に、複数のテンプレート引数を持つスタッククラスの例を示します。

このクラスは、異なる型のデータをスタックとして管理します。

#include <iostream>
#include <vector>
#include <stdexcept>
template <typename T, typename Container = std::vector<T>>
class Stack {
public:
    void push(const T& value) { // 値をスタックに追加
        container_.push_back(value);
    }
    void pop() { // スタックから値を削除
        if (container_.empty()) {
            throw std::out_of_range("スタックが空です。"); // 空のスタックから削除しようとした場合
        }
        container_.pop_back();
    }
    T top() const { // スタックの最上部の値を取得
        if (container_.empty()) {
            throw std::out_of_range("スタックが空です。"); // 空のスタックから取得しようとした場合
        }
        return container_.back();
    }
    bool empty() const { // スタックが空かどうかを確認
        return container_.empty();
    }
private:
    Container container_; // データを保持するコンテナ
};
int main() {
    Stack<int> intStack; // int型のスタック
    intStack.push(10);
    intStack.push(20);
    std::cout << "最上部の値: " << intStack.top() << std::endl; // 最上部の値を表示
    intStack.pop(); // 値を削除
    std::cout << "最上部の値: " << intStack.top() << std::endl; // 最上部の値を表示
    return 0;
}
最上部の値: 20
最上部の値: 10

このStackクラスは、テンプレート引数Tを持ち、デフォルトでstd::vectorを使用してデータを管理します。

pushpoptopemptyメソッドを通じて、スタックの基本的な操作を実装しています。

3. 複数の型を持つペアクラス

最後に、複数の型を持つペアクラスの例を示します。

このクラスは、2つの異なる型のデータをペアとして保持します。

#include <iostream>
template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {} // コンストラクタ
    void print() const { // ペアのデータを表示するメソッド
        std::cout << "First: " << first_ << ", Second: " << second_ << std::endl;
    }
private:
    T1 first_;  // 最初のデータ
    T2 second_; // 2番目のデータ
};
int main() {
    Pair<double, char> myPair(3.14, 'A'); // doubleとcharのペア
    myPair.print(); // データを表示
    return 0;
}
First: 3.14, Second: A

このPairクラスは、2つのテンプレート引数T1T2を持ち、異なる型のデータをペアとして保持します。

コンストラクタで初期化し、printメソッドでデータを表示します。

複数のテンプレート引数を持つクラスを実装することで、さまざまな型のデータを柔軟に扱うことができ、C++のプログラミングにおいて非常に強力な機能となります。

テンプレート引数の制約とSFINAE

C++では、テンプレート引数に対して制約を設けることができ、これにより特定の条件を満たす型のみを受け入れることが可能です。

SFINAE(Substitution Failure Is Not An Error)という技術を使用することで、テンプレートの特殊化や選択を行う際に、型の制約を柔軟に扱うことができます。

以下に、具体的な例を示します。

1. SFINAEの基本

SFINAEは、テンプレート引数の置換が失敗した場合でも、コンパイルエラーを発生させずに他のオーバーロードや特殊化を選択できるという特性です。

これにより、特定の条件を満たす型に対してのみテンプレートを適用することができます。

2. SFINAEを使用した関数の例

以下の例では、SFINAEを使用して、引数が整数型の場合のみ有効な関数を定義します。

std::enable_ifを使用して、条件を満たす場合にのみ関数が有効になります。

#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
process(T value) {
    return value * 2; // 整数型の場合、値を2倍にする
}
int main() {
    int intValue = 5;
    std::cout << "整数の処理結果: " << process(intValue) << std::endl; // 整数型の処理
    // double doubleValue = 3.5;
    // std::cout << "浮動小数点の処理結果: " << process(doubleValue) << std::endl; // コンパイルエラー
    return 0;
}
整数の処理結果: 10

この例では、process関数は整数型の引数に対してのみ有効であり、浮動小数点数を渡すとコンパイルエラーになります。

std::enable_ifを使用して、std::is_integral<T>::valuetrueの場合にのみ関数が有効になるようにしています。

3. SFINAEを使用したクラスの例

次に、SFINAEを使用して、特定の型に対してのみ有効なクラスを定義します。

以下の例では、浮動小数点型に対してのみ有効なクラスを示します。

#include <iostream>
#include <type_traits>
template <typename T, typename Enable = void>
class Calculator; // 前方宣言
template <typename T>
class Calculator<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
public:
    T add(T a, T b) {
        return a + b; // 浮動小数点型の場合、加算を行う
    }
};
int main() {
    Calculator<double> calc; // double型のCalculator
    std::cout << "加算結果: " << calc.add(3.5, 2.5) << std::endl; // 浮動小数点型の加算
    // Calculator<int> intCalc; // コンパイルエラー: int型には対応していない
    return 0;
}
加算結果: 6

この例では、Calculatorクラスは浮動小数点型に対してのみ有効であり、整数型を指定するとコンパイルエラーになります。

std::enable_ifを使用して、条件を満たす場合にのみクラスが定義されるようにしています。

SFINAEを使用することで、テンプレート引数に対する制約を柔軟に扱うことができ、特定の条件を満たす型に対してのみテンプレートを適用することが可能です。

これにより、より安全で再利用可能なコードを書くことができ、C++のプログラミングにおいて非常に強力な技術となります。

テンプレートの特殊化と部分特殊化

C++では、テンプレートの特殊化と部分特殊化を使用することで、特定の型に対して異なる実装を提供することができます。

これにより、より効率的で型に特化したコードを書くことが可能になります。

以下に、特殊化と部分特殊化の具体的な例を示します。

1. テンプレートの特殊化

テンプレートの特殊化は、特定の型に対して異なる実装を提供する方法です。

以下の例では、Calculatorクラスを整数型と浮動小数点型で特殊化しています。

#include <iostream>
template <typename T>
class Calculator {
public:
    T add(T a, T b) {
        return a + b; // デフォルトの加算
    }
};
// 整数型に対する特殊化
template <>
class Calculator<int> {
public:
    int add(int a, int b) {
        std::cout << "整数型の加算" << std::endl;
        return a + b; // 整数型の加算
    }
};
// 浮動小数点型に対する特殊化
template <>
class Calculator<double> {
public:
    double add(double a, double b) {
        std::cout << "浮動小数点型の加算" << std::endl;
        return a + b; // 浮動小数点型の加算
    }
};
int main() {
    Calculator<int> intCalc; // 整数型のCalculator
    std::cout << "結果: " << intCalc.add(3, 4) << std::endl; // 整数型の加算
    Calculator<double> doubleCalc; // 浮動小数点型のCalculator
    std::cout << "結果: " << doubleCalc.add(3.5, 2.5) << std::endl; // 浮動小数点型の加算
    return 0;
}
整数型の加算
結果: 7
浮動小数点型の加算
結果: 6

この例では、Calculatorクラスを整数型と浮動小数点型で特殊化し、それぞれ異なるメッセージを表示しています。

これにより、型に特化した処理を行うことができます。

2. テンプレートの部分特殊化

部分特殊化は、テンプレート引数の一部を特定の型に固定し、残りの引数を一般化する方法です。

以下の例では、Pairクラスを部分特殊化して、最初の引数をint型に固定しています。

#include <iostream>
template <typename T1, typename T2>
class Pair {
public:
    Pair(T1 first, T2 second) : first_(first), second_(second) {}
    void display() const {
        std::cout << "First: " << first_ << ", Second: " << second_ << std::endl;
    }
private:
    T1 first_;
    T2 second_;
};
// 最初の引数がint型の部分特殊化
template <typename T2>
class Pair<int, T2> {
public:
    Pair(int first, T2 second) : first_(first), second_(second) {}
    void display() const {
        std::cout << "整数型のFirst: " << first_ << ", Second: " << second_ << std::endl;
    }
private:
    int first_;
    T2 second_;
};
int main() {
    Pair<double, std::string> myPair1(3.14, "C++"); // 一般的なPair
    myPair1.display(); // 一般的な表示
    Pair<int, double> myPair2(42, 2.718); // 部分特殊化されたPair
    myPair2.display(); // 部分特殊化された表示
    return 0;
}
First: 3.14, Second: C++
整数型のFirst: 42, Second: 2.718

この例では、Pairクラスを部分特殊化し、最初の引数がint型の場合に特別な表示を行うようにしています。

これにより、特定の型に対して異なる動作を実装することができます。

3. 特殊化と部分特殊化の利点

  • 効率性: 特定の型に対して最適化された実装を提供することで、パフォーマンスを向上させることができます。
  • 可読性: 型に特化した実装を持つことで、コードの可読性が向上します。
  • 柔軟性: 特殊化や部分特殊化を使用することで、さまざまな型に対応した柔軟な設計が可能になります。

テンプレートの特殊化と部分特殊化を活用することで、C++プログラミングにおいてより強力で効率的なコードを書くことができます。

これにより、型に特化した処理を行うことができ、プログラムのパフォーマンスや可読性を向上させることが可能です。

実践例:複数のテンプレート引数を活用したデータ構造

複数のテンプレート引数を活用することで、柔軟で再利用可能なデータ構造を作成することができます。

ここでは、スタック(Stack)とキュー(Queue)という2つのデータ構造を実装し、異なる型のデータを扱えるようにします。

1. テンプレートを使用したスタックの実装

以下の例では、複数のテンプレート引数を持つスタッククラスを実装します。

このスタックは、異なる型のデータを保持できるように設計されています。

#include <iostream>
#include <vector>
#include <stdexcept>
template <typename T, typename Container = std::vector<T>>
class Stack {
public:
    void push(const T& value) { // 値をスタックに追加
        container_.push_back(value);
    }
    void pop() { // スタックから値を削除
        if (container_.empty()) {
            throw std::out_of_range("スタックが空です。"); // 空のスタックから削除しようとした場合
        }
        container_.pop_back();
    }
    T top() const { // スタックの最上部の値を取得
        if (container_.empty()) {
            throw std::out_of_range("スタックが空です。"); // 空のスタックから取得しようとした場合
        }
        return container_.back();
    }
    bool empty() const { // スタックが空かどうかを確認
        return container_.empty();
    }
private:
    Container container_; // データを保持するコンテナ
};
int main() {
    Stack<int> intStack; // int型のスタック
    intStack.push(10);
    intStack.push(20);
    std::cout << "最上部の値: " << intStack.top() << std::endl; // 最上部の値を表示
    intStack.pop(); // 値を削除
    std::cout << "最上部の値: " << intStack.top() << std::endl; // 最上部の値を表示
    Stack<std::string> stringStack; // string型のスタック
    stringStack.push("Hello");
    stringStack.push("World");
    std::cout << "最上部の値: " << stringStack.top() << std::endl; // 最上部の値を表示
    stringStack.pop(); // 値を削除
    std::cout << "最上部の値: " << stringStack.top() << std::endl; // 最上部の値を表示
    return 0;
}
最上部の値: 20
最上部の値: 10
最上部の値: World
最上部の値: Hello

このスタッククラスは、テンプレート引数Tを使用して、異なる型のデータを保持することができます。

デフォルトではstd::vectorを使用してデータを管理しています。

2. テンプレートを使用したキューの実装

次に、複数のテンプレート引数を持つキュークラスを実装します。

このキューも異なる型のデータを保持できるように設計されています。

#include <iostream>
#include <deque>
#include <stdexcept>
template <typename T, typename Container = std::deque<T>>
class Queue {
public:
    void enqueue(const T& value) { // 値をキューに追加
        container_.push_back(value);
    }
    void dequeue() { // キューから値を削除
        if (container_.empty()) {
            throw std::out_of_range("キューが空です。"); // 空のキューから削除しようとした場合
        }
        container_.pop_front();
    }
    T front() const { // キューの最前部の値を取得
        if (container_.empty()) {
            throw std::out_of_range("キューが空です。"); // 空のキューから取得しようとした場合
        }
        return container_.front();
    }
    bool empty() const { // キューが空かどうかを確認
        return container_.empty();
    }
private:
    Container container_; // データを保持するコンテナ
};
int main() {
    Queue<int> intQueue; // int型のキュー
    intQueue.enqueue(1);
    intQueue.enqueue(2);
    std::cout << "最前部の値: " << intQueue.front() << std::endl; // 最前部の値を表示
    intQueue.dequeue(); // 値を削除
    std::cout << "最前部の値: " << intQueue.front() << std::endl; // 最前部の値を表示
    Queue<std::string> stringQueue; // string型のキュー
    stringQueue.enqueue("Apple");
    stringQueue.enqueue("Banana");
    std::cout << "最前部の値: " << stringQueue.front() << std::endl; // 最前部の値を表示
    stringQueue.dequeue(); // 値を削除
    std::cout << "最前部の値: " << stringQueue.front() << std::endl; // 最前部の値を表示
    return 0;
}
最前部の値: 1
最前部の値: 2
最前部の値: Apple
最前部の値: Banana

このキュークラスも、テンプレート引数Tを使用して、異なる型のデータを保持することができます。

デフォルトではstd::dequeを使用してデータを管理しています。

複数のテンプレート引数を活用することで、スタックやキューなどのデータ構造を柔軟に実装することができます。

これにより、異なる型のデータを扱うことができ、再利用可能なコードを書くことが可能になります。

テンプレートを使用することで、型に依存しない汎用的なデータ構造を作成することができ、C++プログラミングの強力な機能を活用することができます。

まとめ

この記事では、C++における複数のテンプレート引数を持つ関数やクラスの書き方、さらにそれらを活用したデータ構造の実装について詳しく解説しました。

テンプレートの特殊化や部分特殊化、SFINAEの概念を理解することで、特定の型に対して柔軟な処理を行うことが可能になります。

これらの技術を活用して、より効率的で再利用可能なコードを書くことに挑戦してみてください。

関連記事

Back to top button