C++/テンプレート

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++

はじめに[編集]

テンプレートはC++におけるジェネリックプログラミングの中核をなす機能であり、型に依存しない汎用的なコードを記述することを可能にします。これにより、同一のコードをさまざまな型に対して再利用することができ、プログラムの保守性と効率を大幅に向上させます。本章ではテンプレートの基本概念から高度な使い方までを学び、実際のプログラムに応用する方法を紹介します。

C++のテンプレートは、パラメーター化された型です。 C++には、以下の3種類のテンプレートがあります。

関数テンプレート[編集]

基本構文[編集]

関数テンプレートは、さまざまな型に対して同一の処理を行う関数を定義するためのものです。以下に基本的な関数テンプレートの例を示します。

template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

上記の例では、max関数は任意の型Tに対して動作するように定義されています。これにより、intdoubleなど、あらゆる型に対して同じmax関数を使用することができます。

型推論[編集]

テンプレート関数の呼び出し時に、コンパイラは引数の型からテンプレート引数を推論します。例えば、以下のコードではTintに推論されます。

auto main() -> int {
    int a{10}, b{20};
    std::cout << max(a, b) << std::endl; // Tはintに推論される
}

オーバーロードとテンプレート[編集]

テンプレート関数は通常の関数と同様にオーバーロードが可能です。異なるバージョンの関数を提供することで、特定の型や状況に応じた最適な実装を提供できます。

template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

double max(double a, double b) {
    return a > b ? a : b;
}
例:一般的な関数のテンプレート化
以下は、配列の最大値を求めるテンプレート関数の例です。この関数は、任意の型の配列に対して動作します。
template<typename T>
T arrayMax(T* arr, int size) {
    T maxVal = arr[0];
    for(int i = 1; i < size; ++i) {
        if(arr[i] > maxVal) {
            maxVal = arr[i];
        }
    }
    return maxVal;
}

auto main() -> int {
    int arr[] = {1, 5, 3, 9, 7};
    std::cout << arrayMax(arr, 5) << std::endl; // 出力: 9
}

クラステンプレート[編集]

基本構文[編集]

クラステンプレートは、さまざまな型に対して動作するクラスを定義するためのものです。以下に基本的なクラステンプレートの例を示します。

#include <vector>

template <typename T>
class Stack {
  private:
    std::vector<T> elems;

  public:
    void push(T const &);
    void pop();
    auto top() const -> T;
    [[nodiscard]] auto empty() const -> bool { return elems.empty(); }
}

この例では、Stackクラスは任意の型Tに対して動作するように定義されています。

メンバー関数とデータメンバー[編集]

テンプレートクラスのメンバー関数は、クラスの外部で定義することも可能です。以下にその例を示します。

template<typename T>
void Stack<T>::push(T const& elem) {
    elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop() {
    if(elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elems.pop_back();
}

template<typename T>
T Stack<T>::top() const {
    if(elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}

テンプレートの特殊化[編集]

特定の型に対して異なる実装が必要な場合、テンプレートの特殊化を行います。以下にstd::string型に対する特殊化の例を示します。

template<>
class Stack<std::string> {
 private:
    std::vector<std::string> elems;
 public:
    void push(std::string const&);
    void pop();
    std::string top() const;
    bool empty() const {
        return elems.empty();
    }
};

void Stack<std::string>::push(std::string const& elem) {
    elems.push_back(elem);
}

void Stack<std::string>::pop() {
    if(elems.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elems.pop_back();
}

std::string Stack<std::string>::top() const {
    if(elems.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elems.back();
}
例:テンプレートクラスの設計と使用
auto main() -> int {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    std::cout << intStack.top() << std::endl; // 出力: 20
    intStack.pop();
    std::cout << intStack.top() << std::endl; // 出力: 10

    Stack<std::string> stringStack;
    stringStack.push("Hello");
    stringStack.push("World");
    std::cout << stringStack.top() << std::endl; // 出力: World
}

エイリアステンプレート[編集]

エイリアステンプレートは、既存のテンプレートをより簡潔に参照するための方法です。これにより、コードの可読性が向上します。

template<typename T>
using Vec = std::vector<T>;

int main() {
    Vec<int> intVec; // std::vector<int>と同じ
    intVec.push_back(1);
    intVec.push_back(2);
    for(int i : intVec) {
        std::cout << i << " "; // 出力: 1 2
    }
}

テンプレートの高度な使い方[編集]

部分特殊化[編集]

部分特殊化は、特定の条件に対してテンプレートクラスの一部の実装を変更するために使用されます。以下に部分特殊化の例を示します。

template<typename T, typename U>
class Pair {};

template<typename T>
class Pair<T, int> {
    // Tとintのペアに対する部分特殊化
};

完全特殊化[編集]

完全特殊化は、特定の型の組み合わせに対してテンプレートの全体的な実装を変更するために使用されます。以下に完全特殊化の例を示します。

template<>
class Pair<int, int> {
    // intとintのペアに対する完全特殊化
};

テンプレートの継承[編集]

テンプレートクラスは他のテンプレートクラスや非テンプレートクラスを継承することができます。以下にその例を示します。

template<typename T>
class DerivedStack : public Stack<T> {
    // Stack<T>を継承
};

テンプレートの再帰とメタプログラミング[編集]

テンプレートメタプログラミングは、テンプレートを使用してコンパイル時に計算を行う技術です。以下に再帰的テンプレートの例を示します。

template<int N>
struct Factorial {
    static const int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static const int value = 1;
};

auto main() -> int {
    std::cout << Factorial<5>::value << std::endl; // 出力: 120
}

可変パラメータテンプレート[編集]

可変パラメータテンプレート(Variadic Templates)は、C++11で導入された機能で、テンプレート引数や関数引数の数を柔軟に扱うことができます。この機能を利用すると、任意の数の引数を受け取ることができるテンプレートを作成することが可能です。以下に、関数、クラス、メンバーそれぞれにおける可変パラメータテンプレートの使用例を示します。

関数テンプレートでの可変パラメータ[編集]

関数テンプレートでの可変パラメータテンプレートは、複数の引数を受け取る関数を定義するために使用されます。

単純なプリント関数
#include <iostream>

// 基本ケース
template <typename T> 
void print(T t) { std::cout << t << std::endl; }

// 再帰ケース
template <typename T, typename... Args> 
void print(T t, Args... args) {
    std::cout << t << " ";
    print(args...);
}

auto main() -> int {
    print(1, 2.5, "Hello", 'a');
    return 0;
}

この例では、print関数は1つの引数を出力し、その後に残りの引数に対して再帰的にprint関数を呼び出します。これにより、任意の数の引数を順に出力することができます。

クラステンプレートでの可変パラメータ[編集]

クラステンプレートでも可変パラメータテンプレートを使用することができます。これにより、任意の数の型を引数として受け取ることができるクラスを定義することが可能です。

タプルクラス
#include <iostream>
#include <string>

// 自前のタプルクラス
template <typename... Values> 
class SimpleTuple;

template <typename Head, typename... Tail> 
class SimpleTuple<Head, Tail...> {
  public:
    SimpleTuple(Head head, Tail... tail) : head{head}, tail{tail...} {}

    Head head;
    SimpleTuple<Tail...> tail;
};

template <> 
class SimpleTuple<> {};

auto main() -> int {
    SimpleTuple<int, double, std::string> myTuple(42, 3.14, "Hello");
    std::cout << myTuple.head << ", " << myTuple.tail.head << ", "
              << myTuple.tail.tail.head << std::endl;
    return 0;
}

この例では、SimpleTupleというクラスを定義し、可変パラメータテンプレートを用いて任意の数の型を引数として受け取っています。このようにして、任意の数の要素を持つタプルを実装しています。

クラステンプレートのメンバー関数での可変パラメータ[編集]

クラステンプレートのメンバー関数に対しても可変パラメータテンプレートを使用することができます。これにより、クラス内で可変パラメータを処理するメンバー関数を定義できます。

ログクラス
#include <iostream>
#include <string>

class Logger {
  public:
    template <typename... Args> void log(Args... args) {
        (print(args), ...); // Fold expression (C++17)
        std::cout << std::endl;
    }

  private:
    template <typename T> void print(T t) { std::cout << t << " "; }
};

auto main() -> int {
    Logger logger;
    logger.log(1, 2.5, "Hello", 'a');
    return 0;
}

この例では、Loggerクラスに可変パラメータを受け取るlogメンバー関数を定義しています。printメンバー関数を使用して、各引数を順に出力しています。ここではC++17のフォールド式を使用していますが、再帰的な方法も使用できます。

まとめ

可変パラメータテンプレートは、C++11で導入された強力な機能で、関数テンプレート、クラステンプレート、およびクラスのメンバー関数で使用できます。この機能を利用することで、任意の数の引数を柔軟に処理できるテンプレートを作成することができ、より汎用的で再利用性の高いコードを記述することが可能となります。

テンプレートと標準ライブラリ[編集]

STLコンテナとテンプレート[編集]

STL(Standard Template Library)のコンテナはテンプレートとして設計されており、任意の型を格納することができます。以下にstd::vectorの例を示します。

std::vector<int> vec;
vec.push_back(1);
vec.push_back(2);
std::cout << vec[0] << std::endl; // 出力: 1
std::cout << vec[1] << std::endl; // 出力: 2

アルゴリズムと関数オブジェクト[編集]

STLには汎用的なアルゴリズムが多数用意されており、これらもテンプレート

を活用しています。以下にstd::sortの例を示します。

std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end());
for(int n : vec) {
    std::cout << n << " "; // 出力: 1 1 3 4 5
}

イテレータとジェネリックプログラミング[編集]

イテレータはSTLコンテナ間の共通のインターフェースを提供し、ジェネリックプログラミングを可能にします。以下にイテレータを使用した例を示します。

std::vector<int> vec = {1, 2, 3, 4, 5};
for(std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}

テンプレートのコンパイルとデバッグ[編集]

テンプレートのコンパイルの仕組み[編集]

テンプレートはヘッダーファイルに定義され、インスタンシエーションはコンパイル時に行われます。これにより、テンプレートコードの実装が分離されることなく一貫性が保たれます。

エラーメッセージの読み方[編集]

テンプレートコードのエラーメッセージは複雑ですが、問題の発生箇所と原因を慎重に特定することが重要です。エラーメッセージを読み解くためのコツとしては、テンプレート引数の型やエラーの発生箇所を注意深く確認することが挙げられます。

デバッグのコツとツール[編集]

テンプレートコードのデバッグには、コンパイルフラグや静的解析ツールが役立ちます。例えば、-Wallフラグを使用して警告を有効にし、gdbclang-tidyなどのツールを活用することで、コードの問題を早期に発見することができます。

実践例と応用[編集]

テンプレートによるデザインパターン[編集]

テンプレートを用いたシングルトンパターンの実装例を紹介します。以下にその例を示します。

template<typename T>
class Singleton {
 public:
    static T& instance() {
        static T instance;
        return instance;
    }
 private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

class MyClass {};

auto main() -> int {
    MyClass& instance1 = Singleton<MyClass>::instance();
    MyClass& instance2 = Singleton<MyClass>::instance();
    assert(&instance1 == &instance2); // 同一のインスタンス
}

テンプレートを用いたライブラリ設計[編集]

テンプレートを活用した小さなライブラリの設計例を紹介します。例えば、数学ライブラリとしてベクトル計算を行うテンプレートクラスを設計することができます。

template<typename T>
class Vector {
 private:
    T x, y, z;
 public:
    Vector(T x, T y, T z) : x(x), y(y), z(z) {}
    T dot(const Vector& other) const {
        return x * other.x + y * other.y + z * other.z;
    }
    Vector cross(const Vector& other) const {
        return Vector(
            y * other.z - z * other.y,
            z * other.x - x * other.z,
            x * other.y - y * other.x
        );
    }
};

パフォーマンスと最適化[編集]

テンプレートの使用によるコードの最適化とパフォーマンス向上の方法を解説します。テンプレートを用いることで、コンパイル時に型が確定し、関数インライン化などの最適化が容易に行われます。これにより、実行時のオーバーヘッドを減らすことができます。

C++20の新機能とテンプレート[編集]

コンセプト(Concepts)[編集]

コンセプトはテンプレート引数に対する制約を定義する新しい方法です。これにより、テンプレートの使用におけるエラーを早期に検出することができます。

template<typename T>
concept Integral = std::is_integral_v<T>;

template<Integral T>
T add(T a, T b) {
    return a + b;
}

テンプレートの新しい機能と構文[編集]

C++20で導入されたテンプレートの新機能や構文を紹介します。例えば、非型テンプレートパラメータの拡張や、テンプレートの簡略化された構文などがあります。

template<auto N>
struct ValueHolder {
    static constexpr auto value = N;
};

auto main() -> int {
    ValueHolder<42> intHolder;
    std::cout << intHolder.value << std::endl; // 出力: 42
}
例:C++20を用いたジェネリックプログラミング
C++20の新機能を活用したジェネリックプログラミングの実例を紹介します。例えば、コンセプトを用いた汎用アルゴリズムの実装などがあります。
template<typename T>
concept Sortable = requires(T a) {
    { a.begin() } -> std::input_iterator;
    { a.end() } -> std::input_iterator;
    { std::sort(a.begin(), a.end()) };
};

void sortContainer(Sortable auto& container) {
    std::sort(container.begin(), container.end());
}

auto main() -> int {
    std::vector<int> vec = {3, 1, 4, 1, 5};
    sortContainer(vec);
    for(int n : vec) {
        std::cout << n << " "; // 出力: 1 1 3 4 5
    }
}

まとめと演習問題[編集]

重要なポイントの総復習[編集]

本章で学んだ重要なポイントを総復習します。テンプレートの基本概念、関数テンプレート、クラステンプレート、特殊化、再帰とメタプログラミング、C++20の新機能など、多岐にわたる内容を振り返ります。

実践的な演習問題とその解答例[編集]

演習問題1
テンプレート関数を用いて、任意の型の配列の平均値を計算する関数を実装しなさい。
演習問題2
部分特殊化を用いて、特定の条件を満たすクラステンプレートを実装しなさい。例えば、Pairクラスのうち、片方の型がintの場合に異なる動作をさせる。