Boost.LexicalCastの改良案

なんかついったーで boost::lexical_cast について盛り上がったので、日記のネタにすることにします:

http://togetter.com/li/2178


lexical_cast を知らない方の為に少し説明すると、こいつは「文字列表現を媒介にしたキャスト」であり、例えば

int i = boost::lexical_cast<int>("23");
std::string s = boost::lexical_cast<std::string>(42);

のように書けば、i には数値 23 が、 s には文字列 "42" がそれぞれ格納される、というものです。
この例では int を使いましたが、もちろん int じゃなくても大丈夫で、 doublestd::complex といったものから、ボクの考えた文字列クラス oreore::my_string とかいったものでも普通に変換できます。
その変換可能条件は、ストリームに対する入出力演算が定義されていること。 operator<< が定義されていれば lexical_cast<Target>(src) のキャスト元 src として、 operator>> が定義されていればキャスト先 Target として、それぞれ利用することが出来ます。
これは boost::lexical_cast の実装が、大まかに言って

template<typename Target, typename Source>
Target lexical_cast( const Source& s )
{
  std::stringstream ss;
  Target t;
  ss << s; ss >> t;
  return t;
}

という感じになっている(実際には変換失敗時に例外を投げたり wchar_t に対応したり いろいろと最適化されていたりと、もっと複雑ですが)為。
まー要するに、文字列に/へ変換したい場合にはとりあえず lexical_cast を使えばよい、ということであり、なんつーかBoostらしい優秀なライブラリなのです・・・が、多少の不満点もあったりするわけです。


そこで今日の随筆では、その(重箱の隅をつつくような)不満点を少し挙げてみようかなぁと思います。
最後までお読みいただければ幸い。


不満点1: 遅い?

とりあえず、以下のコードを実行してみてください(@mickey24thx!): http://codepad.org/ib98T2Gd
これは std::sprintfboost::lexical_cast の変換速度を比較するものですが、このコードを動かすと、僕のコンパイラCygwin + gcc 3.4.4 )では boost::lexical_cast の方が倍以上遅い、という結果になります。
この結果は当然といえば当然で、 lexical_cast はストリームを使って変換処理を行っているので遅いです(ただし最新のコンパイラでは大差ないか、むしろ lexical_cast の方が速いという情報もあり、この辺は研究が要るかもしれません)。
もちろん、だからといって lexical_cast の魅力が減るわけではないですが(最大の魅力は「どんな文字列変換でも同じ表現で書ける」という使い勝手なわけですし)、それでも「遅い」というのはC++使いにとっては死活問題でもある訳で、速いに越したことは有りません。
では、何故遅いのか? それは、最初に書いた大まかな実装でも分かるとおり、ストリームを入力と出力の二回使っているからです。
他はどうあれ、 std::string に変換する場合には二回目のストリーム出力は要りませんstr() を呼べばいいので)
幸いにも C++0x の標準ライブラリには、オブジェクトをストリーム経由で文字列変換する to_string 関数が用意されるようなので(個人的には「何故現行のライブラリにこれがないのか」と思います。俺ライブラリでも、これは真っ先に作りました)、恐らく次世代の Boost では string に変換する場合には to_string を使うように特殊化された実装を使うようになるのでしょう。
また同様に string から他のクラスへ変換する場合にも、今度は istringstream を使った実装に特殊化されることになると思います。Boost 製作者の皆様は最適化大好きみたいですしね。

不満点2: 例外うざい

boost::lexical_cast では、「文字列→何か」の変換が失敗した場合、例外 boost::bad_lexical_cast を投げる、という仕様になっています。
が、例外というのはプログラムの実行速度を大きく落とすので、出来ることなら例外を使わずに何とかしたいところ。例えば、事前に例外を投げるかどうかをチェックする、という方法は非常に有効で、多くの例外はこの方法で回避することが出来ます。
しかし lexical_cast では、事前に例外を投げるかどうかをチェックすることが難しいです。無論、不可能ではないですが、「変換可能かどうかのチェックをするなら、そのときに変換してしまえよ」という感じであり、それは本末転倒。
この問題に対処するには、例えば boost::optional で値を返す lexical_cast_optional みたいな関数が存在すると便利です。そこで、俺ライブラリで実装してみようかな・・・と思い立って、 boost::lexical_castソースコードを読んでみたのですが・・・
とてもソースコードが長く、また難しいのです。これでは、とてもじゃないですが自力では実装できない。


そこで僕 SubaruG は、Boost 製作者に以下のような提案をしたいと思います。

  1. 失敗時に例外を投げるのではなく、bool型の戻り値で成功/失敗判定をする lexical_convert 俺ライブラリにもあります。実装は boost::lexical_cast +例外処理ですが)を新たに作る。
  2. さまざまな最適化処理は、全て lexical_convert の実装で行うことにする。
  3. boost::lexical_cast は内部で lexical_convert を呼び出すのみにして、独自の最適化は行わないようにする。

具体的には、こんな感じで実装するのです名前空間は俺Boostということで myboost にしてます)

#include <sstream>

namespace myboost
{
  // Boost にこれを用意する
  // t に s を文字列表現により変換して代入
  // 失敗した場合には false を返し、 t は変更されない
  template<typename Target, typename Source>
  bool lexical_convert( Target& t, const Source& s )
  {
    // 最適化は全てここで行う
    // とりあえず最も自然な実装
    std::stringstream ss;
    return ss << s && ss >> t;
  }
  
  // lexical_cast は lexical_convert を用いて実装する
  
  // とりあえず bad_lexical_cast の中身は空にしておく
  // 実際には std::bad_cast から継承されたクラスです
  struct bad_lexical_cast {};
  
  // 本体
  // s を文字列表現を媒介に Target 型へキャストする
  // 失敗時には bad_lexical_cast 例外を投げる
  template<typename Target, typename Source>
  Target lexical_cast( const Source& s )
  {
    // 単純に lexical_convert を用いて実装する
    Target t;
    
    if( !lexical_convert( t, s ) )
    {
      throw bad_lexical_cast();
    }
    
    return t;
  }

} // namespace myboost

こうすれば、例えば boost::optional で値を返したいときは、

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>

template<typename Target, typename Source>
boost::optional<Target> lexical_cast_optional( const Source& s )
{
  boost::optional<Target> ret( boost::in_place() );
  Target& t = *ret;
  
  if( !myboost::lexical_convert( t, s ) )
  {
    return boost::none;
  }
  
  return ret;
}

とかやれば作れるわけです(あ、 boost::in_place() はコピーコスト削減の為に使っているだけなので、意味的には Target() と同じですよ)


まぁ、もしこういうものを作ったとしても、実際に高速になるかは分かりません。
というわけで実際に、速度比較してみるとしましょうか:

#include <sstream>

namespace myboost
{
  // Boost にこれを用意する
  template<typename Target, typename Source>
  bool lexical_convert( Target& t, const Source& s )
  {
    // 最適化は全てここで行う
    // とりあえず最も自然な実装
    std::stringstream ss;
    
    return ss << s && ss >> t;
  }
  
  // lexical_cast とかは lexical_convert を用いて実装する
  struct bad_lexical_cast {};
  
  template<typename Target, typename Source>
  Target lexical_cast( const Source& s )
  {
    Target t;
    
    if( !lexical_convert( t, s ) )
    {
      throw bad_lexical_cast();
    }
    
    return t;
  }

} // namespace myboost

// optional version を定義する
#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>

template<typename Target, typename Source>
boost::optional<Target> lexical_cast_optional( const Source& s )
{
  boost::optional<Target> ret( boost::in_place() );
  Target& t = *ret;
  
  if( !myboost::lexical_convert( t, s ) )
  {
    return boost::none;
  }
  
  return ret;
}


// boost::lexical_cast で文字列を数値に変換する
// 失敗時には -1 を返す

// boost::lexical_cast version
#include <boost/lexical_cast.hpp>

int f1( const std::string& s )
{
  try
  {
    return boost::lexical_cast<int>(s);
  }
  catch( boost::bad_lexical_cast& )
  {
    return -1;
  }
}

// myboost::lexical_cast version
int f2( const std::string& s )
{
  try
  {
    return myboost::lexical_cast<int>(s);
  }
  catch( myboost::bad_lexical_cast& )
  {
    return -1;
  }
}

// optional version
int f3( const std::string& s )
{
  if( boost::optional<int> p = lexical_cast_optional<int>(s) )
  {
    return *p;
  }
  else
  {
    return -1;
  }
}


// テストコード
#include <iostream>
#include <boost/timer.hpp>
#include <boost/format.hpp>
using namespace std;
using boost::format;

void do_test( const string& s, int c )
{
  cout << format("convert string '%1%' to int %2% times.\n")
            % s % c;
  
  {
    boost::timer t;
    
    for( int i = 0; i < c; ++i )
    {
      f1(s);
    }
    
    cout << format("f1(): %1% sec\n") % t.elapsed();
  }
  
  {
    boost::timer t;
    
    for( int i = 0; i < c; ++i )
    {
      f2(s);
    }
    
    cout << format("f2(): %1% sec\n") % t.elapsed();
  }
  
  {
    boost::timer t;
    
    for( int i = 0; i < c; ++i )
    {
      f3(s);
    }
    
    cout << format("f3(): %1% sec\n") % t.elapsed();
  }
}

int main()
{
  do_test( "12345678", 100000 );
  cout << endl;
  do_test( "not int", 100000 );
  
  return 0;
}

結果( gcc 3.4.4 最適化オプション -O3 で測定):

convert string '12345678' to int 100000 times.
f1(): 0.969 sec
f2(): 1.453 sec
f3(): 1.563 sec

convert string 'not int' to int 100000 times.
f1(): 1.703 sec
f2(): 1.937 sec
f3(): 1.485 sec

結果を見ると、流石にオリジナル boost::lexical_cast は高速だな、というのが分かります。
しかし、その高速な boost::lexical_cast でも、変換失敗時には処理速度が ほぼ倍になり、何も最適化していない状態の lexical_cast_optional に負けてしまう、というのがよく分かるかと思います。
つまり、実際に lexical_cast_optional は有効なのです!
さらに、ここでもし、 myboost::lexical_cast がオリジナルと同程度の速度で動くと仮定すると、boost::optional を使う場合、成功時の処理は一割ほど増加しますが、失敗時に殆ど速度低下しないようにすることが出来る、ということであり、十分にBoostに導入してもいいレベルの部品だと思うのですが、どうでしょう。
とはいえ、このテストの結果から見るに、何の工夫もない状態では圧倒的に boost::lexical_cast が高速なので、lexical_convert から俺 lexical_cast を作るのは無駄でしかない、というのも分かります。


よーするに皆様、車輪の再発明は程々にしましょう、ということで一つ。
あと、誰か、この意見Boostのコミュニティの人に伝えてくれると助かるかもしれないんだよ(他力本願)


追記: アキラさんlexical_cast_optinal って別名関数ではなく lexical_cast って関数名のまま optional 対応版を用意する方法を書いてくれたので、紹介します: http://d.hatena.ne.jp/faith_and_brave/20091225/1261734967