世の中には二種類の文字列がある…

今回の内容は,0 を終端とする文字列と,長さを別に持つ文字列の扱いに関する小ネタです.

検索用のインタフェースを用意するとき,どちらか一方を選択するのであれば,後者にすると思います.しかし,できれば前者も提供したいと思うのが人情というものでしょう.

という理由から,marisa-trie では,どちらも使えるようになっています.とはいえ,別々に実装するのは甚だ面倒です.検索用の関数は lookup(), find() 系列, predict() 系列と大量にあり,ほとんど同じになるものを別々に実装するなんて正気の沙汰ではありません.以降の修正でどちらか一方だけ直し忘れたなんてのも,実にありそうな話です.

シンプルな対策は,0 を終端とする文字列を受け取ったとき,まず長さを求めてやるというものです.しかし,長い文字列が与えられる可能性がある場合,この手は危険です.100MB くらいのテキストが渡されたら…みたいなことを考えるとわかりやすいと思います.あまりなさそうな事例ではありますが,マーフィーの法則を忘れてはいけません.

そこで,marisa::CQuery, marisa::Query というクラスを利用しています.メンバは i 番目の文字を取り出すための関数 operator[]() と,i 番目が終端に位置するかどうかを返す関数 ends_at() だけというつまらないクラスですが,テンプレート引数として受け取るようにすれば,二種類の文字列を同じように参照することができます.

class CQuery {
 public:
  explicit CQuery(const char *str);
  CQuery(const CQuery &query);

  UInt8 operator[](std::size_t i) const;
  bool ends_at(std::size_t i) const;

 private:
  const char *str_;
};

class Query {
 public:
  Query(const char *ptr, std::size_t length);
  Query(const Query &query);

  UInt8 operator[](std::size_t i) const;
  bool ends_at(std::size_t i) const;

 private:
  const char *ptr_;
  std::size_t length_;
};

void Func(const char *query) {
  InternalFunc<CQuery>(CQuery(query));
}

void Func(const char *query_ptr, std::size_t query_length) {
  InternalFunc<const Query &>(Query(query_ptr, query_length));
}

template <typename T>
void InternalFunc(T query) {
  ...;
}

Func() は引数に応じて marisa::CQuery と marisa::Query を選択するようになっています.後は,InternalFunc() を operator[]() と ends_at() を使って実装するのみです.

# CQuery はメンバがポインタ一つなので値渡し,Query はメンバがポインタと長さの二つなので参照渡しにしています.