C++の可変引数テンプレートは、柔軟で強力な機能を提供し、さまざまなデータ型や数の引数を受け取る関数やクラスを定義するのに役立ちます。この機能は、プログラムの再利用性を高め、コードの冗長性を減らすのに非常に有用です。本記事では、可変引数テンプレートの基本的な概念から実装例、さらに応用例までを詳しく解説します。C++の高度な機能を活用して、効率的なプログラム開発を目指しましょう。
可変引数テンプレートの基本概念
可変引数テンプレート(Variadic Templates)は、C++11から導入された機能で、関数やクラスが任意の数の引数を受け取ることを可能にします。これにより、同じ機能を持つ関数やクラスを異なる引数の組み合わせで利用する際に、コードの重複を避けることができます。可変引数テンプレートのメリットには以下のような点があります。
メリット
- コードの再利用性: 一度定義したテンプレートを使い回すことで、コードの再利用性が向上します。
- 柔軟性: 任意の数の引数を取ることができるため、さまざまな状況に対応できます。
- 簡潔性: 複数のオーバーロード関数を定義する必要がなくなり、コードが簡潔になります。
基本構文
可変引数テンプレートの基本構文は以下の通りです。
template<typename... Args>
void functionName(Args... args) {
// 関数の本体
}
ここで、typename... Args
はテンプレートパラメータパックを表し、Args... args
は関数パラメータパックを表します。これにより、関数は任意の数の引数を受け取ることができます。
次の項目では、具体的な使い方について詳しく見ていきます。
基本的な使い方
可変引数テンプレートの基本的な使い方を、具体的なコード例を交えて説明します。
シンプルな例
まずは、シンプルな可変引数テンプレート関数の例を見てみましょう。この関数は、渡された任意の数の引数を単に出力します。
#include <iostream>
// 可変引数テンプレート関数
template<typename... Args>
void printArgs(Args... args) {
(std::cout << ... << args) << '\n';
}
int main() {
printArgs(1, 2, 3.5, "Hello", 'A');
return 0;
}
このコードでは、printArgs
関数が任意の数の引数を受け取り、それらを標準出力に出力します。(std::cout << ... << args)
は、C++17で導入されたフォールド式を使用して、すべての引数を std::cout
に出力しています。
再帰的なパラメータパック展開
次に、再帰的にパラメータパックを展開する方法を紹介します。この方法は、C++11でも使用可能です。
#include <iostream>
// ベースケース
void print() {}
// 可変引数テンプレート関数
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...);
}
int main() {
print(1, 2, 3.5, "Hello", 'A');
return 0;
}
この例では、print
関数が再帰的に呼び出され、パラメータパック args
の各要素が一つずつ処理されます。最終的にベースケースの print()
が呼ばれ、再帰が終了します。
クラステンプレートの例
可変引数テンプレートは関数だけでなく、クラステンプレートにも使用できます。以下の例では、任意の数の引数を持つタプルを作成するクラスを示します。
#include <iostream>
#include <tuple>
// 可変引数テンプレートクラス
template<typename... Args>
class MyTuple {
public:
MyTuple(Args... args) : data(args...) {}
void print() const {
printTuple(data);
}
private:
std::tuple<Args...> data;
template<std::size_t... I>
void printTuple(const std::tuple<Args...>& tup, std::index_sequence<I...>) const {
((std::cout << std::get<I>(tup) << " "), ...);
}
void printTuple(const std::tuple<Args...>& tup) const {
printTuple(tup, std::index_sequence_for<Args...>{});
}
};
int main() {
MyTuple<int, double, const char*> myTuple(1, 2.5, "Hello");
myTuple.print();
return 0;
}
このコードでは、MyTuple
クラスが可変引数テンプレートを使用しており、任意の数の引数を受け取るタプルを保持します。タプルの内容は、print
メソッドを使って出力されます。
次の項目では、テンプレートパラメータパックの展開方法とその注意点について詳しく説明します。
テンプレートパラメータパックの展開
テンプレートパラメータパックを展開する方法について、具体的な例を交えて説明します。展開方法にはいくつかのテクニックがありますが、ここでは主要な方法を紹介します。
再帰展開
再帰的な関数呼び出しを使用して、テンプレートパラメータパックを展開する方法です。これはC++11でも使用可能です。
#include <iostream>
// ベースケース
void print() {
std::cout << "End of recursion" << std::endl;
}
// 再帰的な可変引数テンプレート関数
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // 再帰呼び出し
}
int main() {
print(1, 2.5, "Hello", 'A');
return 0;
}
この例では、print
関数が再帰的に呼び出され、パラメータパック args
の各要素が一つずつ処理されます。最終的に、ベースケースの print()
が呼ばれて再帰が終了します。
フォールド式
C++17で導入されたフォールド式は、パラメータパックを簡潔に展開するための強力な手法です。以下にその使用例を示します。
#include <iostream>
// フォールド式を使用した可変引数テンプレート関数
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << std::endl; // フォールド式
}
int main() {
print(1, 2.5, "Hello", 'A');
return 0;
}
フォールド式を使用すると、パラメータパック全体を一度に処理できるため、再帰展開よりもシンプルで効率的です。
インデックスシーケンスを使用した展開
インデックスシーケンスを使用して、タプルや配列のようなデータ構造の要素を展開する方法もあります。以下にその例を示します。
#include <iostream>
#include <tuple>
#include <utility>
// インデックスシーケンスを使用してタプルの要素を展開する関数
template<typename Tuple, std::size_t... I>
void printTuple(const Tuple& tup, std::index_sequence<I...>) {
((std::cout << std::get<I>(tup) << " "), ...);
}
template<typename... Args>
void print(const std::tuple<Args...>& tup) {
printTuple(tup, std::index_sequence_for<Args...>{});
}
int main() {
auto tup = std::make_tuple(1, 2.5, "Hello", 'A');
print(tup);
return 0;
}
このコードでは、print
関数がタプルの各要素を出力するためにインデックスシーケンスを使用しています。std::index_sequence_for
は、テンプレートパラメータパックに対応するインデックスシーケンスを生成します。
次の項目では、可変引数関数テンプレートの具体的な実装例について詳しく見ていきます。
可変引数関数テンプレートの実装例
可変引数関数テンプレートは、さまざまな状況で非常に役立ちます。ここでは、実際の使用例をいくつか紹介します。
複数の引数を加算する関数
可変引数テンプレートを使用して、複数の引数を加算する関数を実装します。
#include <iostream>
// ベースケース
template<typename T>
T add(T t) {
return t;
}
// 可変引数テンプレート関数
template<typename T, typename... Args>
T add(T first, Args... args) {
return first + add(args...);
}
int main() {
std::cout << add(1, 2, 3, 4, 5) << std::endl; // 出力: 15
std::cout << add(1.1, 2.2, 3.3) << std::endl; // 出力: 6.6
return 0;
}
この例では、add
関数が再帰的に呼び出され、すべての引数を加算して結果を返します。
ログメッセージの生成
可変引数テンプレートを使用して、柔軟なログメッセージ生成関数を実装します。
#include <iostream>
#include <sstream>
// ログメッセージ生成関数
template<typename... Args>
std::string generateLog(Args... args) {
std::ostringstream oss;
(oss << ... << args);
return oss.str();
}
int main() {
std::string log = generateLog("Error: ", "File not found: ", "config.txt");
std::cout << log << std::endl; // 出力: Error: File not found: config.txt
return 0;
}
この例では、generateLog
関数が可変引数を受け取り、それらを連結して一つのログメッセージを生成します。フォールド式を使用して、すべての引数を std::ostringstream
に出力しています。
デバッグ用のプリント関数
デバッグのために、変数の名前と値を出力する関数を実装します。
#include <iostream>
// デバッグ用のプリント関数
template<typename T>
void debugPrint(const char* name, T value) {
std::cout << name << ": " << value << std::endl;
}
template<typename T, typename... Args>
void debugPrint(const char* names, T value, Args... args) {
const char* comma = strchr(names, ',');
std::cout.write(names, comma - names) << ": " << value << std::endl;
debugPrint(comma + 1, args...);
}
int main() {
int a = 5;
double b = 3.14;
const char* c = "Hello";
debugPrint("a, b, c", a, b, c);
// 出力:
// a: 5
// b: 3.14
// c: Hello
return 0;
}
この例では、debugPrint
関数が可変引数を受け取り、それらの名前と値を出力します。引数の名前はカンマ区切りの文字列として渡されます。
次の項目では、可変引数クラステンプレートの具体的な実装例について説明します。
可変引数クラステンプレートの実装例
可変引数クラステンプレートは、クラスが複数の異なる型や数の引数を受け取る場合に役立ちます。ここでは、いくつかの実装例を紹介します。
タプルクラスの実装
標準ライブラリの std::tuple
に似た、簡易版のタプルクラスを実装します。
#include <iostream>
#include <tuple>
// タプルクラスの定義
template<typename... Args>
class MyTuple;
// タプルクラスの特殊化(終端)
template<>
class MyTuple<> {};
// タプルクラスの再帰的定義
template<typename T, typename... Rest>
class MyTuple<T, Rest...> : public MyTuple<Rest...> {
public:
MyTuple(T first, Rest... rest) : MyTuple<Rest...>(rest...), value(first) {}
T value;
};
// タプル要素の取得関数(終端)
template<std::size_t, typename>
struct MyTupleElement;
template<typename T, typename... Rest>
struct MyTupleElement<0, MyTuple<T, Rest...>> {
using type = T;
static T& get(MyTuple<T, Rest...>& tuple) {
return tuple.value;
}
};
template<std::size_t index, typename T, typename... Rest>
struct MyTupleElement<index, MyTuple<T, Rest...>> {
using type = typename MyTupleElement<index - 1, MyTuple<Rest...>>::type;
static type& get(MyTuple<T, Rest...>& tuple) {
return MyTupleElement<index - 1, MyTuple<Rest...>>::get(tuple);
}
};
template<std::size_t index, typename... Args>
typename MyTupleElement<index, MyTuple<Args...>>::type& get(MyTuple<Args...>& tuple) {
return MyTupleElement<index, MyTuple<Args...>>::get(tuple);
}
int main() {
MyTuple<int, double, std::string> myTuple(1, 2.5, "Hello");
std::cout << get<0>(myTuple) << std::endl; // 出力: 1
std::cout << get<1>(myTuple) << std::endl; // 出力: 2.5
std::cout << get<2>(myTuple) << std::endl; // 出力: Hello
return 0;
}
この例では、MyTuple
クラスが任意の数の引数を受け取るタプルを実装しています。また、get
関数を使用してタプルの要素にアクセスできます。
オブザーバーパターンの実装
可変引数クラステンプレートを使用して、オブザーバーパターンを実装します。
#include <iostream>
#include <vector>
#include <functional>
// オブザーバークラスの定義
template<typename... Args>
class Subject {
public:
using Observer = std::function<void(Args...)>;
void addObserver(Observer observer) {
observers.push_back(observer);
}
void notify(Args... args) {
for (auto& observer : observers) {
observer(args...);
}
}
private:
std::vector<Observer> observers;
};
int main() {
Subject<int, const std::string&> subject;
subject.addObserver([](int id, const std::string& message) {
std::cout << "Observer 1: " << id << " - " << message << std::endl;
});
subject.addObserver([](int id, const std::string& message) {
std::cout << "Observer 2: " << id << " - " << message << std::endl;
});
subject.notify(1, "Hello, World!");
subject.notify(2, "Observer Pattern");
return 0;
}
この例では、Subject
クラスが任意の数の引数を受け取るオブザーバーを管理します。notify
メソッドを使用して、すべてのオブザーバーに通知を送信できます。
次の項目では、可変引数テンプレートを使用した便利なユーティリティ関数の作成方法を紹介します。
便利なユーティリティ関数の作成
可変引数テンプレートを使用して、さまざまな便利なユーティリティ関数を作成できます。ここでは、その具体的な例をいくつか紹介します。
簡単なコンテナプリント関数
任意の数のコンテナをプリントする関数を実装します。
#include <iostream>
#include <vector>
#include <list>
// コンテナプリント関数
template<typename Container>
void printContainer(const Container& container) {
for (const auto& element : container) {
std::cout << element << " ";
}
std::cout << std::endl;
}
template<typename Container, typename... Containers>
void printContainers(const Container& first, const Containers&... rest) {
printContainer(first);
printContainers(rest...);
}
void printContainers() {}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<std::string> lst = {"Hello", "World"};
std::vector<double> vec2 = {3.14, 2.71, 1.41};
printContainers(vec, lst, vec2);
return 0;
}
この例では、printContainers
関数が任意の数のコンテナを受け取り、それぞれの要素を出力します。
範囲の最小値と最大値を計算する関数
複数の引数の中から最小値と最大値を計算する関数を実装します。
#include <iostream>
#include <algorithm>
// 最小値を計算する関数
template<typename T>
T findMin(T value) {
return value;
}
template<typename T, typename... Args>
T findMin(T first, Args... args) {
return std::min(first, findMin(args...));
}
// 最大値を計算する関数
template<typename T>
T findMax(T value) {
return value;
}
template<typename T, typename... Args>
T findMax(T first, Args... args) {
return std::max(first, findMax(args...));
}
int main() {
std::cout << "Min: " << findMin(10, 5, 7, 1, 9) << std::endl; // 出力: Min: 1
std::cout << "Max: " << findMax(10, 5, 7, 1, 9) << std::endl; // 出力: Max: 10
return 0;
}
この例では、findMin
と findMax
関数が可変引数テンプレートを使用して、複数の引数の中から最小値と最大値を計算します。
関数のタイミングを測定するユーティリティ
関数の実行時間を計測するためのユーティリティ関数を実装します。
#include <iostream>
#include <chrono>
#include <functional>
// 関数のタイミングを測定するユーティリティ
template<typename Func, typename... Args>
auto measureTime(Func func, Args... args) {
auto start = std::chrono::high_resolution_clock::now();
auto result = func(args...);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> elapsed = end - start;
std::cout << "Execution time: " << elapsed.count() << " seconds" << std::endl;
return result;
}
// テスト関数
int testFunction(int a, int b) {
for (volatile int i = 0; i < 1000000; ++i); // 時間を消費するループ
return a + b;
}
int main() {
int result = measureTime(testFunction, 3, 4);
std::cout << "Result: " << result << std::endl; // 出力: Result: 7
return 0;
}
この例では、measureTime
関数が任意の関数とその引数を受け取り、関数の実行時間を計測して結果を返します。
次の項目では、可変引数テンプレートを使用する際のエラーハンドリングとデバッグの方法について解説します。
エラーハンドリングとデバッグ
可変引数テンプレートを使用する際のエラーハンドリングとデバッグの方法について解説します。これらの手法を理解しておくことで、可変引数テンプレートを効果的に利用し、バグを未然に防ぐことができます。
コンパイル時のエラーチェック
テンプレートはコンパイル時に展開されるため、エラーチェックもコンパイル時に行われます。これにより、実行前にエラーを検出できるメリットがあります。以下に、コンパイル時にエラーチェックを行う例を示します。
#include <iostream>
#include <type_traits>
// 可変引数テンプレート関数
template<typename T, typename... Args>
void checkTypes(T first, Args... args) {
static_assert((std::is_same<T, Args>::value && ...), "All types must be the same");
// 処理内容
}
int main() {
checkTypes(1, 2, 3); // コンパイル成功
// checkTypes(1, 2.5, 3); // コンパイルエラー: All types must be the same
return 0;
}
この例では、static_assert
を使用して、すべての引数が同じ型であることをコンパイル時にチェックしています。型が異なる場合、コンパイルエラーが発生します。
実行時のエラーハンドリング
実行時にエラーが発生した場合に備えて、エラーハンドリングを行う方法を紹介します。以下に、可変引数を持つ関数で例外処理を行う例を示します。
#include <iostream>
#include <stdexcept>
// 可変引数テンプレート関数
template<typename T, typename... Args>
void printWithErrorHandling(T first, Args... args) {
try {
if constexpr (sizeof...(args) > 0) {
if (first < 0) {
throw std::runtime_error("Negative value detected");
}
std::cout << first << " ";
printWithErrorHandling(args...);
} else {
if (first < 0) {
throw std::runtime_error("Negative value detected");
}
std::cout << first << std::endl;
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
}
int main() {
printWithErrorHandling(1, 2, 3); // 出力: 1 2 3
printWithErrorHandling(1, -2, 3); // 出力: 1 Error: Negative value detected
return 0;
}
この例では、負の値が検出された場合に例外をスローし、それをキャッチしてエラーメッセージを表示します。
デバッグ用のメタプログラミング
テンプレートメタプログラミングを使用して、デバッグ情報をコンパイル時に生成することも可能です。以下に、その例を示します。
#include <iostream>
#include <type_traits>
// デバッグ用のメタ関数
template<typename T>
struct TypeInfo;
template<>
struct TypeInfo<int> {
static void print() { std::cout << "Type: int" << std::endl; }
};
template<>
struct TypeInfo<double> {
static void print() { std::cout << "Type: double" << std::endl; }
};
template<>
struct TypeInfo<const char*> {
static void print() { std::cout << "Type: const char*" << std::endl; }
};
// 可変引数テンプレート関数
template<typename T, typename... Args>
void debugTypes(T first, Args... args) {
TypeInfo<T>::print();
if constexpr (sizeof...(args) > 0) {
debugTypes(args...);
}
}
int main() {
debugTypes(1, 2.5, "Hello");
// 出力:
// Type: int
// Type: double
// Type: const char*
return 0;
}
この例では、各引数の型情報をコンパイル時に特定し、デバッグ用の情報を出力します。
次の項目では、可変引数テンプレートを用いたロギングシステムの実装例を紹介します。
応用例: ロギングシステムの実装
可変引数テンプレートを用いたロギングシステムの実装例を紹介します。ロギングシステムは、アプリケーションの動作を記録し、デバッグやモニタリングに役立てるための重要なツールです。ここでは、シンプルかつ柔軟なロギングシステムを実装します。
基本的なロガークラス
まずは、基本的なロガークラスを定義します。このクラスは、可変引数を受け取り、それらをログメッセージとして出力します。
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
// ロガークラス
class Logger {
public:
enum class LogLevel {
INFO,
WARNING,
ERROR
};
Logger(const std::string& filename) : logfile(filename, std::ios::app) {
if (!logfile.is_open()) {
throw std::runtime_error("Failed to open log file");
}
}
~Logger() {
if (logfile.is_open()) {
logfile.close();
}
}
template<typename... Args>
void log(LogLevel level, Args... args) {
std::ostringstream oss;
(oss << ... << args); // フォールド式でメッセージを作成
logfile << "[" << getLogLevelString(level) << "] " << oss.str() << std::endl;
}
private:
std::ofstream logfile;
std::string getLogLevelString(LogLevel level) const {
switch (level) {
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
};
int main() {
try {
Logger logger("app.log");
logger.log(Logger::LogLevel::INFO, "Application started");
logger.log(Logger::LogLevel::WARNING, "Low memory");
logger.log(Logger::LogLevel::ERROR, "An error occurred: ", 404, " Not Found");
} catch (const std::exception& e) {
std::cerr << "Logger exception: " << e.what() << std::endl;
}
return 0;
}
この例では、Logger
クラスが任意の数の引数を受け取り、それらを一つのログメッセージとしてファイルに出力します。log
メソッドは、ログレベル(INFO, WARNING, ERROR)を指定し、メッセージを構築して出力します。
コンソールおよびファイルへのロギング
次に、ログをコンソールにも出力する機能を追加します。
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
// ロガークラス
class Logger {
public:
enum class LogLevel {
INFO,
WARNING,
ERROR
};
Logger(const std::string& filename) : logfile(filename, std::ios::app) {
if (!logfile.is_open()) {
throw std::runtime_error("Failed to open log file");
}
}
~Logger() {
if (logfile.is_open()) {
logfile.close();
}
}
template<typename... Args>
void log(LogLevel level, Args... args) {
std::ostringstream oss;
(oss << ... << args); // フォールド式でメッセージを作成
std::string logMessage = "[" + getLogLevelString(level) + "] " + oss.str();
logfile << logMessage << std::endl;
std::cout << logMessage << std::endl; // コンソールにも出力
}
private:
std::ofstream logfile;
std::string getLogLevelString(LogLevel level) const {
switch (level) {
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARNING";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
};
int main() {
try {
Logger logger("app.log");
logger.log(Logger::LogLevel::INFO, "Application started");
logger.log(Logger::LogLevel::WARNING, "Low memory");
logger.log(Logger::LogLevel::ERROR, "An error occurred: ", 404, " Not Found");
} catch (const std::exception& e) {
std::cerr << "Logger exception: " << e.what() << std::endl;
}
return 0;
}
この改良版のロガークラスでは、ログメッセージがファイルに書き込まれるだけでなく、コンソールにも出力されます。これにより、リアルタイムでログを監視することができます。
次の項目では、理解を深めるための演習問題を提供します。
演習問題
可変引数テンプレートの理解を深めるために、いくつかの演習問題を提供します。これらの問題に取り組むことで、可変引数テンプレートの実用的な使い方を学ぶことができます。
演習1: 可変引数テンプレート関数の作成
与えられた引数の合計を計算する可変引数テンプレート関数 sum
を作成してください。関数は以下の仕様を満たす必要があります。
template<typename... Args>
auto sum(Args... args) -> decltype((args + ...)) {
// 関数の実装
}
int main() {
std::cout << sum(1, 2, 3) << std::endl; // 出力: 6
std::cout << sum(1.5, 2.5, 3.0) << std::endl; // 出力: 7.0
return 0;
}
演習2: コンテナの要素を逆順に出力する関数
任意の数のコンテナを受け取り、それぞれのコンテナの要素を逆順に出力する関数 printReversedContainers
を作成してください。関数は以下の仕様を満たす必要があります。
#include <iostream>
#include <vector>
#include <list>
#include <algorithm>
// コンテナを逆順に出力する関数
template<typename Container>
void printReversed(const Container& container) {
for (auto it = container.rbegin(); it != container.rend(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
}
template<typename Container, typename... Containers>
void printReversedContainers(const Container& first, const Containers&... rest) {
printReversed(first);
printReversedContainers(rest...);
}
void printReversedContainers() {}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<std::string> lst = {"Hello", "World"};
std::vector<double> vec2 = {3.14, 2.71, 1.41};
printReversedContainers(vec, lst, vec2);
return 0;
}
演習3: 型情報を出力する関数
任意の数の引数を受け取り、それぞれの引数の型情報を出力する関数 printTypeInfo
を作成してください。typeid
演算子を使用して型情報を取得し、std::type_info
オブジェクトの name
メソッドを使用して型名を出力します。
#include <iostream>
#include <typeinfo>
// 型情報を出力する関数
template<typename T>
void printTypeInfo(const T& value) {
std::cout << "Type: " << typeid(value).name() << ", Value: " << value << std::endl;
}
template<typename T, typename... Args>
void printTypeInfo(const T& first, const Args&... rest) {
printTypeInfo(first);
printTypeInfo(rest...);
}
int main() {
int a = 5;
double b = 3.14;
const char* c = "Hello";
printTypeInfo(a, b, c);
// 出力:
// Type: int, Value: 5
// Type: double, Value: 3.14
// Type: PKc, Value: Hello
return 0;
}
これらの演習問題に取り組むことで、可変引数テンプレートの使い方を実践的に学び、理解を深めることができます。
次の項目では、本記事のまとめを行います。
まとめ
本記事では、C++の可変引数テンプレートについて、基本概念から具体的な実装例、さらには応用例までを詳しく解説しました。可変引数テンプレートは、柔軟性と再利用性を備えた強力なツールであり、さまざまなシナリオで有効に活用できます。具体例や演習問題を通じて、実際のコードに適用する方法を学びました。これにより、C++プログラムの効率と品質を向上させることができます。今後の開発において、ぜひ可変引数テンプレートを活用してみてください。
コメント