C++/CLI 文字列キャスト

最近 C++/CLI を弄っているのですが,GUI 以外の部分は通常の C++ で記述しているので,どうしても System::String^ と std::string (, or std::wstring) の相互変換が必要になります.ぐぐって見たところ同様の要望は多いようで,各所で解決方法も記載されていました.それで,せっかくなので System::String^ <-> std::string をキャストに見せかけた関数を作成しておくことにしました.

#include <string>
#include <vcclr.h>

/* ----------------------------------------------------------------- */
//  string_converter
/* ----------------------------------------------------------------- */
template <class CharT>
class string_converter {};

/* ----------------------------------------------------------------- */
//  string_converter<char>
/* ----------------------------------------------------------------- */
template <> class string_converter<char> {
public:
    typedef System::String^ cli_string;
    typedef std::basic_string<char> stl_string;
    typedef char char_type;
    
    static stl_string decode(cli_string src) {
        System::IntPtr mptr = System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(src);
        stl_string dest(static_cast<char_type*>(mptr.ToPointer()));
        System::Runtime::InteropServices::Marshal::FreeHGlobal(mptr);
        return dest;
    }
    
    static cli_string encode(const stl_string& src) {
        cli_string dest = gcnew System::String(src.c_str());
        return dest;
    }
};

/* ----------------------------------------------------------------- */
//  string_converter<wchar_t>
/* ----------------------------------------------------------------- */
template <> class string_converter<wchar_t> {
public:
    typedef System::String^ cli_string;
    typedef std::basic_string<wchar_t> stl_string;
    typedef wchar_t char_type;
    
    static stl_string decode(cli_string src) {
        System::IntPtr wptr = System::Runtime::InteropServices::Marshal::StringToHGlobalUni(src);
        stl_string dest(static_cast<char_type*>(wptr.ToPointer()));
        System::Runtime::InteropServices::Marshal::FreeHGlobal(wptr);
        return dest;
    }
    
    static cli_string encode(const stl_string& src) {
        cli_string dest = gcnew System::String(src.c_str());
        return dest;
    }
};

/* ----------------------------------------------------------------- */
//  string_cast
/* ----------------------------------------------------------------- */
template <class Type, class Source>
inline Type string_cast(Source src) {
    return string_converter<typename Type::value_type>::decode(src);
}

template <>
inline System::String^ string_cast(std::basic_string<char> src) {
    return string_converter<char>::encode(src);
}

template <>
inline System::String^ string_cast(std::basic_string<wchar_t> src) {
    return string_converter<wchar_t>::encode(src);
}

使用方法は,変換させたい方の型をテンプレート引数に,変換元の文字列を引数に指定します.例えば,System::String^ から std::string に変換する場合は以下のようになります.

System::String^ src = gcnew System::String(L"Hello, world!");
std::string dest = string_cast<std::string>(src);

全てを C++/CLI 内で完結させる場合は System::String^ だけで済ませばいいのですが,メインの処理部分はできるだけ複数のコンパイラコンパイル可能な形にしておきたいので,この類の変換は不可避になりそうです.

追記

Visual Studio 2008 (VC++ 9.0) では marshal_as<> と言う関数(演算子?)が追加されており,上記の変換はそれを用いて行うことができるようです.最新の VC++ 環境で開発を行っている場合は,わざわざ作成する必要はない模様.

marshal_as

このメソッドは、ネイティブ型とマネージ型との間でデータを変換する簡単な方法です。サポートされているデータ型を確認するには、「C++ におけるマーシャリングの概要」を参照してください。一部のデータ変換ではコンテキストが必要になります。このようなデータ型は、marshal_context クラスを使用して変換できます。

http://msdn.microsoft.com/ja-jp/library/bb384859.aspx

文字列の16進数ダンプ

文字列を 16 進数でダンプする関数が必要になりそうなので作成しました.入力/出力用のイテレータを引数に指定すると,入力から1文字ずつ読み込み,16進数の文字列として出力します.ただ,この関数を使用する場合 hexdump<char>(src.begin(), src.end(), dest) のように,わざわざテンプレート引数に char を指定しなければならないので,入力として文字列およびストリームを指定する関数をオーバーロードしました.

#include <iterator>
#include <limits>
#include <sstream>

template <class CharT, class InIter, class OutIter>
OutIter hexdump(InIter first, InIter last, OutIter dest) {
    static const int shift = ((sizeof(unsigned long) - sizeof(CharT)) * 8);
    static const unsigned long mask = std::numeric_limits<unsigned long>::max() >> shift;
    
    std::basic_stringstream<CharT> ss;
    for (; first != last; ++first, ++dest) {
        ss << std::hex << (static_cast<int>(*first) & mask);
        *dest = ss.str();
        ss.str("");
    }
    return dest;
}

template <class CharT, class OutIter>
OutIter hexdump(std::basic_istream<CharT>& src, OutIter dest) {
    std::istream_iterator<CharT> input(src);
    std::istream_iterator<CharT> last;
    return hexdump<CharT>(input, last, dest);
}

template <class CharT, class OutIter>
OutIter hexdump(const std::basic_string<CharT>& src, OutIter dest) {
    return hexdump<CharT>(src.begin(), src.end(), dest);
}

サンプルプログラムと実行結果は以下の通り.

#include <iostream>
#include <iterator>
#include "hexdump.h"

int main(int argc, char* argv[]) {
    std::ostream_iterator<std::string> out(std::cout, " ");
    hexdump(std::cin, out);
    return 0;
}
Result

$ ./test < example_hexdump.cpp 
23 69 6e 63 6c 75 64 65 3c 69 6f 73 74 72 65 61 6d 3e 23 69 6e 63 6c 75 64 65 3c 
69 74 65 72 61 74 6f 72 3e 23 69 6e 63 6c 75 64 65 22 63 6c 78 2f 68 65 78 64 75 
6d 70 2e 68 22 69 6e 74 6d 61 69 6e 28 69 6e 74 61 72 67 63 2c 63 68 61 72 2a 61 
72 67 76 5b 5d 29 7b 73 74 64 3a 3a 6f 73 74 72 65 61 6d 5f 69 74 65 72 61 74 6f 
72 3c 73 74 64 3a 3a 73 74 72 69 6e 67 3e 6f 75 74 28 73 74 64 3a 3a 63 6f 75 74 
2c 22 22 29 3b 63 6c 78 3a 3a 68 65 78 64 75 6d 70 28 73 74 64 3a 3a 63 69 6e 2c 
6f 75 74 29 3b 72 65 74 75 72 6e 30 3b 7d 

ローカルホストの IP アドレス取得方法

プログラムから自ホストの IP アドレスを取得する必要があったので調べてみました.

3.3 - Winsock プログラム で自分の IP アドレスを取得する方法は?

3種類の方法があり、それぞれに利点、欠点があります:

  1. 最も簡単な方法は、connect 済みのソケットに対して getsockname() を呼び出すことです。connect 済みのソケットが無ければ、その呼出しは失敗するか、あるいは無意味な情報が返されます。
  2. ソケットを事前にオープンすることなしに自分のアドレスを取得するには、gethostname() の返却値に対して gethostbyname() を呼び出すことです。この例に示すように、この呼出しはそのホストが持つ全てのインターフェースのリストを返却します。
  3. 三番目の方法は Winsock 2 でのみ動作します。新しい WSAIoctl() API は SIO_GET_INTERFACE_LIST オプションをサポートしており、返却される情報の一部として、システムの各々のネットワークインターフェースのアドレスが返却されます。
http://www.kt.rim.or.jp/~ksk/wskfaq-ja/intermediate.html

この中で 1. の方法が最も簡単に取得することができるのですが,connect 済みのソケットが存在しない場合には使用できません.ただし,connect 済みのソケットとは“connect(2) システムコールに成功したソケット”であれば何でも良いようです.

UDP ソケットが、bind() 呼出し後の通常の状態である非コネクトソケットであれば、send() や write() は使えません。それは目的アドレスが利用できないためで、データを送るには sendto() しか使えません。

そのソケットに対して connect() を呼び出すと、単に指定されたアドレスとポート番号を、希望の通信相手として記録します。これはつまり、 send() や write() が使えるようになるという意味です。これらは、connect の呼び出しで与えられた目的アドレスとポートをパケットの宛先として使用します。

Programming UNIX Sockets in C - Frequently Asked Questions: UDP/SOCK_DGRAM アプリケーションの作成

UDP ソケットの場合,connect(2) システムコールは単に引数に指定されたアドレスとポート番号を記録するのみなので,指定した IP アドレスが存在しないものであっても成功します.そこで,UDP ソケットを利用してダミーの“connect 済みのソケット”を作成してそれを利用する事を考えてみます.サンプルコードは以下の通りです.尚,下記のコードは POSIX ソケット用です.Winsock の場合は若干の修正が必要となります(どちらでも使えるバージョンは,CLX C++ Libraries - localhost を参照して下さい).

#include <cstring>
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>

std::string localhost() {
    std::string dest;
    int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (s < 0) return dest;
    
    // コネクションを確立(しようと)するためのダミーの IP アドレス
    struct in_addr dummy;
    inet_aton("192.0.2.4", &dummy);
    
    struct sockaddr_in exaddr = { 0 };
    std::memset(&exaddr, 0, sizeof(exaddr));
    exaddr.sin_family = AF_INET;
    std::memcpy((char*)&exaddr.sin_addr, &dummy, sizeof(dummy));
    
    if (connect(s, (struct sockaddr*)&exaddr, sizeof(exaddr)) < 0) {
        close(s);
        return dest;
    }
    
    struct sockaddr_in addr = {0};
    socklen_t len = sizeof(addr);
    int status = getsockname(s, (struct sockaddr*)&addr, &len);
    if (status >= 0) dest = inet_ntoa(addr.sin_addr);
    close(s);
    
    return dest;
}

int main(int argc, char* argv[]) {
    std::cout << localhost() << std::endl;
    return 0;
}

ダミー用の IP アドレスは何でも良かったのですが,例として推奨されているドメイン名とIPアドレス - あどけない話 に習って例に使用される IP アドレスにしておきました(実際に上記の IP アドレスを使用している人がいる可能性が低い).

コンストラクタと例外

C++ のコンストラクタで例外が発生するとそのクラスのデストラクタが呼び出されず,メモリリークの原因となり兼ねないのでコンストラクタでは例外を発生させてはならない,と言う話があります.これに関しては [迷信] コンストラクタから例外を送出してはならないなどの反対意見もあるように,“コンストラクタで例外を発生させるなら自力で後始末せよ”が正しいようです.

これに関連して,コンストラクタで例外が発生した場合メンバ変数のデストラクタは呼ばれるのかどうかが自信がなかったので実験してみました.まず,例題その 1.

class error_example {
public:
    explicit error_example(int n) : p_(new int(n)) {
        std::cout << "constructor: " << *p_ << std::endl;
        throw std::runtime_error("something wrong");
    }
    
    ~error_example() {
        std::cout << "destructor of error_example" << std::endl;
        delete p_;
    }
    
    void print() { std::cout << *p_ << std::endl; }
    
private:
    int* p_;
};

/* ------------------------------------------------------------------------- */
//  main
/* ------------------------------------------------------------------------- */
int main(int argc, char* argv[]) {
    try {
        error_example obj(10);
        obj.print();
    }
    catch (std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
        std::exit(-1);
    }
    
    return 0;
}

この実行結果は以下のようになります.error_example のデストラクタが実行されず,領域の開放が適切に行われていないことが分かります.

$ ./test
constructor: 10
something wrong

次に,error_example を以下のように書き換えてみます.

class error_example {
public:
    explicit error_example(int n) : p_(new int(10)) {
        std::cout << "constructor: " << *p_ << std::endl;
        throw std::runtime_error("something wrong");
    }
    
    ~error_example() {
        std::cout << "destructor of error_example" << std::endl;
    }
    
    void print() { std::cout << *p_ << std::endl; }
    
private:
    shared_ptr<int> p_;
};

そして,shared_ptr のデストラクタに適当に文字列を出力するようにして実行した結果が以下のようになります.

$ ./test
constructor: 10
ref_count: 1
delete object
something wrong

このように,初期化の終了したメンバ変数は,コンストラクタで例外が発生した場合でもデストラクタが呼ばれていることが分かります.そのため,領域の開放をメンバ変数自身がきちんと行うようになっていれば,メンバ変数の領域の解放漏れは防ぐことができるようです.

OpenSSLとselect

OpenSSL を使用しているときに複数のソケットの入力状況を管理する場合,通常の socket と同じように select や epoll を用いて制御を行うとうまくいかないときがあります.

SSL のような暗号通信の場合、送信は「垂れ流し」では済まず、ハンドシェークを行なう必要があるからだが、この時、受信しようと思っていなかったデータ、つまり通信相手が送信したデータまで読み込んでしまう場合がある。

すると、SSL_write を呼んでいるのに、 OpenSSL の受信バッファに、意図せずデータが溜まってしまう。こうなってしまうと、select(2) や epoll(2) では検知できない。 select(2) や epoll(2) は、 I/O レベルでの受信データの有無を調べるシステムコールであり、それより上のレベルである OpenSSL ライブラリの受信バッファのことは関知しないからだ。

仙石浩明の日記: SSL_pending

したがって,select で入力にパケットが到着しているかどうかを判定し,パケットが到着していれば対応する socket に受信操作を行わせると言う制御を行う場合,select で判定すると同時にそれぞれの (SSL) socket の受信バッファにデータが存在しているかどうかを判定する必要があります.コードにすると以下のような形になります.

fd_set sysrfds;
FD_ZERO(&sysrfds);

/*
 * rfds_ は [socket, handler] の map.
 * socket: ソケットクラス.socket() メソッドでディスクリプタを取得している.
 * handler: 入力にパケットが到着したときに行う処理.
 */
for (iterator pos = rfds_.begin(); pos != rfds_.end(); ++pos) {
    FD_SET(pos->first->socket(), &sysrfds);
}

struct timeval tv;
tv.tv_sec = timeout; // 適当なタイムアウト時間
tv.tv_usec = 0;

int n = select(FD_SETSIZE, &sysrfds, NULL, NULL, &tv);
if (n < 0) throw std::runtime_error("select");

for (iterator pos = rfds_.begin(); pos != rfds_.end(); ++pos) {
    if (SSL_pending(pos->ssl()) > 0 || FD_ISSET(pos->socket(), &sysrfds)) {
        pos->second->run(*pos->first);
    }
}

上記のような形で,SSL ソケットで複数のソケットの入力状況を監視するクラスが ssl::sockmanager となります.

Hello, OpenSSL!

SSL 通信用のライブラリを追加しようと少し調べたところ,OpenSSL ライブラリを使うのがやはり王道っぽいので OpenSSL を使って実装してみました.

gcc

gcc を使う環境 (Linux, cygwin, ...) だと大抵の場合は最初からインストールされているようです.ない場合は,ソースコードをダウンロードして make するか,apt-get install とか適当なコマンドでインストールするとすんなり入るだろうと思います.コンパイル時には,crypto と ssl をリンクする必要があるようです.

g++ -Wall -o test example_ssl.cpp -lcrypto -lssl

VC++

Windows 版は,Shining Ligth Productions - Win32 OpenSSL からバイナリをダウンロードすることができるようです.Light と付いているアーカイブにはインクルードファイルなどが含まれていないため,Light と付いてないものを選択します.インストールが終わったら次はインクルードパスとライブラリパスを設定します.私は,普段はターミナルから cl.exe を叩いて確認することが多いので,環境変数 (INCLUDE, LIB) に設定しています(参考:コマンドラインから VC++ のコンパイラ (cl.exe) を使用する - Life like a clown).例えば,c:\openssl にインストールしたとすると,INCLUDE および LIB の環境変数は以下のようになります.

INCLUDE="C:\openssl\include"
LIB="C:\openssl\lib"

Sample code

サンプルコードは以下の通り.http のサンプルコードの http を https に書き換えて,ポート番号を HTTPS 用にかえただけです.

#include <iostream>
#include "clx/https.h"

int main(int argc, char* argv[]) {
    if (argc < 3) return -1;
    
    clx::https session(argv[1], 443);
    session.get(argv[2]);
    	
    std::cout << "code: " << session.code() << std::endl;
    std::cout << std::endl;
    
    std::cout << "head" << std::endl;
    std::cout << "--" << std::endl;
    for (clx::http::head_iterator pos = session.head().begin(); pos != session.head().end(); pos++) {
        std::cout << pos->first << ": " << pos->second << std::endl;
    }
    std::cout << std::endl;
    
    std::cout << "body" << std::endl;
    std::cout << "--" << std::endl;
    std::cout << session.body() << std::endl;
    
    return 0;
}

上記は,サーバの証明書の認証は行っていません.サーバの認証を行う場合は,サーバと接続する前に rootCA の証明書のファイル(or ディレクトリ)の場所を指定しておく必要があります (example_https_verify.cpp).

clx::https session;
session.verify_locations("ca-bundle.crt"); // rootCA 証明書のファイル名
session.start(argv[1], 443);

verify_locations は,引数を 2 つ取ります(第 2 引数のデフォルト値は NULL).第 2 引数は,rootCA 証明書が複数存在するような場合に,それらの rootCA 証明書が置かれているディレクトリへのパスを指定します.第 2 引数を指定した場合は,第 1 引数は NULL でもかまいません.

References - CLX C++ Libraries

現在のところ,SSL 通信関連ライブラリとして以下のものを実装しています.また,sockstream も使用することができます.

strptime の実装

文字列から日付・時刻の構造体 (struct tm) への変換にはこれまで strptime() を用いていたのですが,この関数は Windows には存在しないようです.なので,strptime() 相当のものを独自に実装して Windowsコンパイルする際には代替関数を使うようにしてみました(かなり手を抜いていますが).実装に使用したものは,主に以下の 2 つ.

time_get

time_get は 入力ストリームから日付と時刻を解析するための C++ 標準ライブラリです.今回は,このクラスの get_weekday() メソッドと get_monthday() を利用して,曜日の名前 -> その曜日を表す数値(月の名前 -> 月)の変換を行いました.例えば,“Friday”を 5 に変換するサンプルコードは以下の通りです(曜日は,日曜日を 0 として 0--6 の値で表される).

#include <ctime>
#include <iostream>
#include <locale>
#include <sstream>
#include <string>

int main(int argc, char* argv[]) {
    const std::time_get<char>& tg = std::use_facet<std::time_get<char> >(std::locale());

    std::stringstream s;
    s << "Friday";
    std::istreambuf_iterator<char> from(s);
    std::istreambuf_iterator<char> to;
    
    std::ios_base::iostate err = std::ios::goodbit;
    struct tm date;
    tg.get_weekday(from, to, s, err, &date);
    
    std::cout << date.tm_wday << std::endl;
    
    return 0;
}

get_monthname() もインターフェースは同じ.週名/月名は完全な名前,省略形どちらも指定可能なようです.解析するためにいったんストリームを作成しなければならないのが面倒.time_get のテンプレート引数を見ると以下のようになっていたので,第 2 引数を std::string::iterator にでもすれば行けるのかなと思ったのですが,get_weekday() に入力イテレータの他にストリーム自体も渡さないといけないようで・・・無理なのかなぁ.

template <
    typename CharT,
    typename InputIterator = istreambuf_iterator<CharT>
>
class time_get : public locale::facet, public time_base;

tokenmap

正確には,tokenmap + format_separator.tokenmap は,tokenizer のマップコンテナ版です.format_separator は,scanf() のように書式をもとにして文字列を分割するための TokenizerFunc なのですが,これを少し拡張して,トークンの他にそのトークンを切り出した書式の識別子も返すようにしました.これによって,例えば識別子に指定できる文字を"ymdHMS"の6文字に拡張して,tok['y'], tok['m'] のようにその識別子で各トークンにアクセスできるように実装しています.サンプルコードは以下の通り.

#include <iostream>
#include <string>
#include <vector>
#include "clx/tokenmap.h"

typedef clx::basic_tokenmap<clx::format_separator<char> > date_map;

int main(int argc, char* argv[]) {
    std::string dateset = "ymdHMS";
    std::string infmt = "%04y/%02m/%02d %2H:%02M:%02S";
    std::string sdate = "2006/06/06 17:27:00";
    std::cout << "Input format: " << infmt << std::endl;
    std::cout << "Sample: " << sdate << std::endl;
    
    // analyze time string.
    clx::format_separator<char> f(infmt, dateset);
    date_map m(f);
    m.assign(sdate);
    
    // print time with output format.
    for (date_map::const_iterator p = m.begin(); p != m.end(); p++) {
        std::cout << "map[" << p->first << "] = " << p->second << std::endl;
    }
    
    return 0;
}

詳細は,tokenmaptokenizer_func を参照して下さい.

tokenmap は元々,文字列から日付・時刻を解析するために作成したクラスでした.strptime() の存在を知ってからは使用していなかったのですが,JSON パーサを書く際に便利でした.何でも再利用な形にしておくと,そのうち役に立ちますね(労力とのトレードオフでしょうが).

xstrptime

さて,上記 2 つのクラスを利用して実装した strptime が以下のようになります.

#include <ctime>
#include <locale>
#include <iostream>
#include <sstream>
#include <string>
#include "clx/replace.h"
#include "clx/tokenmap.h"
#include "clx/tokenizer_func.h"
#include "clx/lexical_cast.h"

/* ------------------------------------------------------------------------- */
/*
 *  get_from_xxx
 *
 *  一旦ストリームを作成する必要があるので,その部分の操作を関数化.
 */
/* ------------------------------------------------------------------------- */
void get_from_monthname(const std::basic_string<char>& name, struct tm* dest,
    const std::time_get<char>& tg) {
    std::basic_stringstream<char> ss;
    ss << name;
    std::istreambuf_iterator<char> from(ss);
    std::istreambuf_iterator<char> to;
    std::ios_base::iostate err = std::ios::goodbit;
    tg.get_monthname(from, to, ss, err, dest);
}

void get_from_weekname(const std::basic_string<char>& name, struct tm* dest,
    const std::time_get<char>& tg) {
    std::basic_stringstream<char> ss;
    ss << name;
    std::istreambuf_iterator<char> from(ss);
    std::istreambuf_iterator<char> to;
    std::ios_base::iostate err = std::ios::goodbit;
    tg.get_weekday(from, to, ss, err, dest);
}

/* ------------------------------------------------------------------------- */
//  xstrptime
/* ------------------------------------------------------------------------- */
void xstrptime(const char* src, const char* fmt, struct tm* dest) {
    std::basic_string<char> tmp_fmt(fmt);
    clx::replace(tmp_fmt, std::basic_string<char>("%F"), std::basic_string<char>("%Y-%m-%d"));
    clx::replace(tmp_fmt, std::basic_string<char>("%T"), std::basic_string<char>("%H:%M:%S"));
    
    std::basic_string<char> dateset = "aAbByYmdeHkMSjuw";
    clx::format_separator<char> sep(tmp_fmt, dateset);
    clx::basic_tokenmap<clx::format_separator<char>, char, std::basic_string<char> > date(sep);
    date.assign(src);
    
    // Consider the value of '69 or less as 20XX.
    if (date.exist('y')) {
        int y = clx::lexical_cast<int>(date['y']);
        if (y < 70) dest->tm_year = y;
        else dest->tm_year = y + 100;
    }
    if (date.exist('Y')) dest->tm_year = clx::lexical_cast<int>(date['Y']) - 1900;
    if (date.exist('m')) dest->tm_mon  = clx::lexical_cast<int>(date['m']) - 1;
    if (date.exist('d')) dest->tm_mday = clx::lexical_cast<int>(date['d']);
    if (date.exist('e')) dest->tm_mday = clx::lexical_cast<int>(date['e']);
    if (date.exist('H')) dest->tm_hour = clx::lexical_cast<int>(date['H']);
    if (date.exist('k')) dest->tm_hour = clx::lexical_cast<int>(date['k']);
    if (date.exist('M')) dest->tm_min  = clx::lexical_cast<int>(date['M']);
    if (date.exist('S')) dest->tm_sec  = clx::lexical_cast<int>(date['S']);
    if (date.exist('j')) dest->tm_yday = clx::lexical_cast<int>(date['j']);
    if (date.exist('u')) dest->tm_wday = clx::lexical_cast<int>(date['u']) - 1;
    if (date.exist('w')) dest->tm_wday = clx::lexical_cast<int>(date['w']);
    
    // resolve month name
    const std::time_get<char>& tg = std::use_facet<std::time_get<char> >(std::locale());
    if (date.exist('b')) get_from_monthname(date['b'], dest, tg);
    else if (date.exist('B')) get_from_monthname(date['B'], dest, tg);
    
    // resolve week name
    if (date.exist('a')) get_from_weekname(date['a'], dest, tg);
    else if (date.exist('A')) get_from_weekname(date['A'], dest, tg);
}
$ ./test
Input string: Sat 17 Mar 2007 19:56:19 JST
Input format: %a %d %b %Y %H:%M:%S JST

Result
        • -
year: 107 mon: 2 day: 17 wday: 6 yday: 0 hour: 19 min: 56 sec: 19

ワイド文字と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形式データもパースし易くていいですね.

頻出文字カウントプログラム

C++での単語の出現頻度順に出力するプログラムで,ちょっと躓いた部分があったのでWebを眺めていると,どう書く?orgβ:コード中の文字の頻度分析と言うページを見つけました.面白そうだったので,私もC++でどこまで短く書けるか挑戦.“頻出単語”だとちょっとプログラムが長くなるので,お題の通り頻出文字で.

#include <algorithm>
#include <map>
#include <vector>
#include <string>
#include <iostream>
#include <iterator>
#include <cctype>

bool greater_count(const std::pair<char, int>& x, const std::pair<char, int>& y) {
    return x.second > y.second;
}

int main(int argc, char* argv[]) {
    std::istream_iterator<char> input(std::cin);
    std::istream_iterator<char> last;
    
    std::map<char, int> v;
    while (input != last) v[*input++]++;
    
    std::vector<std::pair<char, int> > tmp(v.begin(), v.end());
    std::sort(tmp.begin(), tmp.end(), greater_count);
    for (size_t i = 0; i < tmp.size(); i++) {
        if (std::isprint(tmp[i].first)) {
            std::cout << tmp[i].first << ": " << tmp[i].second << std::endl;
        }
    }
    
    return 0;
}

今の所,この辺りが限界(何となく書いたら上記のようになっただけなので,言うほど考えてはいませんが).標準入力からガンガン文字列が流されてくる事を想定したプログラムです.空白文字,タブ文字,改行文字は読み飛ばし.

awkのプログラムの短さに感動.そう言えば,以前FizzBuzz問題のワンライナープログラムコンテストが開かれてましたけど,これもワンライナーでいける言語あるのかな.ちょっとC++では無理そうですが.

調査は,boostが手元にあったのでそれで実施.ただし,ちょっと量が多かったので,boostディレクトリ直下のhppファイルに限定しています.

$ cat `ls ../boost_1_35_0/boost/*.hpp` | ./test > result.txt

e: 41887
t: 36679
a: 25011
r: 22817
s: 22136
n: 21827
i: 21617
o: 21410
_: 18978
p: 16365
...

上位10文字は上記の通り.記号だとアンダースコアが唯一ランクイン.これは,C++のクラスや変数の命名習慣がラクダ型(単語の最初を大文字にする)ではなくヘビ型(単語どうしをアンダースコアで繋ぐ)だからでしょう.後は,型を書かなければならない言語らしく,int, iteratorみたいな型を表す単語に含まれる文字が多いですね.ちなみに,括弧は24位と25位で“): 6311”,“(: 6297”でした.括弧の対応があってないってどういうこと・・・?boostのプログラムも傾向的には,どう書く?orgβ:コード中の文字の頻度分析での結果と同じようです.

ちなみに,肝心の探していた問題ですが,今の所スマートな回答が見つからず.何かと言うと,出現頻度順に表示するためにソートする必要があるのですがstd::mapはソート不可(キーを基にした二分木でソートして管理されているので崩せない)なのでどうしたものか,と言うものでした.

std::vector<std::pair<char, int> > tmp(v.begin(), v.end());

と,一旦std::vectorにコピーした後でソートすると言う方法に逃げたのですが,このコピーが気に入らない.

追記

boost::bimaps::bimap*1を使ってみては?と言う指摘を頂きました.

#include <iostream>
#include <iterator>
#include <string>
#include <functional>
#include <cctype>
#include <boost/bimap.hpp>
#include <boost/bimap/vector_of.hpp>

class pair_printer {
public:
    template <class Pair>
    void operator()(const Pair& x) {
        if (std::isprint(x.second)) {
            std::cout << x.second << ": " << x.first << std::endl;
        }
    }
};

int main(int argc, char* argv[]) {
    std::istream_iterator<char> input(std::cin);
    std::istream_iterator<char> last;
    
    using namespace boost::bimaps;
    bimap<char, vector_of<int> > v;
    while (input != last) v.left[*input++]++;
    
    v.right.sort();
    std::for_each(v.right.rbegin(), v.right.rend(), pair_printer());
    return 0;
}

キーと値どちらでもソート可能なmap(と言う認識でいいのかな?).これを使えば,ソートのために一旦vectorにコピーしなくても良くなるのでしょうか.

*1:bidirectional mapの略だそうです.

マルチプラットフォームなスレッドクラス

pthreadとwin32 threadのインターフェースを統一したライブラリです.インターフェースおよびpthread側の実装はboost::threadベース,win32 thread側の実装はいろいろなWebサイトを参考に行いました*1

実装したものは,thread,mutex (mutex, recursive_mutex),condition,call_onceです.劣化boost::threadですが,ヘッダファイルだけなのでboostのビルドが面倒なときに使う分には良いかな.pthreadを先に作ったせいで,win32 thread側は,インターフェースを合わせるために取り合えず作ったメソッドもいくつかあって少し不恰好です.もう少し,修正する必要がありそうです.

動作確認は,gcc 4.x.xとVisualStudio 2005で行っています.

使用関数(API)一覧

ものぐさ備忘録:スレッドの関数一覧(pthread, win32スレッド)の対応表が分かり易かったので,実装に使った関数一覧を載せてみます(各クラスのインターフェースは後述).

thread

機能(メソッド名) pthread win32 thread
スレッドの作成(コンストラクタ) pthread_create CreateThread
使い終わったスレッドidを開放する(デストラクタ) -*2 CloseHandle
自スレッドの終了(clx::exit) pthread_exit ExitThread
スレッド終了時に自動的に確保したリソース
を解放する(detach)
pthread_detach -*3
他スレッドの終了を待つ(join) pthread_join WaitForSingleObject
現在実行中のスレッドidを取得する(get_id) pthread_self GetCurrentThread

mutex,recursive_mutex

機能(メソッド名) pthread win32 thread
mutexの作成(コンストラクタ) pthread_mutex_init CreateMutex
mutexの破壊(デストラクタ) pthread_mutex_destroy CloseHandle
mutexのロックを取る(lock) pthread_mutex_lock WaitForSingleObject
mutexのロックを解除する(unlock) pthread_mutex_unlock ReleaseMutex

condition

conditionは,スレッド間での待受/通知制御を行うためのクラスです.

機能(メソッド名) pthread win32 thread
conditionの作成(コンストラクタ) pthread_cond_init CreateEvent
conditionの破壊(デストラクタ) pthread_cond_destroy CloseHandle
イベントが発生するまで待機する(wait) pthread_cond_wait WaitForSingleObject
同上,タイマー付き(timed_wait) pthread_cond_timedwait WaitForSingleObject
待機しているスレッドに通知する(notify_one) pthread_cond_signal SetEvent
待機している全てのスレッドに通知する(notify_all) pthread_cond_broadcast -*4

call_once

機能(関数名) pthread win32 thread
指定した関数を一度だけ実行する(call_once) pthread_once -*5

サンプルコード

スレッド作成時に渡せる関数(オブジェクト)は,

void function();

または,

class functor {
public:
    void operator()();
};

となります.pthread_createやCreateThreadでは引数を一つ渡せるのですが,実装の都合上,引数は渡せません.

mutex::scoped_lockは,コンストラクタでmutex.lock(),デストラクタでmutex.unlock()を呼ぶクラスです.lock()/unlock()を呼ぶ代わりに,mutexを引数にmutex::scoped_lockオブジェクトを作成することにより,スコープ範囲から抜けると同時にunlock()が呼ばれるので,unlock()のし忘れを防ぐことができます(boost::threadを参照).

call_once()は,関数ポインタの他に関数オブジェクトも指定することができます.ただし,mutexやconditionはコピー不可なクラスなので,指定した関数オブジェクトがmutexやconditionなどのコピー不可なメンバ変数を持っていた場合は,うまく機能しません.

#include <iostream>
#include <sstream>
#include <string>
#include <list>
#include <stdexcept>
#include "clx/thread.h"

/* ------------------------------------------------------------------------- */
//  trivial_queue
/* ------------------------------------------------------------------------- */
template <class Type>
class trivial_queue {
public:
    typedef Type value_type;
    
    trivial_queue() : list_(), mutex_(), not_empty_() {}
    virtual ~trivial_queue() {}
    
    void enqueue(const value_type& x) {
        clx::mutex::scoped_lock lock(mutex_);
        std::cout << "PSH: " << x << " (rest " << list_.size() << ")" << std::endl;
        list_.push_back(x);
        not_empty_.notify_one();
    }
    
    value_type dequeue() {
        clx::mutex::scoped_lock lock(mutex_);
        while (list_.empty()) {
            std::cout << "empty buffer" << std::endl;
            not_empty_.wait(lock);
        }
        value_type tmp = list_.front();
        std::cout << "POP: " << tmp << " (rest " << list_.size() << ")" << std::endl;
        list_.pop_front();
        return tmp;
    }
    
    void reset() {
        clx::mutex::scoped_lock lock(mutex_);
        list_.clear();
    }
    
private:
    std::list<value_type> list_;
    clx::mutex mutex_;
    clx::condition not_empty_;
};

trivial_queue<std::string> data_;
clx::once_flag once_ = CLX_ONCE_INIT;

/* ------------------------------------------------------------------------- */
//  init_data
/* ------------------------------------------------------------------------- */
void init_data() {
    std::cout << "reset data queue" << std::endl;
    data_.reset();
}

/* ------------------------------------------------------------------------- */
//  send_something
/* ------------------------------------------------------------------------- */
void send_something() {
    for (int i = 0; i < 10; i++) {
        clx::call_once(once_, init_data); // call_once test
        std::stringstream ss;
        ss << "element[" << i << "]";
        data_.enqueue(ss.str());
    }
}

/* ------------------------------------------------------------------------- */
//  recv_something
/* ------------------------------------------------------------------------- */
void recv_something() {
    std::string s;
    for (int i = 0; i < 10; i++) {
        clx::call_once(once_, init_data); // call_once test
        s = data_.dequeue();
    }
}

/* ------------------------------------------------------------------------- */
//  main
/* ------------------------------------------------------------------------- */
int main(int argc, char* argv[]) {
    clx::thread enq_th[2];
    clx::thread deq_th[2];
    
    // 第2引数がtrueの場合は,detach属性(joinで後処理を行う必要がない)
    for (size_t i = 0; i < 2; i++) enq_th[i].start(send_something, true);
    for (size_t i = 0; i < 2; i++) deq_th[i].start(recv_something, true);
    
    /*
     * detach属性の場合,作成したスレッドが仕事を終える前にメインスレッドが
     * 終了する可能性があるので,clx::exit(0)で待つ.
     */
    clx::exit(0);
}

実行結果

$ ./test
reset data queue
empty buffer
empty buffer
PSH: element[0] (rest 0)
PSH: element[0] (rest 1)
PSH: element[1] (rest 2)
PSH: element[1] (rest 3)
POP: element[0] (rest 4)
PSH: element[2] (rest 3)
POP: element[0] (rest 4)

・・・(以下略)・・・

インターフェース

全てのクラス,関数はclx名前空間の中に定義してあります.

/* ------------------------------------------------------------------------- */
//  thread
/* ------------------------------------------------------------------------- */
class thread {
public:
    thread();
    template <class Functor>
    explicit thread(Functor f, bool detached = false);
    virtual ~thread();
    
    template <class Functor>
    void start(Functor f, bool detached = false);
    
    bool joinable() const;
    void join();
    void detach();
    void sleep(double sec);
    
    handle_id_type get_id();
};

/* ------------------------------------------------------------------------- */
/*
 *  mutex
 *
 *  recursive_mutexもインターフェースは同じ
 */
/* ------------------------------------------------------------------------- */
class mutex {
public:
    typedef unique_lock<mutex> scoped_lock;
    typedef ... handle_pointer;
    
    mutex();
    virtual ~mutex();
    
    void lock();
    void unlock();
    handle_pointer native_handle();
};

/* ------------------------------------------------------------------------- */
//  condition
/* ------------------------------------------------------------------------- */
class condition {
public:
    typedef ... handle_pointer;
    
    condition();
    virtual ~condition();
    
    template <class LockT>
    void wait(LockT& lock);
    
    template <class LockT>
    bool timed_wait(LockT& lock, double sec);
    
    void notify_one();
    void notify_all();
    handle_pointer native_handle();
};

/* ------------------------------------------------------------------------- */
//  call_once
/* ------------------------------------------------------------------------- */
template <class Functor>
void call_once(once_flag& flag, Functor f);

*1:win32 thread側の実装もboost::threadベースにしようと思ったのですが,ソースを追いきれなかったorz

*2:開放する必要なし

*3:デフォルトでこの設定

*4:取り合えず,インターフェースを合わせるためにnotify_oneと同じ実装にしている

*5:InterlockedIncrementを利用して実装している