この記事は 初心者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以降,new
やdelete
を書かなければいけないプログラマは激減しました12.using namespace std
は,その功罪を正しく認識しないで使うとどこかで痛い目を見ます(つまり右も左も分からない初心者に勧めるべき存在ではないのです).世間の「C++」は,「C言語にとって目新しい言語機能とオブジェクト指向」であり,それは15年以上前の話で,そして未だにその頃の入門情報しか日本語では残っていないのです.
近年のC++でも言語機能が多数導入されましたが,こうした言語機能それぞれを学んでも初心者にはそれら知識を繋げていく線を引くことができず,点のまま終わってしまうのではないかなぁと思います.なぜなら,言語機能だけを学んでもC++の気持ちを理解していないからです.世間の「C++」は,CとかJavaの気持ちでC++を使っているところが多いです(あと古い).そうではなく,C++をC++らしく使うことが大切だと思います.
では,何故世のC++er13は「てんぷれーと」とか「ちゅうさんじょしこんすとえくすぷれっしょん」とか「めたぷろぐらみんぐ」とかよく分からない話をしていて,言語機能やライブラリに関する資料が充実して,「C++を使う」観点での入門資料が増えないのか.それは,そうした人たちは「C++を使う」立場から「C++の使われる部分を作る」立場へと移行したからです.入門者が言語機能の多くよりもC++の気持ちを理解すべきなのは言語機能やライブラリの詳細を理解しなくてもある程度アプリケーションが書けるから^detailですが,そのライブラリは誰が作るのかという話になります.ライブラリを作っているのが世のC++erであり,そしてそうしたライブラリを作る,つまり「C++をC++らしく使えるようにする」ために必要なのが,言語機能への深い理解,ということになります14.世のC++erは自分たちにとって重要で役に立つ内容をたくさん公開しているのですが,これが入門レベルの初心者にとって優先的に学ぶべき内容か,と言われるとそれは違うのではないか,というのが私の主張です.ところで,本記事もそうした入門レベルの人向けの内容ではなく,むしろそうした「使う」レベルからの脱却,「使われる部分を作る」,C++の言語機能を理解することがどういったメリットを生むのか,といった内容です.これだけあーだこーだ言っておきながら自ら入門者を捨て置く非道
というわけで,この記事ではC++の気持ちの一部をかるーく紹介して15,世間の「C++」をC++にします16.
C++の気持ち
「C++らしさ」みたいなものを掴んでもらうために,いくつかお気持ちを表明します.お気持ち表明なので,このあたりまでは初心者でも雰囲気だけ追っかけて読めると思います.
C++では無闇にnew
しない
C++14以降,ユーザーコード中でnew
を記述する機会はほとんどなくなりました17.現代ではstd::make_unique
やstd::make_shared
を使います18.
C++では解放しない
C++ではあまり解放処理をユーザーコードに書きません.例えば,new
に対応したdelete
を書かない,ということです.尤も,実際にnew
を書いてしまった場合はdelete
を自分で書かなければならないのですが19,ここでstd::make_unique
やstd::make_shared
が出てきます.こいつらで動的に確保したメモリは自動でdelete
されるので,new
を手書きした場合とは異なり解放処理を自分で書く必要がありません.
#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しないといけない
}
#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++にはあります20.メモリの動的確保以外には,ファイル入出力のstd::fstream
もclose()
メンバ関数を呼ばなくても勝手にファイルを閉じてくれたりします.積極的に使っていくべきです.逆説的に,std::make_unique
やstd::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といいます.
int arr[100]; //配列(Range)
for(int* it = arr; it != arr + 100; ++it) //for文書くのが面倒
std::cout << *it << std::endl; //ポインタのデリファレンスも面倒
int arr[100]; //配列(Range)
for(auto&& x : arr) //これだけで「Rangeの中身全部について,各要素をxとおいて」みたいな意味になる
std::cout << x << std::endl; //xはRangeの要素そのものなのでデリファレンスも不要
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; //イテレーターのデリファレンスも面倒
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++ライブラリインターフェースのサンプルコードとなります.
#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++でしょ?」って感じのインターフェースですね…21C++の気持ちガン無視です22.
というわけで,今回はMeCabのC++ライブラリインターフェースのラッパーライブラリを書いて,MeCabを使いやすくしてみます.
ラッパーライブラリって?
MeCab以外にも,C言語のライブラリとか,OSのAPIとか…世の中にはいろんな使いにくいライブラリがあります.そうしたライブラリが使いにくいのは,主にインターフェースが悪い,ということに起因します.例えば,C++で言えばRange-based forとかRAIIとか便利で標準的な記法があるのに,使いにくいライブラリがそういった機能に対応してない(同じ型のデータの集まりをRangeではない形式で提供している,確保と解放をユーザーに手動でやらせる)ということです.逆に言えば,使い心地以外の面では機能的には問題がないわけで,インターフェースだけ使いやすいものにしてあげればなんとかなりそうです.ラッパーライブラリとは,こうした「ライブラリそれ自体が機能を持つわけではなく,他のライブラリに覆いかぶさって(他のライブラリをラップして)使いやすいインターフェースを提供するライブラリ」のことです.
ラッパーライブラリを作るためには,その言語で使いやすいインターフェースを知り,既存のライブラリのインターフェースからどのようにしてそのインターフェースに合わせるかを考え,それを実現しなければなりません.これには言語仕様などへの理解が不可欠です.逆説的に,言語仕様への理解があれば世の中の使いにくいライブラリを我慢して使わなくても自力で使いやすいインターフェースに変えることができます.これは言語仕様を理解するメリットの1つとして挙げられると思います.あ,これこの記事の結論ですので,真面目に上から読んできたけどこの後でわかんねーなーってなった方はいつでも安心してブラウザバックしてくださって大丈夫です.
ラッパーライブラリはライブラリとしての機能を自分で考える必要が無い一方で,ライブラリとしてのユーザーインターフェースについての考察や言語仕様の理解などが鍛えられる上,世の中には数多のクソインターフェースライブラリが跋扈しているので題材に困らず,ライブラリ製作の入り口としておすすめです.ちなみに私が作っている物としては,Windows APIのラッパーライブラリであるwillがあります(宣伝).
実際にラップしてみる
それでは実際にMeCabのC++ライブラリインターフェースをラップしていきます.この章から結構ガッツリC++を書きますが,class
の書き方などは知っていることを前提としているので少々難易度が高めかもしれません.
解放を自動化する
とりあえずdelete
23をmain
で書くのがダサい24ので,これを自動化します.ところで,型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>
を使ってしまえば解決します.
#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()
などを使わなければいけないのです25.でも,std::unique_ptr<T>
はdelete
を自動化するもの.delete
以外の自動化なんてできるのでしょうか?
実は,std::unique_ptr
やstd::shared_ptr
にはカスタムデリータという機能があり,解放処理をdelete
から好きな関数に変更することができます.なので,以下のように変更すれば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)
と書きます26.そして,今まではポインタだけを渡していましたが,これと&MeCab::deleteModel
をセットで渡すようにすればOKです.
でも毎回&MeCab::deleteModel
とか書きまくるのが大変なので27,さらにこれをクラスにラップします28.
#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
の中身が少しだけスッキリしました.
Node
とDictionaryInfo
を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,例えばrange
がstd::vector<int> vec
だと,以下のようなコードと等しくなります29.
for (auto it = vec.begin(), end = vec.end(); it != end; ++it) {
T x = *it;
Statement
}
つまり,Rangeはrange.begin()
とrange.end()
が使えて,これらが同じ型を返せば良さそうですね!また,range.begin()
とrange.end()
の戻り値の型は
-
!=
による非等価判定ができる -
++
によるインクリメントで次の要素を指すことができる -
*
による参照ができる
必要があるようです.これ,イテレーターですね30.というわけで,MeCabのコードを見てみましょう.
for (; node; node = node->next) {
は?
for (; b; b = b->bnext) {
は?
for (; d; d = d->next) {
は?
大変残念なことに,MeCabのNode*
やDictionaryInfo*
はイテレーターとは異なる操作方法でオブジェクトの列にアクセスするようです.そのため,これらをイテレーターのインターフェースに合わせる必要があります.具体的には,
- MeCabだと終端を
nullptr
で表現するようなので,end()
で返るイテレーターの中身をnullptr
にしておいて,それとの非等価判定を!=
で行う - MeCabだと
ptr = ptr->next
やptr = ptr->bnext
,ptr = ptr->enext
で次の要素を指すようなので,++itr
の中身でこれを行う - 参照はMeCabでも
*
なので,これはそのまま
という感じでイテレーターを作ります.つまり,以下のようになります31.
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を作ることができました32.これを先程のコードに組み込みます.
#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
でラップするようにしたりすると利便性が上がって良いと思います.これも読者への課題とします.
まとめ
段々疲れてきたので後ろのほうが雑でしたが,とりあえずこんな感じで言語機能の知識があるとより良いインターフェースを自力で提供できるようになるので,綺麗なプログラムが書けるようになります.「動くプログラムが作れるのが楽しい」という人(コーディングは手段なのでソースは汚くてもいいよ派)にはあまりわからない感覚かもしれませんが,「プログラミングそれ自体が楽しい」という人(コーディングも目的のうちなのでソースにもこだわるよ派)は「綺麗なコードを書ける」ことの嬉しさが分かるはずなので33,少しでもこの記事に共感できるかなと思います.世のC++erの全員がこういった理由で言語機能を学んでいるわけではないでしょうが,言語機能を学ぶことによるメリットの1つとしてこういったものもあるよ,という紹介でした.
ラッパーライブラリ製作は「如何に綺麗なインターフェースを提供するか」に注力できるので,その辺のUIとか設計とか言語機能とかを学ぶのには結構いい教材だと思います.みんなもやってみよう!
最後に,以前作って作りかけのMeCabラッパーがあるので答え合わせがしたくなった人はどうぞ.今回は雑に飛ばした部分とかもちゃんと書こうとするとこんな感じになります.一方で,作りかけでMeCabのフルセットをサポートできてないので34,やる気のある人がいたら是非続きをどうぞ.
おしまい.
-
書いたのは12/29です. ↩
-
いやまぁ,セルフマネジメントがなってないよねって言われたら何も言えないんですけど… ↩
-
そういうシステムなのでやむなし ↩
-
ここで「プログラミング自体が難しい」だの「あの言語のほうが」だの言う人は帰ってください,そういう趣旨ではないので… ↩
-
ここで「constexpr」とか言う人も帰ってください,そういう趣旨ではないので… ↩
-
正確には「日本語でC++の学習がしにくい」ですが…英語資料は結構ある気がする ↩
-
書籍として『Effective Modern C++ ― C++11/14プログラムを進化させる42項目』とか『Optimized C++ ― 最適化、高速化のためのプログラミングテクニック』とか ↩
-
実は『Accelerated C++ ― 効率的なプログラミングのための新しい定跡』という名著があって,これが「C++を使えるようになる」という観点からは非常によくできた本なのですが,残念ながら絶版です…また,この本は出版から既に20年弱経っており,最新のC++規格には準拠していないため,読みながら「でもこの記述は最新の言語機能でこう書ける」などといった補足を誰かしらにしてもらえないと厳しいものがあります ↩
-
オブジェクト指向で設計したら当然オブジェクト指向でプログラム書いたほうが楽ですけど,C++はオブジェクト指向を前提とした言語ではないので,私はオブジェクト指向をC++の入門に含めるのはおかしいと思っています.勿論プログラミング入門において小さなプログラムを書くことと並行して設計についてもしっかり学ぶべきではあるのですが,「C++を学ぶ」と「オブジェクト指向を学ぶ」はそれぞれ独立しています. ↩
-
容易に自分の足を撃ち抜けるのがC++の悪いところ / (「規格上」ではなく,「作法」として)他人の作った「正しい」プログラムを見て「正しさ」を知らないと自分で「正しい」プログラムは書けないという話 ↩
-
少なくとも初心者が学ぶべき内容としては他にもっと色々あって,優先順位は随分と低くなりました.入門で学ぶことではないと思います. ↩
-
十把一絡げにC++erなどとまとめていますが,厳密にはC++ライブラリアンです.まぁ,C++詳しい人って大体なんかしらライブラリ作ってるイメージがあるし…(ほんとか?) ↩
-
他にも,それこそ何故
using namespace std
を安易に使うと問題なのか,といったことが理解できるようになります.理解した上で使うのと,理解せずに使うのでは話が違うのです. ↩ -
初心者向け ↩
-
ちょっと難しいかも.脱初心者的内容 ↩
-
ライブラリコードではバリバリ現役です.要するに,ライブラリを書く人たちが一般ユーザーの代わりに
new
を書いてくれるお陰で一般ユーザーはnew
を書く必要が無くなっています. ↩ -
厳密に言えば
std::make_unique
やstd::make_shared
でなんとかならない場合もありますが,今回の大筋には関係しないのでスルーします. ↩ -
メモリリークを起こしてもOSの上なら後でなんとかしてもらえる,と言い張ることもできますが…行儀悪いのでやめましょう. ↩
-
RAIIと言います.詳しくは2年前のC++初心者AdCに書いたので,そちらを読んでいただければと思います(最近記事の内容がいまいちだなーって思ってるので,暇があったら書き直したいです) ↩
-
ちなみに他にも,Windowsだとリソースリークする,
another_model
のdelete
忘れ,CHECK
マクロでtagger
以外のリソースをdelete
してない,などなど普通に雑です(まぁ所詮サンプルコード,という話もあるかもしれませんけど…) ↩ -
一方で,バイナリサイズがほとんど膨れない書き方をしているように見受けられるので,C言語の気持ちとしては割りと正しい線を行っている気もしますし,実行速度が要求されるとしかたないのかなという気持ちもあります.どういうことかというと,
template
を使いだしたりクラスを増やしたりtemplate
を使ったりtemplate
を使ったり,とC++の機能を活用するとプログラムの実行ファイル(バイナリ)のサイズが大きくなるのですが,逆にバイナリサイズがある程度小さいと近頃のCPUのキャッシュにプログラム全体が収まるので実行速度が爆上がりします.つまり,ある程度小規模なプログラムであればC++のリッチなインターフェースに合わせるよりもC言語の泥臭いインターフェースで書いたほうが実行速度が上がる,という事態が発生しえるので,実行速度重視のライブラリとしては極力バイナリサイズを増やさないように努力したほうが得策ということです.まぁ,そういう事情があったとしてもMeCabのC++インターフェースが書きにくいことに変わりはないんですけど…正直Cのインターフェースと何が違うんだってレベルだし,ライブラリインターフェースとしては落第ものでしょ… ↩ -
どうして
new
が無いのにdelete
が出てくるんだ…?と思った方もいらっしゃるかもしれませんが,MeCab::createModel()
とかmodel->createLattice()
の内部でnew
されてるからです. ↩ -
見た目の問題のみならず,
delete
を正しく手で書かないといけないので,CHECKマクロのようにif
文内でreturn
したりする場合にはその手前でちゃんと必要なだけdelete
を書かなければなりません.またコードの途中で例外が発生したりするとdelete
が実行されない可能性もあります.こうしたことを考えていくとキリがないので,自動で解放してもらえるようにしたほうが楽です. ↩ -
公式のサンプルコードなのにWindowsだとリソースリークするのどうなんだ…? ↩
-
std::unique_ptr<T, D>
のD
はstd::unique_ptr<T, D>
のオブジェクトのメンバ変数の型として使われる(厳密にはメンバではなく継承の場合もありますが,ややこしいのでここではメンバとして説明します)のですが,ここでD
が関数型(decltype(MeCab::deleteModel)
でとれるやつ.この場合void(MeCab::Model*)
)だと,関数型の変数は作れないのでコンパイルエラーになってしまいます.decltype(&MeCab::deleteModel)
にするとvoid(*)(MeCab::Model*)
という関数ポインタ型が得られるのですが,こちらは変数の型にすることができます.この辺はC言語の複雑な仕様の名残なので,C++を使うなら諦めて受け入れるしかないです… ↩ -
まぁ今回は
Tagger
とかLattice
とか1回ずつしかでてこないのであんまり旨味を感じないかもしれませんが…先々のことを思って,ね? ↩ -
継承ではなく移譲しましょう…という話もあるのですが,今回は面倒なので継承で済ませます. ↩
-
厳密にはもうちょっといろいろある(例えば,配列は
begin()
メンバ関数を持ってないのにどうしてRange-based forが使えるんだ?とか)んですけど長くなるので省略.Range-based forの詳細についてはcpprefjpの該当ページを参照 ↩ -
ほんとは他にもいろいろな条件を満たさないとイテレーターとは呼べないので,今回作るのはイテレーター未満です. ↩
-
operator!=
とかoperator++
って何!?って人は,「演算子オーバーロード」でググると良いと思います.簡単に言えば,演算子は関数とみなせるので,C++では関数定義と同様の方法でクラスオブジェクトに対する演算子の挙動を自由に変更できます.これを使って++
が呼び出されたら内部でnode = node->next
を実行する,というようなことを実現します. ↩ -
template
などを使って共通コードをまとめるのは読者への課題とします. ↩ -
この区分も相当雑な分け方ですが ↩
-
自分の使う部分だけしかラップしていないため. ↩