C++
にて開発を行っていると、Args
や...
、va_list
など、可変長引数に関してのコードに遭遇すると思いますが、個人的には仕様がわかりづらく、初見では頭を悩まされる事と思います。
本記事では、可変長引数について、できる限り優しく詳細に解説します。
可変長引数とは
そもそも可変長引数とは、その名の通り、宣言時に引数の数を定めなくてよいもののことです。
この仕組みを使用して工夫をすることで、汎用的な関数を作成できるなどのメリットがあります。
コード例
コード例1
まずは、C
スタイルの古い方法で可変長引数を作ってみたいと思います。
#include <cstdarg>//va_start, va_arg, va_endを使用するために必要
#include <iostream>
//-------------------------------------------------------------
// 可変長引数を受け取って、整数をすべて表示する関数
//! @param count 引数の個数
//! @param ... 可変長引数(整数)
//-------------------------------------------------------------
void printAll(int count, ...) {
va_list args; // 引数リストを扱うための変数
va_start(args, count); // 可変長引数の処理を開始(countの次の引数から)
for (int i = 0; i < count; ++i) {
int value = va_arg(args, int); // 次の引数をint型として取得
std::cout << value << " "; // 取得した値を表示
}
va_end(args); // 引数リストの処理を終了
std::cout << std::endl; // 改行
}
//メイン関数
int main() {
printAll(3, 10, 20, 30); // 出力: 10 20 30
//終了
return 0;
system("pause");
}
10 20 30
大前提として、可変長引数...
は、関数内で直接アクセスすることが不可能です。
そのため、va_list
型の変数を宣言し、間接的に可変長引数にアクセスを行うのです。
va_list args; // 引数リストを扱うための変数
宣言した時点ではva_list
に可変長引数が入っておらず、va_start(va_list型の変数,可変長引数の数)
とすることで、va_start()
の第一引数にとったva_list
型の変数を通して、可変長引数にアクセスすることが可能になります。
ここで可変長引数の数と、int型の値が一致していないと、バグが起こる可能性があります。
va_start(args, count); // 可変長引数の処理を開始(countの次の引数から)
可変長引数にアクセスする際にはva_arg(va_list型の変数,引き出す可変長引数の型)
とすることで、可変長引数に、左から順にアクセスすることができます。
引き出す可変長引数の型の値が一致していないと、バグが起こる可能性があります。
int value = va_arg(args, int); // 次の引数をint型として取得
この一連の処理は、内部的にメモリ領域を使用しているため、解放処理が必要となります。
va_end(va_list型の変数)
とすることで、お片づけをしています。
va_end()
を呼び出さないと、メモリリークやスタック破損など、深刻なバグが発生する場合があります。
va_end(args); // 引数リストの処理を終了
コード例2
次に、C++11
以降の機能を使った方法を紹介したいと思います。
テンプレートと再帰関数の仕組みを使用して、自作のprint関数を作成してみます。
#include <iostream>
//-------------------------------------------------------------
// 終了時の関数
//! @note 引数なしの関数をオーバーロード
//-------------------------------------------------------------
void print() {
std::cout << std::endl;// 改行のみ表示
}
//-------------------------------------------------------------
// 再帰的分解で一つずつ出力:最初の引数を処理し、残りを再帰的に渡す
//! @tparam T 最初の引数の型
//! @tparam Rest 残りの引数の型(0個以上)
//! @param first 最初の引数
//! @param rest 残りの引数(0個以上)
//-------------------------------------------------------------
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << " "; // 最初の引数を表示
print(rest...); // 残りの引数を再帰的に処理
}
//メイン関数
int main() {
print(1, 2.5, "Hello", 'A', 42);//自作のプリント関数で表示させてみる
//終了
return 0;
system("pause");
}
1 2.5 Hello A 42
この方法ではprint()
をテンプレートの再帰関数にし、可変長引数を一つづつ取り出しています。
一度の関数呼び出しで出力する引数は第一引数のみで、可変長引数の内容は、再帰処理の次に回すため、先ほどのva_start
のような、メモリ領域をいじるような処理が不要となります。
テンプレートで<typename... テンプレート引数の型名>
のように宣言することで、可変長引数をテンプレートの型として持つことが可能となります。
//-------------------------------------------------------------
// 再帰的分解で一つずつ出力:最初の引数を処理し、残りを再帰的に渡す
//! @tparam T 最初の引数の型
//! @tparam Rest 残りの引数の型(0個以上)
//! @param first 最初の引数
//! @param rest 残りの引数(0個以上)
//-------------------------------------------------------------
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first << " "; // 最初の引数を表示
print(rest...); // 残りの引数を再帰的に処理
}
処理を行うたびに可変長引数のが一つづつ減っていくため、当然再帰処理のどこかのタイミングで、可変長引数の数が0になるタイミングがあります。
その際に、再帰関数と同名で引数なしのオーバーロードした関数が必要となります。
//-------------------------------------------------------------
// 終了時の関数
//! @note 引数なしの関数をオーバーロード
//-------------------------------------------------------------
void print() {
std::cout << std::endl;// 改行のみ表示
}
コード例3
最後は、C++17
以降で使用可能な、フォールド式
という仕組みを使用した方法になります。
フォールド式
の構文が((expr op args), ...)
となり、args
の中身を、左から順に一括で処理を行います。
#include <iostream>
//-------------------------------------------------------------
// フォールド式を使った可変長引数テンプレートで自作のprint関数
//! @template Args 可変長引数
//! @param args [in] 可変長引数
//-------------------------------------------------------------
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // 左フォールド式
std::cout << std::endl; //改行
}
//メイン関数
int main() {
print(1, 2.5, "Hello", 'A', 42); //自作のプリント関数で表示させてみる
//終了
return 0;
system("pause");
}
1 2.5 Hello A 42
フォールド式
を使用することで、再帰処理などを行わずとも、一括での出力が可能です。
//-------------------------------------------------------------
// フォールド式を使った可変長引数テンプレートで自作のprint関数
//! @template Args 可変長引数
//! @param args [in] 可変長引数
//-------------------------------------------------------------
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...); // 左フォールド式
std::cout << std::endl; //改行
}
総括
- 自由に引数の数を決められる引数のことを可変長引数と呼ぶ。
- 可変長引数を工夫して利用することで、汎用性の高い関数を作成することが可能。
-
va_list
を使用することで、C言語
ライクな可変長引数の実装が可能。 -
C++11
からは、テンプレートと再帰処理を利用することで、va_list
など、メモリを触る処理を行わずとも、可変長引数の値にアクセスすることが可能。 -
C++17
からは、フォールド式を利用することで、再帰処理を使わずとも、一括で可変長引数の処理が可能。