可変長テンプレートを使った C++11 時代の可変長引数関数
C++ では printf など可変長の引数をとる関数を結構使うことはあります。
ただ、 C 言語由来である従来の可変長引数は多くの問題を抱えています。
そんな中、 C++ は C++11 仕様で大きな機能追加が行われました。
その一つである可変長テンプレート(Variadic Templates)によって、
可変長引数の扱いは大きく改善されました。
今回は C++11 を経た、新時代の可変長引数関数の書き方について解説したいと思います。
従来方式の問題点
最初に従来方式の問題点の確認です。サンプルコード : oldargs.cpp
整数値の合計
整数値の合計を求める関数を従来方式で書くと、次のようになります。int unsafe_isum(int argnum, ...) { va_list arg; int sum = 0; va_start(arg, argnum); // 引数の数だけ繰り返し for (int cnt = 0; cnt < argnum ; cnt++) { // int として処理 sum += va_arg(arg, int); } va_end(arg); return sum; }
// 使用例 printf("sum = %d\n", unsafe_isum(3, 1, 2, 3)); // 9"..." がなんでも受け取れる引数の指定で、 va_start(), va_end() で囲んだ中に処理を書き込みます。 この辺はオマジナイのようなものです。
一つ目の問題点は引数の数がわからないところです。
わからないので、最初の引数で数を指定して、引数の数だけ処理しています。 もし間違って数が少なければ全部処理しませんし、多ければ不定の動作となります。
二つ目の問題点は型が不明という点で、最も問題です。 va_arg() のところで整数が渡されたものとしています。 これはキャストではないため、もし 1.0 のような値を渡すと double を int として処理して変な値となります。
printf
もう一つ例として printf の実装をみてみます。void unsafe_printf(const char *fmt, ...) { va_list arg; va_start(arg, fmt); ::vprintf(fmt, arg); va_end(arg); }
unsafe_printf("Hello %s, %02d, %.3f\n", "world", 3, 1.2); // "Hello world, 03, 1.200"vprintf()というのは printf のような関数を自作するために用意されている関数で va_list を引数として渡します。
ここがもう一つ問題で、可変長引数を別の可変長引数の関数に渡せません。 va_list を渡す関数が用意されていればいいですが、 ライブラリーなどで可変長引数の関数だけ公開されていると それをラップするような可変長引数の関数を作ることができません。
もちろん、他の 2 つの問題も顕在です。
printf の場合、引数の数はフォーマットの文字列の % の数で判断します。
型は %s, %d などの指定で決めます。 文字列のところにうっかり整数を渡したりすると変なポインターアドレスとなってコアダンプします。 そういった経験をしたことがある人も多いのではないでしょうか。
問題点のまとめ
まとめると引数の主な問題点は 3 つです。- 数が分からない
- 型がわからない
- 可変長引数のまま関数に渡すことができない
例えば、 ポインターを受け取る引数に NULL ポインターのつもりで 0 を渡したとします。 たいてい整数は 32 ビットなのに対し、ポインター型は 64 ビットなので、 32 ビット分ゴミが入ります。 これがうまくいったり、いかなかったりする不具合になります。
わかりづらかったでしょうか。でも心配はいりません。 今後はこんな問題からオサラバできます。
対応と他の言語との比較
C++11 では、可変長テンプレートの関数テンプレートを使って、可変長引数の従来の問題に対応できるようになっています。ちなみにここでは紹介しませんが、可変長テンプレートにはクラステンプレートもあり、 こちらは tuple 型などを作るときに役立ちます。
この章では実際の書き方に入る前に、他の言語での対応と比較してみていきます。
動的型付け言語、 C++ 以外の静的型付け言語
他の言語ではどうやっているかというと "引数の可変長の部分を配列(リスト)として受け取って、ループで処理する" といったことが多いです。Ruby を始めとする動的型付けの言語ではオブジェクト自体が型情報を持っています。 そのため、 配列にはすべてのオブジェクトが格納可能です。
静的型付けでも C# のような言語では、すべてのクラスの基底クラス(Object)があるので、 Object の配列として受け取ることができます。 ただ、 Object として受け取るのは、問題がないわけではありません。
この残りの引数を配列に入れるやり方は動的型付けに向いたやり方です。 任意の型を配列に入れるといった利便性こそ動的型付けの魅力とも言えます。(その分、安全性、速度が犠牲になることもありますが) C++ は静的型付けです。向いていないどころか、共通基底クラスもないため、配列で受け取ってループで回すということが自体ができません。 そこで、新しい C++ ではどういった解決方法をとったかというと ループではなく、再帰を使います。
関数型言語
可変長テンプレートによる書き方は、関数型言語を知っている人であれば、 「パターンマッチと似てるな」と感じると思います。関数型には変数を変更できないという縛りがあるため、 こちらもループが回せないという共通点があります。
また、再帰というとスタックがたまるので、 メモリーを心配される人もいるかと思います。
しかし、テンプレートなので、コンパイル時にはすべて展開されます。 現状、各コンパイラーの対応がどうなっているかはよく知りませんが、 これはコンパイラーが頑張れば解決できる問題だと思います。
ちなみに関数型言語では、末尾再帰最適化といって、 再帰をループとして最適化する機能が言語自体に備わっていることが多いです。
可変長テンプレート関数
前置きが終わったので、これから可変長テンプレート関数の使い方について説明していきます。サンプルコード : sum.cpp
整数の合計
サンプルは整数値を合計する関数にしました。// 末端 isum inline int isum() { return 0; } // 再帰呼び出し isum template<typename First, typename... Rest> int isum(const First& first, const Rest&... rest) { return first + isum(rest...); }
cout << isum(1, 2, 3, 4) << endl; // 10テンプレート引数 Rest や 関数の引数 rest の前についている ... を pack 演算子 といいます。 これが可変長引数の残りを意味していて、 rest の変数にそれがまとめられています。
サンプルで一回目に呼ばれた状況では
rest = 2, 3, 4
です。
pack された変数に対してできる操作は一つだけです。 それが rest の後につけている ... で unpack 演算子 と呼び、 まとめた変数を展開(unpack)して元に戻します。
一回目では rest... => 2, 3, 4 と展開され、これを isum(first, ...rest) 関数に渡しています。
return 1 + isum(2, 3, 4);これが再帰的に繰り返されます。
isum(1, 2, 3, 4) // first = 1, rest = { 2, 3, 4 } 1 + isum(2, 3, 4) // first = 2, rest = { 3, 4 } 1 + 2 + isum(3, 4) // first = 3, rest = { 4 } 1 + 2 + 3 + isum(4) // first = 4, rest = {} 1 + 2 + 3 + 4 + isum() // 末端関数の呼び出し"..." として 0 から任意の個数の引数が受け取れます。 そのため、ここが空になるまで、繰り返し用の isum が呼び出されます。
その後、次の流れになります。
- 空になった rest を展開(rest...) → isum()
- 引数なしの isum() の呼び出し
- 先頭の引数がないため、もう再帰呼び出しの isum に合致しない
- 繰り返しが終了
- isum() の中では再帰呼び出しをしていない
この可変長テンプレート引数を使った方法では従来の問題点が解消されています。
- 引数の数を指定する必要がない
- double の値を渡してもエラーではなく、 C++ の変換ルール(少数点以下切り捨て)で変換
cout << isum(1.0, 2.2, 3.3, 4.9) << endl; // 10 // = 1.0 + 2 + 3 + 4 + isum()
任意の型の合計
参考として任意の型の合計用の関数もあげておきます。template<typename Ret> Ret any_sum() { return Ret(); } template<typename Ret, typename First, typename... Rest> Ret any_sum(const First& first, const Rest&... rest) { return first + any_sum<Ret>(rest...); }使う場合、戻り値の型(Ret)は引数から判断できないので、指定する必要があります。
cout << any_sum<int>(1.0, 2.2, 3.3, 4.9) << endl; // 10 cout << any_sum<double>(1.0, 2, 3.3, 4.9) << endl; // 11.2 cout << any_sum<string>("Hello", " ", "world", "!") << endl; // "Hello world!" // cout << any_sum<string>("Hello ", "world", 3) << endl; // コンパイルエラーテンプレートはダックタイピングなので + 演算子とデフォルトコンストラクターさえあれば string 型などにも使えます。 ただし、 string と整数の + など解決できない演算がある場合にはコンパイルエラーです。
型安全な printf
この可変長テンプレートを使えば、型に対して安全な printf が作成できます。可変長テンプレートを使った printf は Wikipedia でも紹介されています。 ただ、 printf を 0 から作ると大変なので、 今回は boost::format を使って作ってみます。
Boost Format
Boost Format は危険な printf の解決策として作られたもので、 % 演算子のオーバーロードを使っています。format.cpp :
boost::format fmt("Hello %s, %02d, %.3f"); fmt = fmt % "world" % 3 % 1.2; std::cout << fmt << std::endl; // Hello world, 03, 1.200より詳しい使い方については以下の記事を見られると良いと思います。
簡易 printf
boost::format を使って printf を作ると次のようになります。safe_printf.cpp :
inline void safe_printf(boost::format &bfmt) { std::cout << bfmt; } template<typename First, typename... Rest> void safe_printf(boost::format &bfmt, const First& first, const Rest&... rest) { bfmt = bfmt % first; safe_printf(bfmt, rest...); }
boost::format fmt("Hello %s, %02d, %.3f\n"); safe_printf(fmt, "world", 3, 1.2); // Hello world, 03, 1.200 safe_printf("Hello %s, %02d, %.3f\n", "world", 3, 1.2); // Hello world, 03, 1.200可変長部分ではない第一引数がある以外は合計の例と同じです。
様々な printf 系関数への展開
従来方式の問題点 2 つが解決されていることについては isum のところで説明しました。 残り 1 つの問題点は可変長引数のまま他の可変長引数に渡せないことでした。これは、すでに少し出ていますが、 unpack 演算子を使って解決できます。
printf には sprintf, fprintf のような類似の関数がありますが、 この可変長引数関数から可変長引数関数の呼び出しを利用して作ることができます。
safe_printfs.cpp
まず、ベースとなる関数を作ります。
inline void safe_format(boost::format &fmt) { } template<typename First, typename... Rest> void safe_format(boost::format &fmt, const First& first, const Rest&... rest) { fmt = fmt % first; safe_format(fmt, rest...); }これを使って printf 系の関数を実装します。
// 安全版 sprintf template<typename... Rest> std::string safe_sprintf(const char *fmt, const Rest&... rest) { boost::format bfmt(fmt); safe_format(bfmt, rest...); return boost::str(bfmt); } // 安全版 fprintf template<typename... Rest> void safe_fprintf(std::ostream &out, const char *fmt, const Rest&... rest) { boost::format bfmt(fmt); safe_format(bfmt, rest...); out << bfmt; } // 安全版 printf template<typename... Rest> void safe_printf(const char *fmt, const Rest&... rest) { safe_fprintf(std::cout, fmt, rest...); }
const char *fmt = "Hello %s, %02d, %.3f\n"; std::cout << safe_sprintf(fmt, "world", 3, 1.2); safe_fprintf(std::cout, fmt, "world", 3, 1.2); safe_printf(fmt, "world", 3, 1.2);
文字列の結合
途中紹介した文字列の合計では整数を渡すとエラーでした。 このままだと気持ち悪いので、 最後に「任意の型を渡して文字列に変換して結合する関数」の例も紹介します。(サンプルは sum.cpp 内に記述しています)
inline void concat_internal(std::stringstream &sout) { } template<typename First, typename... Rest> void concat_internal(std::stringstream &sout, const First& first, const Rest&... rest) { sout << first; concat_internal(sout, rest...); } template<typename... Args> std::string concat(const Args&... args) { std::stringstream sout; concat_internal(sout, args...); return sout.str(); }
cout << concat("Hello ", "world ", 3, " ", 1.2) << endl; // "Hello world 3 1.2"stringstream を使って文字列に変換しています。 実は boost::format の内部でも stream を利用しています。
これらは ostream の出力を実装していれば、自作のクラスでも使うことができます。
参考
メソッド連鎖(Method chaining)パターン
あまり知られていないかもしれませんが、メソッド連鎖(Method chaining) というデザインパターンがあります。
これは別名 名前付きパラメーターイディオム(named parameter idiom) とも呼ばれ、
パターンというほど大したものではありませんが、個人的には好きな書き方です。
そこで今回はそのメソッド連鎖パターンについて紹介したいと思います。
メソッド連鎖パターンではオブジェクト指向言語で使われるパターンです。 ここでは C++ のサンプルで説明します。
セッターを書く場合、通常、戻り値を
void
にして、何も返さないことが多いと思います。
メソッド連鎖パターンでは、そこで自身の参照を返します。
method_chaining.cpp :
class Person { std::string m_name; int m_age; public: Person() :m_age(0) {} // Getter const std::string &name() const { return m_name; } int age() const { return m_age; } // Setter Person &name(const std::string &name) { m_name = name; return *this; } Person &age(int age) { m_age = age; return *this; } };サンプルではセッターを名詞として命名していますが、 SetXxxx 形式でもかまいません。
自身の参照を返すセッターを作ると何が嬉しいかというと、セッターのメソッドをつなげて書くことができるようになります。
Person person; cout << person.name("Peter").age(21) << endl; // {"Peter"(21)}メソッドを連鎖させて書くとちょっと見やすくなります。
逆にいうと、ただそれだけです。 好みが分かれるところですし、大したメリットもありませんが、 必要なことも
*this
返すだけです。
ちょっとやっておくのもいいのではないでしょうか。
今回、 C++ のサンプルで紹介しましたが、他の言語のサンプルもたくさんあります。
- Method chaining - Wikipedia, the free encyclopedia
- Method Chaining Design Pattern in C# and JavaScript | Prasad Honrao
person.age = 21
などと書くこと多い言語で、わざわざやるほどのことでもないかなとは思います。