私はstreamが嫌いである。scanf、printf系の関数を使うほうがよほど分かりやすく間違いも生じにくいと勝手に思っている。特にiomanipを使ったフォーマットは最悪だ。あんな記述コストが大きくて分かりにくい仕組みをどこの阿呆が考えやがったのか。
C++20からは標準ライブラリにformatが加わるものの、これはprintf系とは振る舞いが異なる。そもそも私は開発中のライブラリでC++14以上をサポートするようにしているので、C++20の機能を導入することはできないし、MSVC2019時点ではそもそもC++20は部分的にしかサポートされておらず、その中にformatは含まれていない。これの実装は既にGitHubでfmtという名前で公開されており広く使われているのだが、外部公開するためのライブラリ開発を行っている私にとって余計な依存関係を増やすのは好ましくない。
とはいえそれでも時と場合によってはstreamを使わざるを得ない状況というのも生じる。例えば私が作っているとあるライブラリは、標準出力をリダイレクトして別のところに書き出したいという要望があり、rdbuf関数で出力先を書き換えられるように標準出力は全てcout、cerrが使われている。
streamのフォーマットは不便極まりないので、可能であればprintf-likeなフォーマット関数があるとよいのだが、これがなかなか悩ましい。
不完全な方法。
例えば次のように、sprintfやsnprintfを使って整形されたstd::stringを出力させる方法はあるが、これは不完全だ。文字数制限が生じてしまう。バッファを長めに確保しておけばよっぽど問題にはならないが、気持ちが悪い。
template <class ...Args> std::string Format(const std::string& fmt, Args ...args) { char buf[1024];//1024文字を超えるとバッファオーバーフロー。snprintfであっても文字数制限は生じる。 sprintf(buf, fmp.c_str(), args...); return std::string(buf); }
気持ち悪いが完全な方法。
snprintf関数はその戻り値として格納された文字数を返す。なので文字数の判定のために一度、実際の文字列の格納のためにもう一度、都合二度snprintf関数を呼び出すことで、上の文字数制限を突破することはできる。ただ、これのパフォーマンスがどうのこうのよりも、この実装そのものが気持ち悪い。snprintfを二度呼び出すのがもう蕁麻疹が出そうなほどに気持ち悪いのだ。
template <class ...Args> std::string Format(const std::string& fmt, Args ...args) { std::size_t len = snprintf(nullptr, 0, fmt.c_str(), args ...); std::vector<char> buf(len + 1); snprintf(buf.data(), len + 1, fmt.c_str(), args ...); return std::string(buf.data()); }
全部自作する方法。シングルヘッダーライブラリ。
さすがに腹立たしいので、私は色々と悩んだ末、printfと近い動作をするFormat関数を作成しシングルヘッダー化して使っている。ソースコードはこちら。私がGitHubで公開しているGnuplot用ライブラリに同梱されているので、そちらへのリンクだ。
これはostringstreamによってprintfのような振る舞いを再現したものである。以下のように使うことができる。
std::string str = adapt::Format("%03d %5.3lf %s", 12, 3.4, "5678"); //std::cout << adapt::Format("%03d %5.3lf %s", 12, 3.4, "5678");戻り値はstringなので、coutに直接出力してもよい。
ある程度printfと同様の動作をするが、部分的に未実装の機能があったり、仕様の問題なのか厳密に再現できないところもあった。ただ折角自作するのならと、少しばかりprintfにない機能を追加したりもしている(%sがstd::stringに対応していたり、bool値をtrue/falseで表示できたり、万能の長さ修飾子"?"を使えたり)。代用品としてはそこそこ使えるとは思う。実行速度?知らん。