STLを使おう

ファイルを読んで字句解析して、いろいろ加工してファイルに出力する、という パターンのプログラムはよくあります。 字句解析(というか、文字列を切ったり貼ったり)するのって、 「Perlだったら1行で済むのに」と思ってしまうような 処理って結構ありますよね。

先頭が1〜3文字の英大文字で、続いて数字が並んでいるときに、 その数字部分を取り出す、みたいな処理は、 C++で書くとそれなりに行数が必要ですが、Perlだと

        $num = $line =~ /^[A-Z]{1,3}(\d+)/ && $1;

というように書けてしまいます。 字句解析ってのは要するに正規表現に当てはめていく処理なんですね。

ちなみに筆者は最近 rubyも 使ってみたりしていますが、 この手の処理に限定するならば、正規表現や置換の機能が強力な分、Perlの方が やりたいことを素早く処理してしまえるように感じています。 Perl5で更にいろんな表現が増えましたね。

てな訳で、C++で字句解析のプログラムを作るときも、 Perl風の文で函数の機能仕様を記述していたりします。 これは書くのが楽で便利だけど、正規表現を知らない人が読んだら 訳わからないだろうな。

もうひとつ、こういうプログラムにつきものなのが、 「辞書」とか「連想配列」とかいうものです。 あるデータのかたまりに名前をつけて記録しておき、 いつでもその名前で検索できるようにしたもの(我ながら実に大雑把な説明)。 これもPerlならば専用の機能があるのですぐに使えるのですが、 もどかしいことにC++ではそれなりの処理を書かなければなりません。 手を抜いて、配列を使って線形検索しても、処理速度を気にしなければ 一応は使えるのですが、それにしても何とかならんもんだろうか、と思います。

VC++についているMFCには、CMapやCTypedPtrMapというクラスがあって、 連想配列を割合手軽に実現できるようになっています。 実は筆者は、ひところこのクラスや、CTypedPtrArray、CTypedPtrList(配列、リスト)を 多用したことがありました。 ところが、同じプログラムをMacintoshでも動かそう、という話になったとき、 はたと困ったんですな。 対応するクラスがないので、関係するところを書き替えなければなりません。 幸か不幸か、筆者自身は移植に関与せずに済んだのですが、 ○○氏(一応伏字ね)はその後どうしたのだろうか...

標準テンプレート・ライブラリーで作る連想配列

枕が長くなりましたが、そういうわけで、移植にあたってもそのまま使える ライブラリーというのは価値が高いのです。 STL (Standard Templete Library)が使えるようになってきたので、 筆者も徐々に移行しようとしています。 どんなC++コンパイラーにも標準で付いてくる、という状況に、近い将来なることでしょう。 まあ、何年もかけて開発し、現に使われているプログラムを直ちにSTLで 書きかえるのも大変ですから、実験的にその一部機能をSTLで作ってみているところ。

連想配列はmapという(テンプレート・)クラスを使います。 ほかにもmultimapsetmultisetというものがありますが、概念的にPerlの連想配列に 一番近いのはmapでしょう。 これを、連想キーとして文字列を使って実現します。

宣言

名前(文字列)をキーとした連想配列は、次のようになります。 typedef を使わないで直接宣言する方法もありますが、こうしておくとちょっとだけ プログラムの記述がすっきりします。

        typedef map<string, CHoge *>    HogeMap_t;
        HogeMap_t       HogeMap;

その他の宣言

VC++ の場合、STLを使うと(多分)意味のない警告が出てくるので、 #pragma warning(disable:4786) という記述を入れておくと ちょっとだけ幸せになります(5.0の場合。他の場合はちょっと違うかも)。

#include するのは、拡張子なしのmapやstringというファイル。 こういう命名のしかたが新しいC++の流儀のようですが、 *.cppや*.hという拡張子に起動するエディターが対応している Windowsでは、ちょっと不便。 もっとも実際にmapやstringの中身を見ることはあまりない、というか、 見ても何をやっているのか非常にわかりにくいのです。

で、using namespace std; というのは、自分で独自に 名前空間を定義しようなどと思わないならば、おまじないのつもりで この通り書いておきます。 これがないと名前にいちいち接頭辞"std::"をつけて 修飾しなければなりません。

#pragma warning(disable:4786)
#include        <map>
#include        <string>
using namespace std;

連想配列に登録

登録は、連想キーと実体とを組にして、insert()で行います。 例えばこんな具合。

CHoge *         hoge = new CHoge(......);
string          key = hoge->GetName();
HogeMap.insert(HogeMap_t::value_type(key, hoge));

hogeを構築し、キーとなる名前keyを使って登録します。 先にHogeMap_tという型をtypedefしておきましたが、 これがないと、

HogeMap.insert(map<string, CHoge *>::value_type(key, hoge));

のようになって、何回も出てくるとちょっとうっとうしくなります。

実際にはキーが重複しないかなど考えなければならないことが多いのですが、 条件が許せば次のような簡便な書き方もあります。 こっちの方が「いかにも連想配列」という感じがしますね。

        string  key = ......;
        HogeMap[key] = new CHoge(......);

キーを使って検索

検索はfind()を使います。 みつからなかったことの判定にHogeMap.end()が現れるというのは ちょっと違和感がありましたが、 map以外のvectordequeでも find()が同様に使えることと対比すると、 自然な判定方法に思えてきます。

HogeMap_t::const_iterator       n = HogeMap.find(key);
if (n == HogeMap.end()) {
        /* みつからなかった場合の処理 */
}
else {
        CHoge *         hoge = n->second;
        /* hoge を使った処理 */
}

vectordequeについては 詳しく書きません。適当なマニュアルを参照。 それにしてもdequeって、 "Double-Ended Queue"のことなんですね。 キューから取り出す操作「デキュー」を連想してしまう名前なのがどうも...

後始末

登録した要素をすべて削除するにはclear()を使うのですが、 上記のようにnewしたインスタンスを登録していた場合は、それもdeleteしなければ なりません。 こういう連想配列をたくさん使う場合は、次のようなテンプレートを定義しておけば 嬉しくなります。 例によってconst_iteratorなんかの説明は省略しますが、 繰り返し処理の常套手段で、たいていの本には載っているはずですので...

template <class T>
void clear_map_deep(T obj)
{
        for (T::const_iterator iter = obj.begin(); iter != obj.end(); ++iter) {
                delete iter->second;
        }
        obj.clear();
};

すると次の1文で後始末ができるようになります。連想配列の型に関係なく 同じように記述できるので便利です。

clear_map_deep(HogeMap);

テンプレートを使うとわけわかのエラーメッセージが出る

STLを使ってプログラムを作っていると、 テンプレートの定義ファイルの方でコンパイル・エラーが出ることがあります。 該当箇所を表示すると、自分が作ったのではない、しかも暗号みたいなコードなので 最初は焦ります。"_T"とか"_E"とか、短い名前が たくさん使われているので、暗号に見えるんですな。 STLのバグか、なんて思ったりするかも知れませんが、 筆者の経験ではたいていの場合、 mapなどのテンプレートで直接間接に引用しているクラスの定義を ちゃんと#includeしていないことが原因です。 まあこれはMFCのCTypedPtrMapなんかを使っていたときも同様で、 STLではなくテンプレートの仕組みの問題ですね。