最終更新日時:
が更新

履歴 編集

Type-safe 'printf-like' format class

Choices made

"Le pourquoi du comment" ( - "どうしてそうなの?")

The syntax of the format-string

format は新しいライブラリだ。そのゴールの一つは、 printf の代替物を提供することにある。つまり、 formatprintf 用に設計された書式文字列を構文解析することができて、与えられた引数にその書式を適用して printf と同じ結果を生成できる。

この制限の下で、書式文字列の文法には大雑把に3つの選択肢が有り得た :

  1. printf とまったく同じ文法を用いる。これは多くの経験のあるユーザに知られているし、 ほとんどすべてのニーズにフィットする。しかし命令の終端を断定するために不可欠な型変換文字は、 C++ ストリームの文脈では、 ストリームの関連する書式化オプションをセットする程度の役にしか立たない(%x なら hexa をセットする、等...) このお仕着せの型変換文字は、意味付けを変更した上で、省略可能にするのが良いだろう。
  2. 互換性を維持しながら拡張された printf 文法。まだ printf の文法として有効でない文字や構造を用いる。例. : "%1%", "%[1]", "%|1$d|", .. 始端 / 終端マークを用いることで、あらゆる種類の拡張を考慮できるようになる。
  3. printf 互換のものと平行して、非レガシーモードを提供する。 既存の printf 文法との互換性という制約を受けずに、他の目的に適すように設計できる。
    • しかし printf の文法の代替物(既存のものより明確に優れていて、かつパワフルなものになるだろう)の設計は、 format クラスの構築とはまた別の仕事だ。 そのような文法が設計されたときには、 Boost.Format を二つのライブラリに分割することも考慮すべき だろう : 一方はこの新しい文法と歩調を合わせて開発され、もう一つはレガシーな文法を サポートする (おそらくは高速で、 snprintf やその同類に勝る安全面での改良が組み込まれたバージョンになるだろう)。

完全で、気の利いた、 printf よりも明確に C++ ストリームに適応した新しい文法が手元にないので、二つ目のアプローチを選択することにした。 Boost.Format は printf の文法を用い、その文法を拡張することで拡張機能(桁送り、中寄せ)を表現することができる。

また、 printf の文法の弱点を克服するために、これまでのものに替わる互換表記を提供する :

  • "%N%" より単純な位置指定、型指定無し、オプション無しの表記。
  • %|spec| printf の命令を視覚的により明確な構造に密閉する一手段であり、 同時に printf の'型変換文字'を省略可能にする。

なぜ関数呼び出しではなく演算子で引数を渡すのですか?

演算子による方法の不便さ(一部の人にとって)は、混乱させられることがあるということだ。 演算子をオーバーロードし過ぎると人々を真の混乱に陥れるという お決まりの警告だ。

format オブジェクトの仕様は限られた文脈(最も多いのは "cout << " の直後)になるだろうってことと、 引数がいかにも書式文字列に続いているように見えることから :

format(" %s at %s  with %s\n") % x % y % z;

人々をそれほど混乱させないだろうと期待できる。

演算子の別の恐怖は優先順位の問題だ。 format("%s") % (x+y) と書かずに format("%s") % x+y を書いた場合どうなるだろう?

これだとコンパイル時に問題が起きるので、エラーはすぐに検出されるだろう。

もちろん、この行は tmp = operator%( format("%s"), x) を呼び、それから operator+(tmp, y) を呼ぶ。

暗黙の変換が定義されていない限り tmpformat オブジェクトとなるだろう。そのため operator+ の呼び出しは失敗する。 (もちろん、君がそんな演算子を定義した場合は除く)。 だから君は優先順位の間違いはコンパイルの際に知らされると安心して決め込んでいい。

その一方で、関数アプローチには本物の不便さがある。 多くのテンプレート関数を定義する必要があるんだ。こんな感じに :

template <class T1, class T2,  .., class TN> 
string format(string s,  const T1& x1, .... , const T1& xN);

そして N を 500 まで定義したとしても、 まだ C の printf にはない上限を設けることになる。

それに、 format はどうにかして printf をエミュレートできる場合もあるけど、 printf の完全な等価物には程遠い。根本的に異なる外見を用いる方がベストだ。そして演算子呼び出しを使うのは、その点ではとても成功している!

いずれにせよ、もし僕らが実際にフォーマルな関数呼び出しテンプレートの仕組みを選択していたら、

operator<< (stream, const T&)

が与えられているクラス T しか表示することができなかっただろう。 なぜなら、 const と 非 const の両方を許容すると組み合わせ爆発が生じるからだ - もし 10 個までの引数で行くにしても、 2^10 個の関数が必要になる。

(T&const T& のオーバーロードを提供することは C++ 標準の不備の最先端だが、おかげでサポートの保証からは程遠い。しかし現在ではいくつかのコンパイラがそうしたオーバーロードをサポートしている)

const 版の等価物しか提供しないという悪い設計をすることもできるけど、それはユーザにまた別の根拠の無い制限を押し付けることになる。

また、マニピュレータのいくつかは関数なので、 const な参照として渡すことができない。 そのため関数呼び出しアプローチはマニピュレータを上手くサポートしない。

結論として、コンパイル時に引数の数を知ることができない場合には、専用の二項演算子を用いることが最もシンプルで、ロバストで、かつ制限の少ない引数渡しのメカニズムなんだ。

なぜ 'with(..)' のようなメンバ関数でなく operator% なんですか??

技術的には、

format(fstr) % x1 % x2 % x3;

は、

format(fstr).with( x1 ).with( x2 ).with( x3 );

と同じ構造をしている。後者には優先順位の問題は何も無い。 後者のただ一つの欠点は、演算子を用いるのに比べて、一見してこの行が何をしているのか 把握しづらいということだ。 .with(..) を呼び出すのは、コードのほかの行でやっていることと同じように見える。 だから、好みの問題だけど、この方がより良いな解決方法だろう。 余計な文字を用いる点と、'with(..)' を用いたコードの行の全般的に散らかった側面は、僕に真の演算子を選択させるのに十分だった。

なぜいつもの operator<< でなく operator% なんですか??

  • なぜなら format オブジェクトに引数を渡すことは、ストリームに順に変数を送ること同じではないからだ。それに format オブジェクトはストリームでも、マニピュレータでもない。

    僕らは引数を渡すのに演算子を使う。 format は、関数が単純に引数を一つずつ取るようにそれを使うだろう。

    format オブジェクトはストリームのような振る舞いはしない。君がマニピュレータのように動作する format オブジェクトを実装しようとしてストリームを返すようにすれば、ユーザはストリームのマニピュレータと完璧に同じものだと信じることになる。そして遅かれ早かれ、そのユーザはこの視点のおかげで欺かれる。

    振る舞いの違いの最も明白な例は、

    cout << format("%s %s ") << x;
    cout << y ;  // うわぁ、 format は本当はストリームマニピュレータじゃないよ
    

  • % の優先順位は << よりも高い。 これは問題のように見える。なぜなら +- は括弧の内側にグループ化しなければならないからだ。一方で << にはそんな必要は無い。 しかしもしユーザがこのことを忘れても、誤りはコンパイルの際に捕らえられて、きっと彼は二度と忘れないだろう。

    その一方で、より高い優先順位は format の振る舞いをとても直観的にしてくれる。

    cout << format("%s %s ") % x % y << endl;
    

    は正確には次のように扱われる :

    cout << ( format("%s %s ") % x % y ) << endl;
    

    だから % を用いることで、 format オブジェクトの寿命が周囲のストリームの文脈を妨げることはない。 これはあり得る振る舞いの中で最も単純なものだ。そのためユーザは format オブジェクトの後でストリームを使いつづけることができる。

    << 演算子では、この状況では物事はより一層厄介だ。この行 :

    cout << format("%s %s ") <<  x  <<  y << endl;
    

    は次のように解釈される :

    ( ( ( cout << format("%s %s ") ) << x ) <<  y ) << endl;
    

    代替となる実装の中には << 演算子を選択しているものもあるが、これが働くようにする方法は一つしかない :

    最初の

    operator<<( ostream&, format const&)
    

    呼び出しは プロクシを返す。プロクシは最終的な出力先 (cout) と書式文字列の情報をカプセル化している。

    引数を渡している先が format なのか、それとも format の完了後の最終的な出力先なのかは区別できない。これは問題だ。

    僕はいくつか考え得る実装を試してみたけど、どれも完璧には希望に沿っていない。

    例えば : ユーザの誤りを捕らえるために、引数が多く渡されすぎたときに例外を発生するのは筋が通っている。 しかしこの文脈では、余分な引数が最終的な出力先に向けられていることはほとんど間違いない。 ここでいくつかの選択肢がある :

    • 引数が過剰かどうかの検出を諦めて、プロクシのテンプレートメンバ operator<< ( const T&) が単純にすべての余分な引数を cout に転送するようにする。
    • format の引数を特殊なマニピュレータ 'endf' で以下のように閉じるよう、ユーザに要求する。 :

      cout << format("%s %s ") <<  x  <<  y << endf << endl;
      

      endf はプロキシの内部に保持されていた最終的な出力先を返す関数として定義できる。 それで万事解決だ。 endf の後は、ユーザは再び cout に向けて << を呼んでいる。 - 中間的な解決方法もある。最も頻繁な使い方は、単にもう一つ多くのマニピュレータ (std::flushendl, ..) を cout へ出力したい場合だろう。

      cout << format("%s %s \n") <<  x  <<  y << flush ;
      

      だからその解決方法は operator<< をマニピュレータに対してオーバーロードすることだ。 この方法では endf は不要だが、マニピュレータ以外のものを format の引数の後に出力する事はできない。

    最も完全な解決方法は endf マニピュレータを使うものだ。 % 演算子を使う場合、この書式終端関数は不要だ。さらにどの引数が format オブジェクトの中へと向かい、どれがストリームへ向かうのかがすぐに分かる。 - 美しさの問題 : '%' は書式文字列の内部で使われているものと同じ文字だ。それぞれの引数を渡すのに同じ文字を使うというのはなかなか良い考えだろう。 '<<' は2文字、 '%' は1文字。 '%' はサイズの面でもより小さい。 見た目の面でも全般的に改善している (何がどうなっているのかが分かる) :

    cout << format("%s %s %s") %x %y %z << "And  avg is" << format("%s\n") %avg;
    

    これと次を比較すると :

    cout << format("%s %s %s") << x << y << z << endf <<"And avg is" << format("%s\n") << avg;
    

    "<<" は、ストリームに渡されているオブジェクトと同じレベルで引数を与えているから、間違いを起こしやすい。 - python も書式化に % を使っている。だから "聞いたことも無いような" ものじゃないって納得してくれるよね ;-)

なぜ operator()operator[] ではなく operator% なんですか??

operator() には、関数に引数を送る自然な方法であるというメリットがある。 また、 operator[] の意味が format で使うには上手く当てはまると考える人もいる。

技術的にはこれらは operator% と同じくらい良い選択だ。しかしすごく醜い。 (好みの問題だ)

それにそもそも、書式文字列の中の "%" で参照されている引数を operator% を使って渡すことは、それらの演算子を使うよりずっと自然に見える。


July 07, 2001

© Copyright Samuel Krempp 2001. Permission to copy, use, modify, sell and distribute this document is granted provided this copyright notice appears in all copies. This document is provided "as is" without express or implied warranty, and with no claim as to its suitability for any purpose.

Japanese Translation Copyright © 2003 Kent.N

オリジナルの、及びこの著作権表示が全ての複製の中に現れる限り、この文書の複製、利用、変更、販売そして配布を認める。このドキュメントは「あるがまま」に提供されており、いかなる明示的、暗黙的保証も行わない。また、いかなる目的に対しても、その利用が適していることを関知しない。