というわけで(前回参照)、文字参照を奇麗に書き表す方法を考えることにする。
といっても、まぁ、「別に奇麗に書ける必要は無い」のである、正直。
速度を追求するのであれば、最後に出したような一文字ずつ switch
する方法で大丈夫であるし、
もし速度なんて気にしないのであれば、一番最初の例のように replace_all
を使えばよい話。
ではあるのだが、replace_all
の場合、同じ文字列に対して三回も頭から置換処理をするのは正直あまりにも勿体ないし、
かといって switch
だと余りに低級すぎるし、それに main()
の中身が増大して、少し困る訳だ。
無論、後者の「 main()
の中身が増大する」問題に関しては、別関数に切り出す事で、対処できる:
#include <iostream> #include <string> #include <boost/xpressive/xpressive.hpp> using namespace std; using namespace boost::xpressive; string char_ref( char c ) { switch(c) { case '&': return "&"; case '<': return "<"; case '>': return ">" default: return string( 1, c ); } // switch } int main() { ostringstream buf; char c; while( cin.get(c) ) { buf << char_ref(c); } // コメントの定義は略 cout << regex_replace( buf.str(), comment, string("<span class=comment>$&</span>") ); }
こんな感じだ。前に比べ、ずいぶんスッキリした感じである。
・・・が。個人的には、まだまだ気に食わない訳だ。理由は、char_ref( char c )
である。
この関数、なんと戻り値として string オブジェクトなんてものを使っているではないか!
確かに、C++における生成と破棄のコストは、他のオブジェクト指向言語より遥かに低コストなのだが・・・こんな感じに、一文字ずつ余計なオブジェクトを生成していたのでは、いくら何でも馬鹿にならない。
となると、考えられる対応策としては、引数に ostream を渡して、
// 前略 ostream& char_ref( ostream& os, char c ) { switch(c) { case '&': return os << "&"; case '<': return os << "<"; case '>': return os << ">" default: return os << c; } // switch } int main() { ostringstream buf; char c; while( cin.get(c) ) { char_ref( buf, c ); } // 以下略 }
こんな感じにする事である。ちなみに戻り値が ostream&
なのは、単に returrn os << ...
って書きたかったからで、深い意味は無い。
この方法だとコストも低いし、可読性も大幅に上がっているし、良い事ずくめ、なのだが・・・
個人的に、気に食わないのである。
というのも、char_ref( buf, c );
は、なんか読みにくいのだ。
少なくとも僕にとっては、最初の buf << char_ref(c);
の方が、ずっと読みやすい。
なにしろ、こっちは「文字を変換してストリームに流している」という様子が分かりやすいし、それに、その後に他の出力を続ける事も出来るのだ(まぁ今回は文字レベルだから、そんなことは無いだろうが)。
かといって、そのように書こうとすると、今度は戻り値の型に困ってしまう。
string はコスト的に駄目駄目だし、const char* では char から生成された文字列を上手く扱えないし、こういう時のために作ったはずの <gintenlib/output_element.hpp> も、コスト面では駄目駄目な始末。
と、こういう時に、ふと思う訳である。
「標準ライブラリの引数なしマニピュレータみたいに、ストリーム出力を関数呼び出しに書き換えるシステムがあれば、全て解決なんじゃない?」
ふむ。確かに。もしそんな事が出来れば「無くてもいいけど、有るとちょっと嬉しい」システムとして重宝できるだろう。
そして、そういうシステムを作る事こそ、 銀天ライブラリ の作者たる僕の本懐なのだ。
というわけで、前置きが長くなったが、早速、今回作ったものをご覧いただきたい。
#include <gintenlib/output_element.hpp> namespace gintenlib { namespace output { // 実装クラス。メンバが全部参照なので、本当は変数に保存できないようにするべき。 // 今回は適当に作った都合上、特に対策はしていない。 template<typename Func, typename Arg1> struct functional_impl_1_ : element_base< functional_impl_1_<Func, Arg1> > { functional_impl_1_( const Func& func, const Arg1& arg1) : func_(func), arg1_(arg1) { } const Func& func_; const Arg1& arg1_; template<typename Stream> Stream& operator()( Stream& os ) const { return func_( os, arg1_ ); } }; // 同様に、functional_impl_2_< Func, Arg1, Arg2 >, etc... と作っていく // 今回は時間もないので、一変数版だけ template<typename Func> struct functional { explicit functional( const Func& src ) : func(src) { } const Func func; template<typename Arg1> functional_impl_1_<Func, Arg1> operator()( const Arg1& arg1 ) const { return functional_impl_1_<Func, Arg1>( func, arg1 ); } // 同様に operator()( const Arg1&, const Arg2& ) const 等を作る。 // そうすれば、任意引数に対応して対象の関数(オブジェクト)へ出力処理を転送できる }; // functional } // namespace output } // namespace gintenlib // 普通の関数に対し、いちいち 型を指定して functional を作るのは面倒。 // そういう機械的な作業は機械にやってもらおう、というわけで、 // 適当に定義された functional を自動生成するマクロを用意。 #define GINTENLIB_OUTPUT_FUNCTION( ret, function, arglist ) \ inline ret function##_impl_ arglist; \ namespace \ { \ typedef ret (*function##_impl_t_)arglist; \ const gintenlib::output::functional<function##_impl_t_> \ function( &function##_impl_ ); \ } \ \ inline ret function##_impl_ arglist // 実際に使う場合は、cout << function( arglist から第一引数を取り除いたリスト ); // こんな感じで出力すると、自動的に第一引数に出力対象のストリームが格納される。 // 注意すべきは、引数として const でない参照を指定するとコンパイルが通らない(値なら大丈夫) #include <iostream> using namespace std; // これが、実際に使ってみた例である。 // このように、マクロに ( 戻り値の型, 関数名, (仮引数リスト) )と指定してから、 // 何事も無かったかのように関数の本体を書き始めれば良い GINTENLIB_OUTPUT_FUNCTION( ostream&, char_ref, ( ostream& os, char c ) ) { switch(c) { case '&': return os << "&"; case '<': return os << "<"; case '>': return os << ">"; default: return os << c; } } // 試しに使ってみる int main() { char c; while( cin.get(c) ) { // 小細工なしの char_ref( cout, c ) より、ずっと見やすい! cout << char_ref(c); } return 0; }
以上である。なお、スペースの都合上、コメントのタグ付けは省略させていただいた。
見れば分かる通り、やってることは非常に単純である。
関数へのポインタを保持しておいて、operator()
が適用されたら 引数への参照を保持した隠しオブジェクトを生成、そのオブジェクトがストリームに流されるタイミングで、保持しておいた引数を関数ポインタへと適用する。
ポイントは、全て参照を使っている点だ。こうすることにより、無駄なコピーは一切生まれない・・・つまり、一時オブジェクトをいちいち作ってストリームに流すという動作の割には、かなりコスト的には低い、はずである。
無論、参照を使うからには有効期限の問題がつきまとう訳で、例えば gintenlib::output_element
(ostream出力可能なものを何でも格納できるクラス)に入れたりするとヤバいのであるが、これはトリックによって幾らでも回避できるので問題ない(今回は特に回避していないが)。
また、const
ではない参照を取る関数に対して上手く適応できないという問題点があるのだが、これは仕方ないというしか無い。そもそも出力関数が出力対象のオブジェクトを書き換えるなどということは、本来起こってはいけないはずである。そんな妙な関数に対処して安全性を失うのは、本末転倒なのだ。
あ、ちなみに出力は、そうは見えないかもしれませんが functional_impl_1_::operator()
の呼び出し時点で行っています。operator <<
は定義されていないように見えますが、ちゃんと element_base
の方で定義されているので、ご安心を。