boost::tokenizer で scanf を作ってみる

Boost には printf() のような動きをするクラスとして boost::format がありますが,scanf() のような動きをするクラスは(多分)ありません.そこで,今回は scanf() のような動きをするクラスを自作してみます.今回は,boost::tokenizer をカスタマイズする方法でこれを実現してみます.

boost::tokenizer は,以下のような宣言になっています.

template <
      class TokenizerFunc = char_delimiters_separator<char>, 
      class Iterator = std::string::const_iterator,
      class Type = std::string
>
class tokenizer

boost::tokenzier の挙動は TokenizerFunc がそのほとんどを決定しています.したがって,自作した TokenizerFunc クラスをテンプレート・パラメータとして与えることで,boost::tokenizer の挙動を柔軟に制御することができます.

TokenizerFunc で与えるクラスには,reset() と ()演算子が定義されている必要があります.このうち,重要となってくるのは ()演算子の方です.TokenizerFunc として与えるクラスの ()演算子は,呼ばれるたびに「次のトークンを引数 token にセットする」と言う挙動をします.

書式にしたがって文字列を分割する format_separator

scanf() のようなクラスを作成する前に,まず boost::tokenizer の TokenizerFunc として使用するクラスを作成します.少し長いですが,このクラスは以下のような実装になります.

#ifndef FORMAT_SEPARATOR_H
#define FORMAT_SEPARATOR_H

#include <string>
#include <boost/lexical_cast.hpp>

class format_separator {
public:
    typedef char char_type;
    typedef std::basic_string<char> string_type;
    typedef string_type::iterator iterator;
    
    // constructor and destructor
    format_separator() : fmt_("%s"), skipws_(true) {}
    
    explicit format_separator(const string_type& fmt, bool x = true) :
        fmt_(fmt), skipws_(x) {
        cur_ = fmt_.begin();
    }
    
    virtual ~format_separator() {}
    
    void reset() { cur_ = fmt_.begin(); }
    
    /* --------------------------------------------------------------------- */
    /*
     *  operator()
     *
     *  [next,last) から次のトークンを読み取り,dest にセットする.
     */
    /* --------------------------------------------------------------------- */
    template <class InIter, class Token>
    bool operator()(InIter& next, InIter last, Token& dest) {
        while (next != last && cur_ != fmt_.end()) {
            if (*cur_ == '%' && cur_ + 1 != fmt_.end() && *(cur_ + 1) != '%') {
                string_type flag;
                if (!this->get_format(flag)) break;
                
                string_type normal;
                this->get_normal(normal);
                
                dest = this->get(next, last, normal, flag);
                return true;
            }
            else {
                string_type normal;
                if (!this->get_normal(normal) || !this->compare(next, last, normal)) break;
            }
        }
        return false;
    }
    
private:
    string_type fmt_;
    bool skipws_;
    iterator cur_;
    
    /* --------------------------------------------------------------------- */
    /*
     *  get
     *
     *  [next,last) から次のトークンを読み取って返す.
     */
    /* --------------------------------------------------------------------- */
    template <class InIter>
    string_type get(InIter& next, InIter last,
        const string_type& normal, const string_type& flag) {
        int n = -1;
        if (!flag.empty() && isdigit(flag.at(0))) n = boost::lexical_cast<int>(flag);
        if (skipws_) {
            while (next != last && *next == ' ') ++next;
        }
        
        string_type dest;
        while (next != last && n != 0) {
            InIter prev = next;
            if (this->compare(next, last, normal)) break;
            next = prev;
            dest += *next;
            ++next;
            --n;
        }
        
        return dest;
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  get_normal
     *
     *  [cur_, fmt_.end()) から次の書式ではない部分の文字列(% で始まる
     *  文字列ではない部分)を読み取って normal にセットする.
     */
    /* --------------------------------------------------------------------- */
    bool get_normal(string_type& normal) {
        normal.clear();
        while (cur_ != fmt_.end()) {
            if (*cur_ == '%') {
                if (cur_ + 1 != fmt_.end() && *(cur_ + 1) == '%') ++cur_;
                else break;
            }
            normal += *cur_;
            ++cur_;
        }
        
        if (normal.empty()) return false;
        return true;
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  get_format
     *
     *  [cur_, fmt_.end()) から次の書式文字列(% で始まる文字列)を
     *  読み取って flag にセットする.ただし,識別子自体 (s) は読み捨てる.
     */
    /* --------------------------------------------------------------------- */
    bool get_format(string_type& flag) {
        ++cur_;
        flag.clear();
        
        // "%s" のみ許す場合.
        // "%d" や "%x" なども許す場合は,isalpha(*cur_) など
        // 適当なものに変更する.
        while (cur_ != fmt_.end() && *cur_ != 's') {
            flag += *cur_;
            ++cur_;
        }
        if (cur_ == fmt_.end()) return false;
        ++cur_;
        return true;
    }
    
    /* --------------------------------------------------------------------- */
    /*
     *  compare
     *
     *  [next,last) と s を比較する.一致した位置まで next を進める.
     */
    /* --------------------------------------------------------------------- */
    template <class InIter>
    bool compare(InIter& next, InIter last, const string_type& s) {
        if (s.empty()) return false;
        string_type::const_iterator pos = s.begin();
        
        while (next != last) {
            if (skipws_ && *next == ' ' && *pos != ' ') ++next;
            else if (*next != *pos) return false;
            else {
                ++next;
                ++pos;
                if (pos == s.end()) break;
            }
        }
        
        return true;
    }
};

後述しますが,型判定は変数に代入する際に boost::lexical_cast に任せます.したがって,現在のところ format_seprator では,書式の識別子としては "s" しか許していません("%d" や "%x" などは無効).また,skipws は解析する文字列の空白文字 (0x20) を読み飛ばすかどうかを決定します.

この format_separator は,boost::tokenizer とともに以下のような形で使用します.

#include <iostream>
#include <string>
#include <boost/tokenizer.hpp>
#include "format_separator.h"

void put_token(const std::string& s, const std::string& fmt) {
    typedef boost::tokenizer<format_separator> fmttokenizer;
    format_separator sep(fmt);
    fmttokenizer token(s, sep);
    
    std::cout << "source: " << s << std::endl;
    for (fmttokenizer::iterator pos = token.begin(); pos != token.end(); ++pos) {
        std::cout << "<" << *pos << "> ";
    }
    std::cout << std::endl;
}

int main(int argc, char* argv[]) {
    // date コマンド(で出力される書式の一つ)の解析.
    std::string s1 = "Tue Sep  1 22:30:12 JST 2009";
    std::string fmt1 = "%s %s %s %s:%s:%s %s %s";
    put_token(s1, fmt1);
    std::cout << std::endl;
    
    // 日時の数字のみを連続させて表記した場合.
    std::string s2 = "20090901223012";
    std::string fmt2 = "%4s%2s%2s%2s%2s%2s";
    put_token(s2, fmt2);
    std::cout << std::endl;
    
    // tcpdump の出力(一部省略).
    std::string s3 = "23:30:25.030254  IP  192.168.1.2.100 > 192.168.1.3.200: . ack 12 win 256 <XXX>";
    std::string fmt3 = "%s:%s:%s.%s IP %s > %s: %s ack %s win %s <%s>";
    put_token(s3, fmt3);	

    return 0;
}
実行結果
[example]$ g++ -Wall example_format_separator.cpp
[example]$ ./a
source: Tue Sep  1 22:30:12 JST 2009
<Tue> <Sep> <1> <22> <30> <12> <JST> <2009> 

source: 20090901223012
<2009> <09> <01> <22> <30> <12>

source: 23:30:25.030254  IP  192.168.1.2.100 > 192.168.1.3.200: . ack 12 win 256 <XXX>
<23> <30> <25> <030254> <192.168.1.2.100> <192.168.1.3.200> <.> <12> <256> <XXX>

scanf のような動きをする scanner

最後に,先ほど作成した format_separator を利用して scanf のような動きをするクラスを作成してみます.ここでは,scanner と言うクラス名にしておきます.

#ifndef SCANNER_H
#define SCANNER_H

#include <string>
#include <vector>
#include <boost/tokenizer.hpp>
#include <boost/lexical_cast.hpp>
#include "format_separator.h"

class scanner {
public:
    typedef char char_type;
    typedef unsigned int size_type;
    typedef std::basic_string<char> string_type;
    
    scanner() : v_(), cur_() {}
    
    scanner(const string_type& s, const string_type& fmt) :
        v_(), cur_() {
        this->assign(s, fmt);
    }
    
    virtual ~scanner() throw() {}
    
    scanner& assign(const string_type& s, const string_type& fmt) {
        separator sep(fmt);
        parser token(s, sep);
        v_.assign(token.begin(), token.end());
        cur_ = v_.begin();
        return *this;
    }
    
    template <class Type>
    scanner& operator%(Type& dest) {
        if (cur_ != v_.end()) dest = boost::lexical_cast<Type>(*cur_++);
        return *this;
    }
    
private:
    typedef format_separator separator;
    typedef boost::tokenizer<separator> parser;
    
    std::vector<string_type> v_;
    std::vector<string_type>::iterator cur_;
};

#endif // SCANNER_H

コンストラクタ,または assign() で文字列と書式を指定すると,まず boost::tokenizer を使用して文字列分割を行います.その後は,%演算子が呼ばれるたびに,引数に指定された変数にトークンを格納していきます.%演算子は,boost::format に似せて使ってみました.

scanner では,型変換は boost::lexical_cast に任せています.数字文字列ではないのに引数に int 型の変数が指定されたなど boost::lexical_cast で変換できない場合は boost::bad_lexical_cast が例外として送出されます.

以下はサンプルプログラム.

#include <iostream>
#include <string>
#include "scanner.h"

int main(int argc, char* argv[]) {
    // date コマンド(で出力される書式の一つ)の解析.
    std::string s1 = "Tue Sep  1 22:30:12 JST 2009";
    std::string fmt1 = "%s %s %s %s:%s:%s %s %s";
    
    std::string week, mon, zone;
    int year = 0, day = 0, hour = 0, min = 0, sec = 0;
    
    // boost::format のような形.
    scanner(s1, fmt1) % week % mon % day % hour % min % sec % zone % year;
    
    std::cout << year << ", " << mon << ", " << day << ", " << week << std::endl;
    std::cout << hour << ":" << min << ":" << sec << std::endl;
    
    return 0;
}
実行結果
[example]$ g++ -Wall example_scanner.cpp
[example]$ ./a
2009, Sep, 1, Tue
22:30:12

boost::tokenzier は結構カスタマイズし易いので面白いのではないかな,と思います.