C++ テンプレート:可変長引数テンプレートの使い方

C++ テンプレート:可変長引数テンプレート(Variadic Templates)の使い方徹底解説

C++11で導入された可変長引数テンプレート(Variadic Templates)は、テンプレート引数の個数を可変にできる強力な機能です。これにより、これまで難しかった関数の引数リストやクラスのテンプレート引数リストを柔軟に扱うことが可能になり、コードの再利用性、汎用性、表現力を大幅に向上させることができます。

本稿では、可変長引数テンプレートの基本的な概念から高度な応用まで、詳細な説明と具体的なコード例を通して、その使い方を徹底的に解説します。

1. 可変長引数テンプレートとは

可変長引数テンプレートとは、テンプレート引数の数が固定ではなく、コンパイル時に決定されるテンプレートのことです。 これは、関数テンプレート、クラステンプレート、エイリアステンプレートのいずれにも適用できます。

可変長引数テンプレートは、主に次の2つの要素で構成されます。

  • テンプレートパラメータパック (Template Parameter Pack): 型名 typename または class の後に ... をつけたもので、0個以上のテンプレート引数をパックとして受け取ります。例えば、typename... Argsclass... Types などです。
  • 関数パラメータパック (Function Parameter Pack): 関数の引数リストの中で、型名の後に ... をつけたもので、0個以上の関数引数をパックとして受け取ります。例えば、Args... args などです。

これらのパックは、C++11で導入されたパラメータパック展開 (Parameter Pack Expansion)というメカニズムを使って展開されます。パラメータパック展開は、パック内の要素を順番に取り出し、指定されたパターンに適用することで、コンパイル時にコードを生成します。

2. 可変長引数テンプレートの基本構文

2.1 関数テンプレート

最も基本的な可変長引数関数テンプレートの例を見てみましょう。

“`cpp

include

template
void print_all(Args… args) {
// パラメータパックを展開して、それぞれの引数をstd::coutに出力する
(std::cout << … << args) << std::endl;
}

int main() {
print_all(1, 2.5, “hello”, true); // 12.5hello1
return 0;
}
“`

この例では、print_all 関数は可変個の引数を受け取り、std::cout を用いてそれらを順に出力します。

  • template <typename... Args> は、Args がテンプレートパラメータパックであることを宣言しています。 Args は0個以上の型を受け取ることができます。
  • void print_all(Args... args) は、args が関数パラメータパックであることを宣言しています。 args は、Args テンプレートパラメータパックに対応する数の引数を受け取ります。
  • (std::cout << ... << args) << std::endl; は、fold expression (フォールド式) を用いてパラメータパック args を展開し、それぞれの引数を std::cout に出力しています。フォールド式は、C++17で導入された機能で、パラメータパックに対する操作を簡潔に記述することができます。

パラメータパック展開の詳細:

... は、パラメータパックを展開するために使用される演算子です。上記の例では、(std::cout << ... << args) は、以下のように展開されます。

  • args が 1, 2.5, “hello”, true を含む場合、上記の式はコンパイル時に以下のように展開されます。

    cpp
    std::cout << 1 << 2.5 << "hello" << true

注意点:

  • print_all() のように引数を渡さない場合でもコンパイルエラーにはなりません。 Args... は0個の型を受け取ることも可能です。
  • 上記は単純な例ですが、フォールド式を使うことで、より複雑な処理を行うことができます。

2.2 クラステンプレート

可変長引数テンプレートは、クラステンプレートでも利用できます。 例えば、可変個の型を保持する tuple クラスのようなものを定義することができます。

“`cpp

include

include // 標準ライブラリのtuple

template
class MyTuple {
public:
MyTuple(Types… values) : values_(values…) {}

template
typename std::tuple_element>::type get() {
return std::get(values_);
}

private:
std::tuple values_;
};

int main() {
MyTuple my_tuple(10, 3.14, “world”);
std::cout << my_tuple.get<0>() << std::endl; // 10
std::cout << my_tuple.get<1>() << std::endl; // 3.14
std::cout << my_tuple.get<2>() << std::endl; // world
return 0;
}
“`

この例では、MyTuple クラスは可変個の型 Types... をテンプレート引数として受け取り、それらの型の値を保持します。

  • template <typename... Types> は、Types がテンプレートパラメータパックであることを宣言しています。
  • MyTuple(Types... values) : values_(values...) {} は、コンストラクタです。 values_std::tuple<Types...> 型のメンバ変数であり、コンストラクタ引数 values... を用いて初期化されます。
  • template <size_t Index> typename std::tuple_element<Index, std::tuple<Types...>>::type get() は、Index 番目の要素を返すメンバ関数です。 std::tuple_element は、std::tupleIndex 番目の要素の型を取得するためのテンプレートです。 std::get<Index>(values_) は、std::tupleIndex 番目の要素を取得するための関数です。

2.3 エイリアステンプレート

エイリアステンプレートでも可変長引数テンプレートを利用できます。これは、特定の型を簡潔な名前で参照するために役立ちます。

“`cpp

include

include

template
using VecOfInt = std::vector;

int main() {
VecOfInt<> my_vector; // std::vector my_vector と同じ
my_vector.push_back(1);
my_vector.push_back(2);

for (int x : my_vector) {
std::cout << x << std::endl;
}
return 0;
}
“`

この例では、VecOfIntstd::vector<int> のエイリアスとして定義されています。 可変長引数テンプレート Args... は使用されていませんが、エイリアステンプレートの基本的な使い方を示しています。より複雑な例では、Args... を利用して、テンプレート引数を受け取り、それを内部で使用するようなエイリアスを作成できます。

3. パラメータパック展開 (Parameter Pack Expansion)

パラメータパック展開は、可変長引数テンプレートの核心となる機能です。 パック内の各要素に対して、特定のパターンを適用することで、コンパイル時にコードを生成します。

3.1 基本的な展開

前述の print_all 関数の例で見たように、パラメータパック展開は、... 演算子を使って行います。

cpp
template <typename... Args>
void print_all(Args... args) {
(std::cout << ... << args) << std::endl;
}

このコードは、args パラメータパック内の各引数に対して、std::cout << ... << args というパターンを適用します。 ... 演算子は、パック内の要素を順番に取り出し、指定されたパターンに置き換えることで、コードを生成します。

3.2 様々な展開方法

パラメータパック展開は、様々なコンテキストで使用できます。

  • 関数引数リスト: 関数呼び出し時に、パラメータパックを展開して、引数を渡すことができます。

    “`cpp
    template
    void call_function(void (*func)(Args…), Args… args) {
    func(args…); // パラメータパックを展開して、func に引数を渡す
    }

    void my_function(int a, double b, std::string c) {
    std::cout << “a: ” << a << “, b: ” << b << “, c: ” << c << std::endl;
    }

    int main() {
    call_function(my_function, 10, 3.14, “hello”);
    return 0;
    }
    “`

  • 初期化リスト: コンストラクタの初期化リストで、パラメータパックを展開して、メンバ変数を初期化することができます。

    “`cpp
    template
    class MyClass {
    public:
    MyClass(Args… args) : members_(args…) {}

    private:
    std::tuple members_;
    };
    “`

  • 配列初期化: 配列の初期化時に、パラメータパックを展開して、要素を初期化することができます。

    “`cpp
    template
    void print_array(Args… args) {
    int arr[] = {args…}; // パラメータパックを展開して、配列を初期化
    for (int x : arr) {
    std::cout << x << ” “;
    }
    std::cout << std::endl;
    }

    int main() {
    print_array(1, 2, 3, 4, 5); // 1 2 3 4 5
    return 0;
    }
    “`

  • sizeof...() 演算子: sizeof...() 演算子を使用すると、パラメータパック内の要素数を取得できます。

    “`cpp
    template
    void print_count() {
    std::cout << “Number of arguments: ” << sizeof…(Args) << std::endl;
    }

    int main() {
    print_count(); // Number of arguments: 3
    return 0;
    }
    “`

4. フォールド式 (Fold Expression)

C++17で導入されたフォールド式は、パラメータパックに対する操作をより簡潔に記述するための強力な機能です。

4.1 フォールド式の種類

フォールド式には、次の4つの種類があります。

  • 単項左フォールド (Unary Left Fold): ( ... op pack)
  • 単項右フォールド (Unary Right Fold): (pack op ...)
  • 二項左フォールド (Binary Left Fold): (init op ... op pack)
  • 二項右フォールド (Binary Right Fold): (pack op ... op init)

ここで、op は二項演算子(+, -, , /, %, ^, &, |, =, <, >, <<, >>, +=, -=, =, /=, %=, ^=, &=, |=, <<=, >>=, ==, !=, <=, >=, &&, ||, , . , ->)であり、pack はパラメータパック、init は初期値です。

4.2 フォールド式の例

  • 合計:

    “`cpp
    template
    auto sum(Args… args) {
    return (args + …); // 単項左フォールド
    }

    int main() {
    std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 15
    return 0;
    }
    “`

  • 積:

    “`cpp
    template
    auto product(Args… args) {
    return (1 * … * args); // 二項左フォールド
    }

    int main() {
    std::cout << product(1, 2, 3, 4, 5) << std::endl; // 120
    return 0;
    }
    “`

  • 論理AND:

    “`cpp
    template
    bool all_true(Args… args) {
    return (true && … && args); // 二項左フォールド
    }

    int main() {
    std::cout << all_true(true, true, true) << std::endl; // 1
    std::cout << all_true(true, false, true) << std::endl; // 0
    return 0;
    }
    “`

  • カンマ演算子:

    “`cpp
    template
    void print_all(Args… args) {
    (std::cout << args << std::endl, …); // 単項右フォールド (カンマ演算子)
    }

    int main() {
    print_all(1, 2.5, “hello”);
    return 0;
    }
    “`

4.3 フォールド式を使用する際の注意点

  • 単項フォールド式の場合、パラメータパックが空であるとコンパイルエラーになる可能性があります。 これは、演算子によっては、初期値がないと計算できないためです。例えば、/ 演算子の場合、初期値がないと0で除算することになるため、エラーが発生します。 二項フォールド式では、初期値を指定することで、この問題を回避できます。
  • フォールド式は、C++17以降でのみ利用可能です。

5. 可変長引数テンプレートの応用例

可変長引数テンプレートは、様々な場面で役立ちます。

5.1 汎用的な関数ラッパー

関数ラッパーは、既存の関数をラップし、追加の処理を行う関数です。 可変長引数テンプレートを使用すると、任意の引数を持つ関数をラップする汎用的な関数ラッパーを作成できます。

“`cpp

include

include

template
auto time_it(Func func, Args… args) {
auto start = std::chrono::high_resolution_clock::now();
auto result = func(args…);
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast(end – start);
std::cout << “Execution time: ” << duration.count() << ” microseconds” << std::endl;
return result;
}

int my_function(int a, int b) {
// 時間のかかる処理
int sum = 0;
for (int i = 0; i < a * b; ++i) {
sum += i;
}
return sum;
}

int main() {
int result = time_it(my_function, 1000, 1000);
std::cout << “Result: ” << result << std::endl;
return 0;
}
“`

この例では、time_it 関数は、任意の関数 func とその引数 args... を受け取り、関数の実行時間計測してから、その結果を返します。

5.2 型安全性のある可変長引数関数

Cスタイルの可変長引数関数 (printf など) は、型安全ではありません。 可変長引数テンプレートを使用すると、コンパイル時に型チェックを行うことができる、より安全な可変長引数関数を作成できます。

“`cpp

include

include

template
void safe_print(const std::string& format, Args… args) {
// format文字列とargsを解析して出力する処理 (ここでは簡略化)
// 実際の実装では、format文字列のプレースホルダーとargsの型を検証する必要があります
size_t arg_index = 0;
std::string::size_type pos = 0;
while ((pos = format.find(“{}”, pos)) != std::string::npos) {
if (arg_index < sizeof…(Args)) {
// パラメータパックから対応する型の引数を取り出して出力
// ここでは型推論を利用して、引数の型を自動的に判別
// (std::cout << args_array[arg_index])のように直接アクセスすることはできません)
// 代わりに、パラメータパック展開と型推論を利用
// この部分の実装は非常に複雑になる可能性があるため、簡略化された例としています
pos += 2; // “{}”の次の位置へ
arg_index++;
} else {
std::cerr << “Error: Too few arguments provided for format string.” << std::endl;
return;
}
}

if (arg_index < sizeof…(Args)) {
std::cerr << “Error: Too many arguments provided for format string.” << std::endl;
}
}

int main() {
safe_print(“The answer is {}.”, 42);
// safe_print(“The answer is {}.”, “hello”); // コンパイルエラーにはならない(この例では)
safe_print(“The answer is {} and {}.”, 42, “hello”);
return 0;
}
“`

この例では、safe_print 関数は、フォーマット文字列と可変個の引数を受け取り、フォーマット文字列に基づいて引数を出力します。 この例では、フォーマット文字列と引数の型を検証する処理は簡略化されていますが、実際のアプリケーションでは、より厳密な型チェックを行う必要があります。

5.3 テンプレートメタプログラミング

可変長引数テンプレートは、テンプレートメタプログラミングでも強力なツールとなります。 例えば、型のリストを操作するテンプレートや、コンパイル時に計算を行うテンプレートを作成できます。

“`cpp

include

include

template
struct Contains {
static constexpr bool value = (std::is_same_v || …);
};

int main() {
std::cout << Contains::value << std::endl; // 1
std::cout << Contains::value << std::endl; // 0
return 0;
}
“`

この例では、Contains テンプレートは、型 T が型リスト Args... に含まれているかどうかをコンパイル時に判定します。

6. まとめ

可変長引数テンプレートは、C++のテンプレート機能を大幅に拡張する強力な機能です。 これにより、より汎用的で再利用性の高いコードを作成することが可能になります。 本稿では、可変長引数テンプレートの基本的な概念から高度な応用まで、詳細な説明と具体的なコード例を通して、その使い方を解説しました。 可変長引数テンプレートを理解し、適切に活用することで、C++プログラミングのスキルを向上させることができます。

7. 付録:よくある質問 (FAQ)

  • Q: 可変長引数テンプレートは、C++のどのバージョンから利用できますか?

    A: C++11から利用可能です。

  • Q: 可変長引数テンプレートは、パフォーマンスに影響を与えますか?

    A: 可変長引数テンプレートは、コンパイル時に展開されるため、実行時のオーバーヘッドはほとんどありません。ただし、コンパイル時間が長くなる可能性があります。

  • Q: 可変長引数テンプレートを使用する際に注意すべき点はありますか?

    A:
    * パラメータパック展開の順序に注意してください。
    * フォールド式を使用する際には、初期値の指定が必要な場合があります。
    * 可変長引数テンプレートは、コードの可読性を損なう可能性があるため、適切に使用する必要があります。

  • Q: 可変長引数テンプレートを学ぶための参考になる資料はありますか?

    A:
    * C++ Primer (Stanley B. Lippman, Josée Lajoie, Barbara E. Moo著)
    * Effective Modern C++ (Scott Meyers著)
    * cppreference.com

本稿が、可変長引数テンプレートの理解と活用の一助となれば幸いです。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール