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
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 は結構カスタマイズし易いので面白いのではないかな,と思います.