TokyoCabinet を C++ 上で template とキャスト演算子を使ってエレガントにラップする

先日友人に TokyoCabinet というものを教えてもらいました。今まで DBM というものを知りませんでした。これはハッシュをファイルシステム上に実装したものです。ハッシュの巨大化にも耐えられ、かつメモリにキャッシュすることでオンメモリと遜色ないくらいに高速に動くことを期待する、そういうライブラリです。プログラムからは単なるハッシュのように見えます。RDB ほど複雑な要求はないけど、RDB よりも高いメモリ効率や高い実効性能が欲しいときに使えます。

データを全部なめるような場合にはキャッシュは関係ないので、プログラムがちょっと簡単にかけるかもしれない以上の意味はないでしょう。そもそもメモリに乗るくらい小さい場合はハッシュとシリアライズで十分、かつその方が高速でしょう。複雑なクエリが必要な場合は RDB を使えばいいでしょう。単純なハッシュで十分なんだけど、データが巨大すぎて困る場合に欲しくなります。実は、Google N-gram のデータが巨大すぎて困ってて、ファイルシステム上にハッシュを作るような仕組みを考えてたんですが、まさにこれです。といっても、もう卒業しちゃったのでいつ使うかは不明。

TokyoCabinet は数ある DBM 実装の中でも高速、コンパクトに動く新しいライブラリです。全文検索エンジン HyperEstraier で使われている DBM である、QDBM の改良版とのこと。高速アクセスが必須である全文検索エンジンに、現実的に使われている*1という信頼感がいいですね。

さて、今日はこれを使って遊んでみましたというお話。DBM はハッシュだよ、ということを実証するために、operator [] とキャスト演算子オーバーロードを使って、hashtbl の代替になるようにラッパを作ってみましたということです。完璧な代替はそもそもムリですが、ある程度それっぽい。


以下が tcdb.hpp

#pragma once

#include <tcbdb.h>
#include <string.h>
#include <string>

namespace tcpp {

using namespace std;

typedef pair<const void*, int> key_t;

template <typename T>
key_t make_key(const T&);

template <typename T>
const T conv_key(const key_t&);

class DB {
public:
  DB() { bdb = tcbdbnew(); }
  
  void open(const string& filename, int open_mode) {
    tcbdbopen(bdb, filename.c_str(), open_mode);
  }
  
  void close() { tcbdbclose(bdb); }

  void put(const key_t& key, const key_t& val) {
    tcbdbput(bdb, key.first, key.second, val.first, val.second);
  }
  
  key_t get(const key_t& key) {
    int size;
    void* buf = tcbdbget(bdb, key.first, key.second, &size);
    return make_pair(buf, size);
  }
private:
  TCBDB *bdb;
};

template <typename K, typename V>
class Cell {
public:
  Cell(DB& d, const key_t& k) : db(d), key(k) {}
  Cell& operator = (const V& v) {
    db.put(key, make_key(v));
    return *this;
  }

  operator V() const {
    return conv_key<V>(db.get(key));
  }
private:
  DB& db;
  const key_t key;
};


template <typename K, typename V>
class DBMap {
public:
  DBMap(DB& d) : db(d) {}
  
  const Cell<K, V> operator[](const K& key) const {
    return Cell<K, V>(db, make_key(key));
  }
  
  Cell<K, V> operator[](const K& key) {
    return Cell<K, V>(db, make_key(key));
  }
private:
  DB& db;
};

////////////////////////////////////////////////////////////
// data converter
template <>
inline key_t make_key<int>(const int& i) {
  return make_pair(&i, sizeof(int));
}

template <>
inline const int conv_key<int>(const key_t& k) {
  return *((int*)k.first);
}

typedef const char* CSTR;
template <>
inline key_t make_key<CSTR>(const CSTR& s) {
  return make_pair(s, strlen(s));
}

template <>
inline const CSTR conv_key<CSTR>(const key_t& k) {
  return (char*)k.first;
}

}

DB が thin wrapper で、DBMap が map 実装。Cell が今回の肝で、見ての通り DBMap の operator[] が Cell のインスタンスを返します。Cell は operator = で TokyoCabinet の put 関数を、operator V() で get 関数を呼ぶ寸法です。わかりやすいと言えばわかりやすい。ふつうの map のように V& をとることはできません。当然といえば当然。これよく見たら、Cell の K 使ってないな・・・。

元々 void* を key と value に使えるのですが、静的に型付けるために DBMap は template にしました。任意の型 T から void* への関数(make_key)と、void* から T への関数(conv_key)を、関数の特殊化を使って用意します。これで任意の型の間の map をエレガントに実現できます。特殊化してない型を使うと、コンパイルエラーが起きます。見ての通り make_key の中で T のコピーを取っていないなど、ポインタの扱いはええかげんにしています。たぶん、変な使い方すると落ちるよ(ぉ

このライブラリを使ったサンプルがこちら。

#include <tcbdb.h>
#include <iostream>
#include "tcdb.hpp"

int main() {
  using namespace std;
  using namespace tcpp;
 
  tcpp::DB db;
  db.open("hoge.db", BDBOWRITER | BDBOCREAT);

  tcpp::DBMap<const char*, int> m(db);
  m["hoge"] = 3;
  cout << m["hoge"] << endl;

  db.close();
}
$ g++ sample.cpp -ltokyocabinet -lz

まぁ、だからなんだといわれると、だからなんなんでしょう。実際に使うか? 使わないんじゃない(ぉ

*1:mixi