ワイド文字とcodecvt

Rubyなどのスクリプト言語に比べてC++がしんどい部分の一つに,ワイド文字(utf-?)があります.ただ,コンパイラ屋も頑張っているようで,Visual Studio 2005だとC++標準のナロー文字<->ワイド文字変換クラスであるstd::codecvt<wchar_t, char, mbstate_t>が結構まともに動きました.そこで,ここらでC++でワイド文字も楽に扱えるようにしようと,いくつか追加&修正してみました.

さて,サンプルプログラムを何にしようかと考えていたのですが,少し前にはてなブックマークのエントリ情報を取得するスクリプトを書いたばかりだったので,このプログラムをC++で書いてみることに.ちなみに,gcc (version 4.1.2 cygwin) でコンパイルすると,

std::wcout なんてネエョ

と言われて絶望したので,こっちは取り合えず放置しています.

// ワイド文字関連のCLXライブラリを使用するために必要
#ifndef CLX_USE_WCHAR
#define CLX_USE_WCHAR
#endif

#include <iostream>
#include <string>
#include "clx/http.h"
#include "clx/uri.h"
#include "clx/code_convert.h"
#include "clx/json.h"

int main(int argc, char* argv[]) {
    if (argc < 3) std::exit(-1);
    
    clx::http session(clx::uri::encode(argv[1]));
    if (!session.get(clx::uri::encode(argv[2]))) {
        std::cerr << "failed to get response" << std::endl;
        std::exit(-1);
    }
    
    /*
     * parse a hatena bookmark entry.
     * はてなのJSONは()で括られているので,その部分は除外.
     */
    std::wstring body = clx::code_convert<wchar_t>(session.body());
    clx::wjson hateb(body.begin() + 1, body.end() - 1);
    
    std::wcout.imbue(std::locale("japanese"));
    for (clx::wjson::iterator pos = hateb.begin(); pos != hateb.end(); pos++) {
        std::wcout << pos->first << L": " << pos->second << std::endl;
    }
    std::wcout << std::endl;
    
    if (hateb.find(L"bookmarks") == hateb.end()) return 0;
    
    // parse and print the detail of "bookmarks"
    clx::wjson_array bk(hateb[L"bookmarks"]);
    std::wcout << L"bookmarks have " << bk.objects().size() << L" object, "
        << bk.strings().size() << L" string" << std::endl;
    std::wcout << L"-----" << std::endl;
    for (size_t i = 0; i < bk.objects().size(); i++) {
        std::wcout << L"object " << i << L":" << std::endl;
        for (clx::wjson::iterator pos = bk.object(i).begin();
            pos != bk.object(i).end(); pos++) {
            std::wcout << L"\t" << pos->first << L": " << pos->second << std::endl;
        }
    }
    
    return 0;
}

やってることは,

  1. httpでb.hatena.ne.jpからブックマークエントリ情報を取得.
  2. std::string -> std::wstringに変換.
  3. JSONデータの解析.

です.30〜40行程度で実装できるようになったので,これ位ならスクリプト言語とも張り合える!とか勝手に思っています.別に張り合う必要もないのですが.std::codecvt<wchar_t, char, mbstate_t>は,ナロー文字->ワイド文字の変換メソッド(in())と,ワイド文字->ナロー文字の変換メソッド(out())が異なっていて使いづらかったのでラッパー関数を書いています.以下のテンプレート関数は,src->destへの変換を行います.

template <class Type, class CharT>
std::basic_string<Type> code_convert(const std::basic_string<CharT>& src,
    const std::locale& loc = std::locale());

template <class Type, class Source>
std::basic_string<Type> code_convert(const Source* src,
    const std::locale& loc = std::locale());

実行結果は以下の通り.一応まともに動きました.

$ ./clxtest b.hatena.ne.jp "/entry/json/?url=http://d.hatena.ne.jp/tt_clown/20080823/p1"
bookmarks: [{"comment":" ","timestamp":"2008/08/25 16:01:00","user":"HISAMATSU","tags":["\u306f\u3066\u306a","ruby"]},{"comment":"\u306f\u3066\u306a\u30d6\u30c3\u30af\u30de\u30fc\u30af\u3000\u985e\u4f3c","timestamp":"2008/08/24 21:11:03","user":"poafag","tags":["ruby","\u306f\u3066\u306a"]}]
count: 2
eid: 9756340
entry_url: http://b.hatena.ne.jp/entry/http://d.hatena.ne.jp/tt_clown/20080823/p1
related: []
screenshot: http://screenshot.hatena.ne.jp/images/120x90/0/f/f/9/5/d9701e8c6a5392d9a3a729155d07315e310.jpg
title:  github::clown::ruby-hatena - Life like a clown
url: http://d.hatena.ne.jp/tt_clown/20080823/p1

bookmarks have 2 object, 0 string
        • -
object 0: comment: tags: ["\u306f\u3066\u306a","ruby"] timestamp: 2008/08/25 16:01:00 user: HISAMATSU object 1: comment: はてなブックマーク 類似 tags: ["ruby","\u306f\u3066\u306a"] timestamp: 2008/08/24 21:11:03 user: poafag

JSONの紹介を見ると,C++JSONのパーサを実装する場合,boost::anyとboost::spiritを活用するパターンがほとんどだったのですが,上記のパーサはJSONパーサ用のTokenizerFunctionを書くことで実装してみました(正確には,std::mapに対応できるように少しインターフェースを変えていますが).

basic_tokenmap<json_separator<char>, std::string, std::string> json;
basic_tokenmap<json_separator<wchar_t>, std::wstring, std::wstring> wjson;

basic_json_array<char> json_array;
basic_json_array<wchar_t> wjson_array;

結果を見ても分かりますが,

  • 1段階分しかパースされない(ユーザが,得られた文字列を引数に指定してさらにパースする必要がある).
  • 文字列,数値,true,false,nullは全て文字列として返す.

といくつか制限はありますが,100〜200行程度で実装できるのでお手軽でいいかなと思います.TokenizerFunctionのコンセプトはいろいろと使い易くて好きです.JSON形式データもパースし易くていいですね.