LoginSignup
88
73

More than 5 years have passed since last update.

C++ラッパーを書く ~ C++の複雑怪奇な言語仕様を理解することによるメリット

Last updated at Posted at 2017-12-29

この記事は 初心者C++er Advent Calendar 2017 23日目の(枠を埋める)記事になります1.諸事情あって2某AdCを遅刻してたら枠を追い剥ぎされて3暇になってしまったので,私も無理言って遅刻してた方から枠をいただきました.
 本記事の想定読者は「C使える,C++も使える,でも C++の中身はよく知らない 」ぐらいのC++初心者です.想定読者が脱初心者する一助となることを願っています.


 皆さんも「C++は難しい」という言説を見かけたこと,一度くらいはあると思います4.しかし,実際にアプリケーションを書く程度であれば,CなんかよりもC++の方がずっと楽です.例えばRAIIが使えたり,オーバーロードが使えたり,classが使えたりします5.確かにCより機能が豊富な分覚えなきゃいけないことはちょっと増えるけど,そんなに難しいかな…?なんて思ってる方もいらっしゃるかもしれません.今回は,そんな「C++使える」から「C++の気持ちがちょっとわかる」になるための,脱初心者的なお話です.

世間の「C++」

この章は半分くらいポエムです.
 C++は学習がしにくい,というのは割りとよく言われていることであります6.近年はcpprefjpみたいなまとまったサイトもできてきてますし,実を言えばこの8年くらいでC++の言語機能やライブラリそれ自体を深く学ぶための資料7はかなり充実しました.また,中級者のレベルアップに最適な情報も結構出てたりします8.一方,初心者が「C++を使う」という立場で一から学ぶためのまとまった資料というのは,実はほとんど無いのではないか,というのが私見です9.「C++ 入門」で検索すれば,大抵のサイトが「オブジェクト指向」だの「クラス」だの「コンストラクタ/デストラクタ」だのを割と序盤で投げつけてきますし,「new/delete」とかも題目に並びます.「using namespace std」を推奨してるサイトなんかもありますね.これが世間における「C++」です.でも,オブジェクト指向なんて分からなくてもC++でプログラムを書くことはできます10.アプリケーションレベルまで持っていくとなると自作のクラスも作れたほうが都合は良いですが,クラスを作る前にまずありもののクラスを使ってその便利さや構成を学ばないと,ほぼ確実にヤバいクラスが生まれてしまいます11.C++14以降,newdeleteを書かなければいけないプログラマは激減しました12using namespace stdは,その功罪を正しく認識しないで使うとどこかで痛い目を見ます(つまり右も左も分からない初心者に勧めるべき存在ではないのです).世間の「C++」は,「C言語にとって目新しい言語機能とオブジェクト指向」であり,それは15年以上前の話で,そして未だにその頃の入門情報しか日本語では残っていないのです.
 近年のC++でも言語機能が多数導入されましたが,こうした言語機能それぞれを学んでも初心者にはそれら知識を繋げていく線を引くことができず,点のまま終わってしまうのではないかなぁと思います.なぜなら,言語機能だけを学んでもC++の気持ちを理解していないからです.世間の「C++」は,CとかJavaの気持ちでC++を使っているところが多いです(あと古い).そうではなく,C++をC++らしく使うことが大切だと思います.
 では,何故世のC++er13は「てんぷれーと」とか「ちゅうさんじょしこんすとえくすぷれっしょん」とか「めたぷろぐらみんぐ」とかよく分からない話をしていて,言語機能やライブラリに関する資料が充実して,「C++を使う」観点での入門資料が増えないのか.それは,そうした人たちは「C++を使う」立場から「C++の使われる部分を作る」立場へと移行したからです.入門者が言語機能の多くよりもC++の気持ちを理解すべきなのは言語機能やライブラリの詳細を理解しなくてもある程度アプリケーションが書けるから14(一方で,気持ちを理解せずに書くとC++的に非常に気持ち悪く扱いにくいプログラムができあがるから)ですが,そのライブラリは誰が作るのかという話になります.ライブラリを作っているのが世のC++erであり,そしてそうしたライブラリを作る,つまり「C++をC++らしく使えるようにする」ために必要なのが,言語機能への深い理解,ということになります15.世のC++erは自分たちにとって重要で役に立つ内容をたくさん公開しているのですが,これが入門レベルの初心者にとって優先的に学ぶべき内容か,と言われるとそれは違うのではないか,というのが私の主張です.ところで,本記事もそうした入門レベルの人向けの内容ではなく,むしろそうした「使う」レベルからの脱却,「使われる部分を作る」,C++の言語機能を理解することがどういったメリットを生むのか,といった内容です.これだけあーだこーだ言っておきながら自ら入門者を捨て置く非道
 というわけで,この記事ではC++の気持ちの一部をかるーく紹介して16,世間の「C++」をC++にします17

C++の気持ち

「C++らしさ」みたいなものを掴んでもらうために,いくつかお気持ちを表明します.お気持ち表明なので,このあたりまでは初心者でも雰囲気だけ追っかけて読めると思います.

C++では無闇にnewしない

C++14以降,ユーザーコード中でnewを記述する機会はほとんどなくなりました18.現代ではstd::make_uniquestd::make_sharedを使います19

C++では解放しない

C++ではあまり解放処理をユーザーコードに書きません.例えば,newに対応したdeleteを書かない,ということです.尤も,実際にnewを書いてしまった場合はdeleteを自分で書かなければならないのですが20,ここでstd::make_uniquestd::make_sharedが出てきます.こいつらで動的に確保したメモリは自動でdeleteされるので,newを手書きした場合とは異なり解放処理を自分で書く必要がありません

C++03時代
#include<iostream>

int* f(){
  int* ptr = new int; //newしたら
  *ptr = 30;
  return ptr;
}

void g(){
  int* p = f();
  std::cout << *p << std::endl;
  delete p; //deleteしないといけない
}
C++14以降
#include<iostream>
#include<memory> //std::make_sharedのために必要

auto f(){
  auto ptr = std::make_shared<int>(); //make_sharedしても
  *ptr = 30;
  return ptr;
}

void g(){
  auto p = f();
  std::cout << *p << std::endl;
} //gが終了した時点でp(=fのptr)が解放される

このように,手で解放処理を書かなくても自動で解放される仕組みがC++にはあります21.メモリの動的確保以外には,ファイル入出力のstd::fstreamclose()メンバ関数を呼ばなくても勝手にファイルを閉じてくれたりします.積極的に使っていくべきです.逆説的に,std::make_uniquestd::make_sharedなどで書ける部分についてはnewで書かないようにしましょう.

C++では同じ型のデータの集まりをRangeで処理する

この節は恐らく議論の別れるところでしょう.というのも,非常に残念なことに未だにRangeはC++標準規格でまともにサポートされていないからです.Rangeという概念の登場から10年以上経っているにも関わらず,です.Rangeの導入の障害になるならConceptの導入を見送れって無限に言ってる.
 与太話はさておき,Rangeというのは「こういう操作ができる何か」という概念です.細かい話は後述しますが,例えば配列はRangeです.std::vectorとかstd::listもRangeです.与太話の通り,現状標準ではRangeのサポートがほとんど無いので恩恵が受けにくいのですが,唯一C++11で入ったRange関連の機能が使えます.Range-based forといいます.

C++03時代
int arr[100]; //配列(Range)
for(int* it = arr; it != arr + 100; ++it) //for文書くのが面倒
  std::cout << *it << std::endl; //ポインタのデリファレンスも面倒
C++11以降
int arr[100]; //配列(Range)
for(auto&& x : arr) //これだけで「Rangeの中身全部について,各要素をxとおいて」みたいな意味になる
  std::cout << x << std::endl; //xはRangeの要素そのものなのでデリファレンスも不要
C++03時代
std::vector<int> vec; //std::vector(Range)
for(std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) //for文書くのが面倒
  std::cout << *it << std::endl; //イテレーターのデリファレンスも面倒
C++11以降
std::vector<int> vec; //std::vector(Range)
for(auto&& x : vec) //これだけで「Rangeの中身全部について,各要素をxとおいて」みたいな意味になる
  std::cout << x << std::endl; //xはRangeの要素そのものなのでデリファレンスも不要

まぁ,現状だとこれだけと言えばこれだけなんですけど…とはいえ,一度Rangeに慣れてくるとRangeじゃない構造が嫌になってきます.なんだかんだで便利です.また,C++20辺りでRangeのサポートがようやく入ると言われており,それらが入ればRangeの利便性はグッと増します.今のうちにRangeの構造を作っておくことは将来的に得です.

本題 : 「C++」をC++にする

さて,それでは先述のお気持ちに沿って世間の「C++」をC++にしていきたいと思います.

題材

今回の題材は,形態素解析エンジンMeCabのC++ライブラリインターフェースです.以下はMeCabの公式ページより一部抜粋したC++ライブラリインターフェースのサンプルコードとなります.

MeCabのC++のサンプルコード(一部抜粋)
#include <iostream>
#include <mecab.h>

#define CHECK(eval) if (! eval) { \
   const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
   std::cerr << "Exception:" << e << std::endl; \
   delete tagger; \
   return -1; }

int main (int argc, char **argv) {
  char input[1024] = "太郎は次郎が持っている本を花子に渡した。";

  // Create model object.
  MeCab::Model *model = MeCab::createModel(argc, argv);

  // Create Tagger
  // All taggers generated by Model::createTagger() method share
  // the same model/dictoinary.
  MeCab::Tagger *tagger = model->createTagger();
  CHECK(tagger);

  // Create lattice object per thread.
  MeCab::Lattice *lattice = model->createLattice();

  // Gets tagged result in string
  lattice->set_sentence(input);

  // this method is thread safe, as long as |lattice| is thread local.
  CHECK(tagger->parse(lattice));
  std::cout << lattice->toString() << std::endl;

  // Gets node object.
  const MeCab::Node* node = lattice->bos_node();
  CHECK(node);
  for (; node; node = node->next) {
    std::cout << node->id << ' ';
    if (node->stat == MECAB_BOS_NODE)
      std::cout << "BOS";
    else if (node->stat == MECAB_EOS_NODE)
      std::cout << "EOS";
    else
      std::cout.write (node->surface, node->length);

    std::cout << ' ' << node->feature
          << ' ' << (int)(node->surface - input)
          << ' ' << (int)(node->surface - input + node->length)
          << ' ' << node->rcAttr
          << ' ' << node->lcAttr
          << ' ' << node->posid
          << ' ' << (int)node->char_type
          << ' ' << (int)node->stat
          << ' ' << (int)node->isbest
          << ' ' << node->alpha
          << ' ' << node->beta
          << ' ' << node->prob
          << ' ' << node->cost << std::endl;
  }


  // begin_nodes/end_nodes
  const size_t len = lattice->size();
  for (int i = 0; i <= len; ++i) {
    MeCab::Node *b = lattice->begin_nodes(i);
    MeCab::Node *e = lattice->end_nodes(i);
    for (; b; b = b->bnext) {
      printf("B[%d] %s\t%s\n", i, b->surface, b->feature);
    }
    for (; e; e = e->enext) {
      printf("E[%d] %s\t%s\n", i, e->surface, e->feature);
    }
  }

  //(中略)

  // Dictionary info
  const MeCab::DictionaryInfo *d = model->dictionary_info();
  for (; d; d = d->next) {
    std::cout << "filename: " <<  d->filename << std::endl;
    std::cout << "charset: " <<  d->charset << std::endl;
    std::cout << "size: " <<  d->size << std::endl;
    std::cout << "type: " <<  d->type << std::endl;
    std::cout << "lsize: " <<  d->lsize << std::endl;
    std::cout << "rsize: " <<  d->rsize << std::endl;
    std::cout << "version: " <<  d->version << std::endl;
  }

  // Swap model atomically.
  MeCab::Model *another_model = MeCab::createModel("");
  model->swap(another_model);

  delete lattice;
  delete tagger;
  delete model;

  return 0;
}

「C言語の構造体と関数をクラスにまとめればC++でしょ?」って感じのインターフェースですね…22C++の気持ちガン無視です23
 というわけで,今回はMeCabのC++ライブラリインターフェースのラッパーライブラリを書いて,MeCabを使いやすくしてみます.

ラッパーライブラリって?

MeCab以外にも,C言語のライブラリとか,OSのAPIとか…世の中にはいろんな使いにくいライブラリがあります.そうしたライブラリが使いにくいのは,主にインターフェースが悪い,ということに起因します.例えば,C++で言えばRange-based forとかRAIIとか便利で標準的な記法があるのに,使いにくいライブラリがそういった機能に対応してない(同じ型のデータの集まりをRangeではない形式で提供している,確保と解放をユーザーに手動でやらせる)ということです.逆に言えば,使い心地以外の面では機能的には問題がないわけで,インターフェースだけ使いやすいものにしてあげればなんとかなりそうです.ラッパーライブラリとは,こうした「ライブラリそれ自体が機能を持つわけではなく,他のライブラリに覆いかぶさって(他のライブラリをラップして)使いやすいインターフェースを提供するライブラリ」のことです.
 ラッパーライブラリを作るためには,その言語で使いやすいインターフェースを知り,既存のライブラリのインターフェースからどのようにしてそのインターフェースに合わせるかを考え,それを実現しなければなりません.これには言語仕様などへの理解が不可欠です.逆説的に,言語仕様への理解があれば世の中の使いにくいライブラリを我慢して使わなくても自力で使いやすいインターフェースに変えることができます.これは言語仕様を理解するメリットの1つとして挙げられると思います.あ,これこの記事の結論ですので,真面目に上から読んできたけどこの後でわかんねーなーってなった方はいつでも安心してブラウザバックしてくださって大丈夫です.
 ラッパーライブラリはライブラリとしての機能を自分で考える必要が無い一方で,ライブラリとしてのユーザーインターフェースについての考察や言語仕様の理解などが鍛えられる上,世の中には数多のクソインターフェースライブラリが跋扈しているので題材に困らず,ライブラリ製作の入り口としておすすめです.ちなみに私が作っている物としては,Windows APIのラッパーライブラリであるwillがあります(宣伝).

実際にラップしてみる

それでは実際にMeCabのC++ライブラリインターフェースをラップしていきます.この章から結構ガッツリC++を書きますが,classの書き方などは知っていることを前提としているので少々難易度が高めかもしれません.

解放を自動化する

とりあえずdelete24mainで書くのがダサい25ので,これを自動化します.ところで,型Tに対してnew Tの代わりにstd::make_unique<T>()が使える,というような話でしたが,今回ユーザーコードにnewの文字はありません.どこを置き換えればよいのでしょうか.
 実は,std::make_unique<T>()関数はstd::unique_ptr<T>というクラスのオブジェクトを作ります(今回は説明しませんが,std::make_shared<T>も同様にstd::shared_ptr<T>のオブジェクトを作ります).このstd::unique_ptr<T>が自動解放などの実権を握っているので,以下のように直接T*の代わりとしてstd::unique_ptr<T>を使ってしまえば解決します.

deleteを消す
#include <iostream>
#include <memory>
#include <mecab.h>

#define CHECK(eval) if (! eval) { \
   const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
   std::cerr << "Exception:" << e << std::endl; \
   return -1; }

int main (int argc, char **argv) {
  char input[1024] = "太郎は次郎が持っている本を花子に渡した。";

  // Create model object.
  std::unique_ptr<MeCab::Model> model{MeCab::createModel(argc, argv)};

  // Create Tagger
  // All taggers generated by Model::createTagger() method share
  // the same model/dictoinary.
  std::unique_ptr<MeCab::Tagger> tagger{model->createTagger()};
  CHECK(tagger);

  // Create lattice object per thread.
  std::unique_ptr<MeCab::Lattice> lattice{model->createLattice()};

  // Gets tagged result in string
  lattice->set_sentence(input);

  // this method is thread safe, as long as |lattice| is thread local.
  CHECK(tagger->parse(lattice.get()));
  std::cout << lattice->toString() << std::endl;

  // Gets node object.
  const MeCab::Node* node = lattice->bos_node();
  CHECK(node);
  for (; node; node = node->next) {
    std::cout << node->id << ' ';
    if (node->stat == MECAB_BOS_NODE)
      std::cout << "BOS";
    else if (node->stat == MECAB_EOS_NODE)
      std::cout << "EOS";
    else
      std::cout.write (node->surface, node->length);

    std::cout << ' ' << node->feature
          << ' ' << (int)(node->surface - input)
          << ' ' << (int)(node->surface - input + node->length)
          << ' ' << node->rcAttr
          << ' ' << node->lcAttr
          << ' ' << node->posid
          << ' ' << (int)node->char_type
          << ' ' << (int)node->stat
          << ' ' << (int)node->isbest
          << ' ' << node->alpha
          << ' ' << node->beta
          << ' ' << node->prob
          << ' ' << node->cost << std::endl;
  }


  // begin_nodes/end_nodes
  const size_t len = lattice->size();
  for (int i = 0; i <= len; ++i) {
    MeCab::Node *b = lattice->begin_nodes(i);
    MeCab::Node *e = lattice->end_nodes(i);
    for (; b; b = b->bnext) {
      printf("B[%d] %s\t%s\n", i, b->surface, b->feature);
    }
    for (; e; e = e->enext) {
      printf("E[%d] %s\t%s\n", i, e->surface, e->feature);
    }
  }

  //(中略)

  // Dictionary info
  const MeCab::DictionaryInfo *d = model->dictionary_info();
  for (; d; d = d->next) {
    std::cout << "filename: " <<  d->filename << std::endl;
    std::cout << "charset: " <<  d->charset << std::endl;
    std::cout << "size: " <<  d->size << std::endl;
    std::cout << "type: " <<  d->type << std::endl;
    std::cout << "lsize: " <<  d->lsize << std::endl;
    std::cout << "rsize: " <<  d->rsize << std::endl;
    std::cout << "version: " <<  d->version << std::endl;
  }

  // Swap model atomically.
  std::unique_ptr<MeCab::Model> another_model{MeCab::createModel("")};
  model->swap(another_model.get());

  return 0;
}

これでdeleteが駆逐できました.解放漏れもないので安心ですね!一部std::unique_ptrではなく本物のポインタが必要な場所は適宜get()メンバ関数を呼んでいます.
 ちなみに,MeCab::Node*MeCab::DictionaryInfo*などはまだ残っていますが,これらはstd::unique_ptrにはしません.これらはdeleteしない(してない)からです.

Windows対応

実はこのサンプルコード,Windowsだと正しく解放されないです.MeCabのC++ライブラリインターフェースのドキュメントに「In some environment, e.g., MS-Windows, an object allocated inside a DLL must be deleted in the same DLL too. (筆者訳: MS-Windowsなどの幾つかの環境では,DLL内で動的に確保されたオブジェクトは同一DLL内でdeleteされなければいけません)」とあるように,Windowsでも正しく動かすためにはdeleteの代わりにMeCab::deleteModel()などを使わなければいけないのです26.でも,std::unique_ptr<T>deleteを自動化するもの.delete以外の自動化なんてできるのでしょうか?
 実は,std::unique_ptrstd::shared_ptrにはカスタムデリータという機能があり,解放処理をdeleteから好きな関数に変更することができます.なので,以下のように変更すればWindows対応も簡単にできます.

Windows対応版
#include <iostream>
#include <memory>
#include <mecab.h>

#define CHECK(eval) if (! eval) { \
   const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
   std::cerr << "Exception:" << e << std::endl; \
   return -1; }

int main (int argc, char **argv) {
  char input[1024] = "太郎は次郎が持っている本を花子に渡した。";

  // Create model object.
  std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)> model{MeCab::createModel(argc, argv), &MeCab::deleteModel};

  // Create Tagger
  // All taggers generated by Model::createTagger() method share
  // the same model/dictoinary.
  std::unique_ptr<MeCab::Tagger, decltype(&MeCab::deleteTagger)> tagger{model->createTagger(), &MeCab::deleteTagger};
  CHECK(tagger);

  // Create lattice object per thread.
  std::unique_ptr<MeCab::Lattice, decltype(&MeCab::deleteLattice)> lattice{model->createLattice(), &MeCab::deleteLattice};

  // Gets tagged result in string
  lattice->set_sentence(input);

  // this method is thread safe, as long as |lattice| is thread local.
  CHECK(tagger->parse(lattice.get()));
  std::cout << lattice->toString() << std::endl;

  // Gets node object.
  const MeCab::Node* node = lattice->bos_node();
  CHECK(node);
  for (; node; node = node->next) {
    std::cout << node->id << ' ';
    if (node->stat == MECAB_BOS_NODE)
      std::cout << "BOS";
    else if (node->stat == MECAB_EOS_NODE)
      std::cout << "EOS";
    else
      std::cout.write (node->surface, node->length);

    std::cout << ' ' << node->feature
          << ' ' << (int)(node->surface - input)
          << ' ' << (int)(node->surface - input + node->length)
          << ' ' << node->rcAttr
          << ' ' << node->lcAttr
          << ' ' << node->posid
          << ' ' << (int)node->char_type
          << ' ' << (int)node->stat
          << ' ' << (int)node->isbest
          << ' ' << node->alpha
          << ' ' << node->beta
          << ' ' << node->prob
          << ' ' << node->cost << std::endl;
  }


  // begin_nodes/end_nodes
  const size_t len = lattice->size();
  for (int i = 0; i <= len; ++i) {
    MeCab::Node *b = lattice->begin_nodes(i);
    MeCab::Node *e = lattice->end_nodes(i);
    for (; b; b = b->bnext) {
      printf("B[%d] %s\t%s\n", i, b->surface, b->feature);
    }
    for (; e; e = e->enext) {
      printf("E[%d] %s\t%s\n", i, e->surface, e->feature);
    }
  }

  //(中略)

  // Dictionary info
  const MeCab::DictionaryInfo *d = model->dictionary_info();
  for (; d; d = d->next) {
    std::cout << "filename: " <<  d->filename << std::endl;
    std::cout << "charset: " <<  d->charset << std::endl;
    std::cout << "size: " <<  d->size << std::endl;
    std::cout << "type: " <<  d->type << std::endl;
    std::cout << "lsize: " <<  d->lsize << std::endl;
    std::cout << "rsize: " <<  d->rsize << std::endl;
    std::cout << "version: " <<  d->version << std::endl;
  }

  // Swap model atomically.
  std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)> another_model{MeCab::createModel(""), &MeCab::deleteModel};
  model->swap(another_model.get());

  return 0;
}

std::unique_ptrの場合は,まずstd::unique_ptr<T, D>という形に変更します.この時,Dはカスタムデリータの型を書きます.今回はカスタムデリータとしてMeCab::deleteModelなどを使うので,これらの型を書きます.でも,型を調べて書くのは面倒なので,今回はC++11から入ったdecltypeという機能を使います.なんと,decltype(MeCab::deleteModel)と書くとMeCab::deleteModelの型が取れるのです!便利ですね!でもC言語の関数の仕様のせいでdecltype(MeCab::deleteModel)をそのまま書くとコンパイルが通らないので今回はdecltype(&MeCab::deleteModel)と書きます27.そして,今まではポインタだけを渡していましたが,これと&MeCab::deleteModelをセットで渡すようにすればOKです.
 でも毎回&MeCab::deleteModelとか書きまくるのが大変なので28,さらにこれをクラスにラップします29

クラスにラップ
#include <iostream>
#include <mecab.h>

#define CHECK(eval) if (! eval) { \
   const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
   std::cerr << "Exception:" << e << std::endl; \
   return -1; }

namespace mecab{

struct model : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{
  model(int argc, char** argv) : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{MeCab::createModel(argc, argv), &MeCab::deleteModel}{}
  model(const char* arg) : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{MeCab::createModel(arg), &MeCab::deleteModel}{}
};

struct tagger : std::unique_ptr<MeCab::Tagger, decltype(&MeCab::deleteTagger)>{
  tagger(MeCab::Tagger* ptr) : std::unique_ptr<MeCab::Tagger, decltype(&MeCab::deleteTagger)>{ptr, &MeCab::deleteTagger}{}
};

struct lattice : std::unique_ptr<MeCab::Lattice, decltype(&MeCab::deleteLattice)>{
  lattice(MeCab::Lattice* ptr) : std::unique_ptr<MeCab::Lattice, decltype(&MeCab::deleteLattice)>{ptr, &MeCab::deleteLattice}{}
};

}

int main (int argc, char **argv) {
  char input[1024] = "太郎は次郎が持っている本を花子に渡した。";

  // Create model object.
  mecab::model model(argc, argv);

  // Create Tagger
  // All taggers generated by Model::createTagger() method share
  // the same model/dictoinary.
  mecab::tagger tagger = model->createTagger();
  CHECK(tagger);

  // Create lattice object per thread.
  mecab::lattice lattice = model->createLattice();

  // Gets tagged result in string
  lattice->set_sentence(input);

  // this method is thread safe, as long as |lattice| is thread local.
  CHECK(tagger->parse(lattice.get()));
  std::cout << lattice->toString() << std::endl;

  // Gets node object.
  const MeCab::Node* node = lattice->bos_node();
  CHECK(node);
  for (; node; node = node->next) {
    std::cout << node->id << ' ';
    if (node->stat == MECAB_BOS_NODE)
      std::cout << "BOS";
    else if (node->stat == MECAB_EOS_NODE)
      std::cout << "EOS";
    else
      std::cout.write (node->surface, node->length);

    std::cout << ' ' << node->feature
          << ' ' << (int)(node->surface - input)
          << ' ' << (int)(node->surface - input + node->length)
          << ' ' << node->rcAttr
          << ' ' << node->lcAttr
          << ' ' << node->posid
          << ' ' << (int)node->char_type
          << ' ' << (int)node->stat
          << ' ' << (int)node->isbest
          << ' ' << node->alpha
          << ' ' << node->beta
          << ' ' << node->prob
          << ' ' << node->cost << std::endl;
  }


  // begin_nodes/end_nodes
  const size_t len = lattice->size();
  for (int i = 0; i <= len; ++i) {
    MeCab::Node *b = lattice->begin_nodes(i);
    MeCab::Node *e = lattice->end_nodes(i);
    for (; b; b = b->bnext) {
      printf("B[%d] %s\t%s\n", i, b->surface, b->feature);
    }
    for (; e; e = e->enext) {
      printf("E[%d] %s\t%s\n", i, e->surface, e->feature);
    }
  }

  //(中略)

  // Dictionary info
  const MeCab::DictionaryInfo *d = model->dictionary_info();
  for (; d; d = d->next) {
    std::cout << "filename: " <<  d->filename << std::endl;
    std::cout << "charset: " <<  d->charset << std::endl;
    std::cout << "size: " <<  d->size << std::endl;
    std::cout << "type: " <<  d->type << std::endl;
    std::cout << "lsize: " <<  d->lsize << std::endl;
    std::cout << "rsize: " <<  d->rsize << std::endl;
    std::cout << "version: " <<  d->version << std::endl;
  }

  // Swap model atomically.
  mecab::model another_model("");
  model->swap(another_model.get());

  return 0;
}

mainの中身が少しだけスッキリしました.

NodeDictionaryInfoをRangeにする

この節の内容をちゃんと理解するためにはまずイテレーターへの理解が必要です.先に3日めのほっとさんの記事を読んでおくと理解がしやすいでしょう.
 さて,Rangeを作るためにはまずRangeの中身を知る必要があります.Rangeってなんなのでしょうか.今回は「Range-based forで使えるやつ」をRangeの定義としますが,つまりRangeとは「Range-based forで要求される操作を満たしている型」のことを指します.Range-based forは

for(T x : range)
  Statement

という記法です.ここで,Tはなんかの型,xは自由な名前,rangeはRangeのオブジェクト,Statementはfor文の中身です.このRange-based for,例えばrangestd::vector<int> vecだと,以下のようなコードと等しくなります30

for (auto it = vec.begin(), end = vec.end(); it != end; ++it) {
  T x = *it;
  Statement
}

つまり,Rangeはrange.begin()range.end()が使えて,これらが同じ型を返せば良さそうですね!また,range.begin()range.end()の戻り値の型は

  • !=による非等価判定ができる
  • ++によるインクリメントで次の要素を指すことができる
  • *による参照ができる

必要があるようです.これ,イテレーターですね31.というわけで,MeCabのコードを見てみましょう.

MeCabのコードの抜粋
for (; node; node = node->next) {

は?

MeCabのコードの抜粋
for (; b; b = b->bnext) {

は?

MeCabのコードの抜粋
for (; d; d = d->next) {

は?

 大変残念なことに,MeCabのNode*DictionaryInfo*はイテレーターとは異なる操作方法でオブジェクトの列にアクセスするようです.そのため,これらをイテレーターのインターフェースに合わせる必要があります.具体的には,

  • MeCabだと終端をnullptrで表現するようなので,end()で返るイテレーターの中身をnullptrにしておいて,それとの非等価判定を!=で行う
  • MeCabだとptr = ptr->nextptr = ptr->bnextptr = ptr->enextで次の要素を指すようなので,++itrの中身でこれを行う
  • 参照はMeCabでも*なので,これはそのまま

という感じでイテレーターを作ります.つまり,以下のようになります32

Range
struct nodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->next; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct bnodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->bnext; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct enodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->enext; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct dictionary_info{
  const MeCab::DictionaryInfo* beg;
  struct iterator{
    const MeCab::DictionaryInfo* d;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.d != rhs.d;}
    iterator& operator++(){d = d->next; return *this;}
    const MeCab::DictionaryInfo& operator*()const{return *d;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

ほとんど一緒のクラスが複数並んでしまいましたが,とにかくこれでイテレーターとRangeを作ることができました33.これを先程のコードに組み込みます.

Rangeを導入
#include <iostream>
#include <mecab.h>

#define CHECK(eval) if (! eval) { \
   const char *e = tagger ? tagger->what() : MeCab::getTaggerError(); \
   std::cerr << "Exception:" << e << std::endl; \
   return -1; }

namespace mecab{

struct nodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->next; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct bnodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->bnext; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct enodes{
  const MeCab::Node* beg;
  struct iterator{
    const MeCab::Node* node;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.node != rhs.node;}
    iterator& operator++(){node = node->enext; return *this;}
    const MeCab::Node& operator*()const{return *node;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct dictionary_info{
  const MeCab::DictionaryInfo* beg;
  struct iterator{
    const MeCab::DictionaryInfo* d;
    friend bool operator!=(const iterator& lhs, const iterator& rhs){return lhs.d != rhs.d;}
    iterator& operator++(){d = d->next; return *this;}
    const MeCab::DictionaryInfo& operator*()const{return *d;}
  };
  iterator begin(){return {beg};}
  iterator end(){return {nullptr};}
};

struct model : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{
  model(int argc, char** argv) : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{MeCab::createModel(argc, argv), &MeCab::deleteModel}{}
  model(const char* arg) : std::unique_ptr<MeCab::Model, decltype(&MeCab::deleteModel)>{MeCab::createModel(arg), &MeCab::deleteModel}{}
  mecab::dictionary_info dictionary_info()const{return {(*this)->dictionary_info()};}
};

struct tagger : std::unique_ptr<MeCab::Tagger, decltype(&MeCab::deleteTagger)>{
  tagger(MeCab::Tagger* ptr) : std::unique_ptr<MeCab::Tagger, decltype(&MeCab::deleteTagger)>{ptr, &MeCab::deleteTagger}{}
};

struct lattice : std::unique_ptr<MeCab::Lattice, decltype(&MeCab::deleteLattice)>{
  lattice(MeCab::Lattice* ptr) : std::unique_ptr<MeCab::Lattice, decltype(&MeCab::deleteLattice)>{ptr, &MeCab::deleteLattice}{}
  nodes bos_nodes()const{return {(*this)->bos_node()};}
  bnodes begin_nodes(std::size_t i)const{return {(*this)->begin_nodes(i)};}
  enodes end_nodes(std::size_t i)const{return {(*this)->end_nodes(i)};}
};

}

int main (int argc, char **argv) {
  char input[1024] = "太郎は次郎が持っている本を花子に渡した。";

  // Create model object.
  mecab::model model(argc, argv);

  // Create Tagger
  // All taggers generated by Model::createTagger() method share
  // the same model/dictoinary.
  mecab::tagger tagger = model->createTagger();
  CHECK(tagger);

  // Create lattice object per thread.
  mecab::lattice lattice = model->createLattice();

  // Gets tagged result in string
  lattice->set_sentence(input);

  // this method is thread safe, as long as |lattice| is thread local.
  CHECK(tagger->parse(lattice.get()));
  std::cout << lattice->toString() << std::endl;

  // Gets node object.
  for (auto&& node : lattice.bos_nodes()) {
    std::cout << node.id << ' ';
    if (node.stat == MECAB_BOS_NODE)
      std::cout << "BOS";
    else if (node.stat == MECAB_EOS_NODE)
      std::cout << "EOS";
    else
      std::cout.write (node.surface, node.length);

    std::cout << ' ' << node.feature
          << ' ' << (int)(node.surface - input)
          << ' ' << (int)(node.surface - input + node.length)
          << ' ' << node.rcAttr
          << ' ' << node.lcAttr
          << ' ' << node.posid
          << ' ' << (int)node.char_type
          << ' ' << (int)node.stat
          << ' ' << (int)node.isbest
          << ' ' << node.alpha
          << ' ' << node.beta
          << ' ' << node.prob
          << ' ' << node.cost << std::endl;
  }


  // begin_nodes/end_nodes
  const size_t len = lattice->size();
  for (std::size_t i = 0; i <= len; ++i) {
    for (auto&& b : lattice.begin_nodes(i)) {
      printf("B[%d] %s\t%s\n", i, b.surface, b.feature);
    }
    for (auto&& e : lattice.end_nodes(i)) {
      printf("E[%d] %s\t%s\n", i, e.surface, e.feature);
    }
  }

  //(中略)

  // Dictionary info
  for (auto&& d : model.dictionary_info()) {
    std::cout << "filename: " <<  d.filename << std::endl;
    std::cout << "charset: " <<  d.charset << std::endl;
    std::cout << "size: " <<  d.size << std::endl;
    std::cout << "type: " <<  d.type << std::endl;
    std::cout << "lsize: " <<  d.lsize << std::endl;
    std::cout << "rsize: " <<  d.rsize << std::endl;
    std::cout << "version: " <<  d.version << std::endl;
  }

  // Swap model atomically.
  mecab::model another_model("");
  model->swap(another_model.get());

  return 0;
}

その他

node.surfaceがNULL終端されてないので,これをstd::string_viewでラップするようにしたりすると利便性が上がって良いと思います.これも読者への課題とします.

まとめ

段々疲れてきたので後ろのほうが雑でしたが,とりあえずこんな感じで言語機能の知識があるとより良いインターフェースを自力で提供できるようになるので,綺麗なプログラムが書けるようになります.「動くプログラムが作れるのが楽しい」という人(コーディングは手段なのでソースは汚くてもいいよ派)にはあまりわからない感覚かもしれませんが,「プログラミングそれ自体が楽しい」という人(コーディングも目的のうちなのでソースにもこだわるよ派)は「綺麗なコードを書ける」ことの嬉しさが分かるはずなので34,少しでもこの記事に共感できるかなと思います.世のC++erの全員がこういった理由で言語機能を学んでいるわけではないでしょうが,言語機能を学ぶことによるメリットの1つとしてこういったものもあるよ,という紹介でした.
 ラッパーライブラリ製作は「如何に綺麗なインターフェースを提供するか」に注力できるので,その辺のUIとか設計とか言語機能とかを学ぶのには結構いい教材だと思います.みんなもやってみよう!

 最後に,以前作って作りかけのMeCabラッパーがあるので答え合わせがしたくなった人はどうぞ.今回は雑に飛ばした部分とかもちゃんと書こうとするとこんな感じになります.一方で,作りかけでMeCabのフルセットをサポートできてないので35,やる気のある人がいたら是非続きをどうぞ.

おしまい.


  1. 書いたのは12/29です. 

  2. いやまぁ,セルフマネジメントがなってないよねって言われたら何も言えないんですけど… 

  3. そういうシステムなのでやむなし 

  4. ここで「プログラミング自体が難しい」だの「あの言語のほうが」だの言う人は帰ってください,そういう趣旨ではないので… 

  5. ここで「constexpr」とか言う人も帰ってください,そういう趣旨ではないので… 

  6. 正確には「日本語でC++の学習がしにくい」ですが…英語資料は結構ある気がする 

  7. 一昔前に「C++闇の軍団」と称されていた人たちのブログが主です.あとは江添さんの本とか 

  8. 書籍として『Effective Modern C++ ― C++11/14プログラムを進化させる42項目』とか『Optimized C++ ― 最適化、高速化のためのプログラミングテクニック』とか 

  9. 実は『Accelerated C++ ― 効率的なプログラミングのための新しい定跡』という名著があって,これが「C++を使えるようになる」という観点からは非常によくできた本なのですが,残念ながら絶版です…また,この本は出版から既に20年弱経っており,最新のC++規格には準拠していないため,読みながら「でもこの記述は最新の言語機能でこう書ける」などといった補足を誰かしらにしてもらえないと厳しいものがあります 

  10. オブジェクト指向で設計したら当然オブジェクト指向でプログラム書いたほうが楽ですけど,C++はオブジェクト指向を前提とした言語ではないので,私はオブジェクト指向をC++の入門に含めるのはおかしいと思っています.勿論プログラミング入門において小さなプログラムを書くことと並行して設計についてもしっかり学ぶべきではあるのですが,「C++を学ぶ」と「オブジェクト指向を学ぶ」はそれぞれ独立しています. 

  11. 容易に自分の足を撃ち抜けるのがC++の悪いところ / (「規格上」ではなく,「作法」として)他人の作った「正しい」プログラムを見て「正しさ」を知らないと自分で「正しい」プログラムは書けないという話 

  12. 少なくとも初心者が学ぶべき内容としては他にもっと色々あって,優先順位は随分と低くなりました.入門で学ぶことではないと思います. 

  13. 十把一絡げにC++erなどとまとめていますが,厳密にはC++ライブラリアンです.まぁ,C++詳しい人って大体なんかしらライブラリ作ってるイメージがあるし…(ほんとか?) 

  14. 尤も,ライブラリに関して本当に何も知らないと訳の分からないプログラムを書きがちなので,cpprefjpなどを見つつ多少は理解して書く必要があります.優先順位的にそうした仔細に関する勉強よりも気持ちを理解したほうが良いという話です(もっと言えば,学習レベルから脱してちゃんとしたアプリケーションを組むならちゃんとした設計を学んでください.「動けばええやろ!」とかいう考えの人がのさばるせいで…貴重な時間がな…お前らな…). 

  15. 他にも,それこそ何故using namespace stdを安易に使うと問題なのか,といったことが理解できるようになります.理解した上で使うのと,理解せずに使うのでは話が違うのです. 

  16. 初心者向け 

  17. ちょっと難しいかも.脱初心者的内容 

  18. ライブラリコードではバリバリ現役です.要するに,ライブラリを書く人たちが一般ユーザーの代わりにnewを書いてくれるお陰で一般ユーザーはnewを書く必要が無くなっています. 

  19. 厳密に言えばstd::make_uniquestd::make_sharedでなんとかならない場合もありますが,今回の大筋には関係しないのでスルーします. 

  20. メモリリークを起こしてもOSの上なら後でなんとかしてもらえる,と言い張ることもできますが…行儀悪いのでやめましょう. 

  21. RAIIと言います.詳しくは2年前のC++初心者AdCに書いたので,そちらを読んでいただければと思います(最近記事の内容がいまいちだなーって思ってるので,暇があったら書き直したいです) 

  22. ちなみに他にも,Windowsだとリソースリークする,another_modeldelete忘れ,CHECKマクロでtagger以外のリソースをdeleteしてない,などなど普通に雑です(まぁ所詮サンプルコード,という話もあるかもしれませんけど…) 

  23. 一方で,バイナリサイズがほとんど膨れない書き方をしているように見受けられるので,C言語の気持ちとしては割りと正しい線を行っている気もしますし,実行速度が要求されるとしかたないのかなという気持ちもあります.どういうことかというと,templateを使いだしたりクラスを増やしたりtemplateを使ったりtemplateを使ったり,とC++の機能を活用するとプログラムの実行ファイル(バイナリ)のサイズが大きくなるのですが,逆にバイナリサイズがある程度小さいと近頃のCPUのキャッシュにプログラム全体が収まるので実行速度が爆上がりします.つまり,ある程度小規模なプログラムであればC++のリッチなインターフェースに合わせるよりもC言語の泥臭いインターフェースで書いたほうが実行速度が上がる,という事態が発生しえるので,実行速度重視のライブラリとしては極力バイナリサイズを増やさないように努力したほうが得策ということです.まぁ,そういう事情があったとしてもMeCabのC++インターフェースが書きにくいことに変わりはないんですけど…正直Cのインターフェースと何が違うんだってレベルだし,ライブラリインターフェースとしては落第ものでしょ… 

  24. どうしてnewが無いのにdeleteが出てくるんだ…?と思った方もいらっしゃるかもしれませんが,MeCab::createModel()とかmodel->createLattice()の内部でnewされてるからです. 

  25. 見た目の問題のみならず,deleteを正しく手で書かないといけないので,CHECKマクロのようにif文内でreturnしたりする場合にはその手前でちゃんと必要なだけdeleteを書かなければなりません.またコードの途中で例外が発生したりするとdeleteが実行されない可能性もあります.こうしたことを考えていくとキリがないので,自動で解放してもらえるようにしたほうが楽です. 

  26. 公式のサンプルコードなのにWindowsだとリソースリークするのどうなんだ…? 

  27. std::unique_ptr<T, D>Dstd::unique_ptr<T, D>のオブジェクトのメンバ変数の型として使われる(厳密にはメンバではなく継承の場合もありますが,ややこしいのでここではメンバとして説明します)のですが,ここでDが関数型(decltype(MeCab::deleteModel)でとれるやつ.この場合void(MeCab::Model*))だと,関数型の変数は作れないのでコンパイルエラーになってしまいます.decltype(&MeCab::deleteModel)にするとvoid(*)(MeCab::Model*)という関数ポインタ型が得られるのですが,こちらは変数の型にすることができます.この辺はC言語の複雑な仕様の名残なので,C++を使うなら諦めて受け入れるしかないです… 

  28. まぁ今回はTaggerとかLatticeとか1回ずつしかでてこないのであんまり旨味を感じないかもしれませんが…先々のことを思って,ね? 

  29. 継承ではなく移譲しましょう…という話もあるのですが,今回は面倒なので継承で済ませます. 

  30. 厳密にはもうちょっといろいろある(例えば,配列はbegin()メンバ関数を持ってないのにどうしてRange-based forが使えるんだ?とか)んですけど長くなるので省略.Range-based forの詳細についてはcpprefjpの該当ページを参照 

  31. ほんとは他にもいろいろな条件を満たさないとイテレーターとは呼べないので,今回作るのはイテレーター未満です. 

  32. operator!=とかoperator++って何!?って人は,「演算子オーバーロード」でググると良いと思います.簡単に言えば,演算子は関数とみなせるので,C++では関数定義と同様の方法でクラスオブジェクトに対する演算子の挙動を自由に変更できます.これを使って++が呼び出されたら内部でnode = node->nextを実行する,というようなことを実現します. 

  33. templateなどを使って共通コードをまとめるのは読者への課題とします. 

  34. この区分も相当雑な分け方ですが 

  35. 自分の使う部分だけしかラップしていないため. 

88
73
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
88
73