stringstream で簡単 split

iPhone 触ってみたいですねー。商標でもめてますが、いっそのこと Apple Phone でも良いような。


さて、C++ で独自フォーマットのテキストファイルを読み込む場合には、find_first_* と substr を組み合わせて泥臭く処理をすることが多かったのですが、『区切り文字を空白に置換する』という前処理を追加することで、stringstream を活用できることに気付きました。いくつか例を挙げて紹介していきます。


まず最初に、以下のようなカンマ区切りのファイルを読み込む例です。

###
### sample.csv
###

1,tom,dvorak,1.0
1,bob,qwerty,0.5
...

コードは以下の通り。

struct csvdata {
    int type;
    std::string key;
    std::string value;
    float priority;
};

std::ifstream ifs("sample.csv");
std::string line;
csvdata data;

while(getline(ifs, line)) {
    if(line.empty() || line[0] == '#') continue;

    // ',' を空白に置換して
    std::replace(line.begin(), line.end(), ',', ' ');
    // 分解
    std::stringstream buf(line);
    // 読み取り
    if(buf >> data.type >> data.key >> data.value >> data.priority) {
    ...

型変換も自動的に行なえるのでかなりシンプルです。ただし、このままでは文字列に空白を含めることができません。空白を禁止する仕様とするか、それが許されないのであれば、std::replace する前にエンコードして読み出し後にデコードするなどの対処が必要になります。


次は split 関数の例です。

std::vector<std::string> split(const std::string& str, const std::string& delimiter) {
    // delimiter(2 文字以上も可) を空白に置換
    std::string item(str);    
    for(unsigned pos = item.find(delimiter); pos != std::string::npos; pos = item.find(delimiter, pos)) {
        item.replace(pos, delimiter.size(), " ");
    }
    // 分解
    std::stringstream buf(item);

    // 読み取り
    std::vector<std::string> result;
    while(buf >> item) {
        result.push_back(item);
    }

    return result;
}

区切り文字で文字列を分割した結果を std::vector に格納して返しますが、空値は無視され、空白で分割されてしまいます。見た目のシンプルさと引き換えに、機能面のトレードオフがあるというわけです。まとめると、

メリット
  • シンプルで安全(range_error に怯えなくて良い)
  • 型変換サポート
デメリット
  • データに空白を含めるなら、エンコード/デコードが必要
  • 空値は無視される

になります。


数百行程度のテキストを処理するのであればパフォーマンスも問題にはならないでしょう。
ミニパーサーとしても使える stringstream は、なかなか魅力的な標準ライブラリです。

追記(25:10)

サンプルを若干修正