2010年6月7日月曜日

std::mapの使い方

Check
Collada読み込みにおいて、stlのmapを使ったほうがよさそうな場面に出くわしたので、使い方をチェック。キーはstd::stringが手っ取り早そう。要素の挿入・参照方法は、イテレータを使用しないシンプルなものを採用した。

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc, char **argv)
{
   map<string, int> tbl;

   // 挿入
   tbl["hoge"] = 0;
   tbl["piyo"] = 10;
   tbl["fuga"] = 20;

   // 参照
   cout << tbl["hoge"] << endl;
   cout << tbl["piyo"] << endl;
   cout << tbl["fuga"] << endl;
   return 0;
}



ふと気になってoperator[]に無効なキーを与えた場合はどうなるのか実験してみた。予想だと例外を吐いて、戻り値なしかなーとか思ってたのですが…
#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc, char **argv)
{
   map<string, int> tbl;

   // 挿入
   tbl["hoge"] = 0;
   tbl["piyo"] = 10;
   tbl["fuga"] = 20;

   // 参照
   cout << tbl["hoge"] << endl;//0
   cout << tbl["piyo"] << endl;//10
   cout << tbl["fuga"] << endl;//20

   // 無効なキーにアクセスした場合
   int ret = tbl["invalid key"];
   cout << ret << endl;//0 <- intの初期値0が新しいキー"invalid key"と一緒に作成される模様
   return 0;
}
結果から言うと、普通にアクセスできて、0が返ってきた。どうやら、無効なキーを使ってoperator[]経由でマップにアクセスした場合は、そのキーを元に新しい要素(ここではint)が作成される模様。この場合intの初期値である0で値が初期化され、("invalid key", 0)という要素がマップに追加されたというわけだ。

私がやりたいことは、例えばstd::stringの"VERTEX"というキーがあるかどうか調べて、あればそのキーに関連した値(unsigned intで表現されるインデックス配列のオフセット値)を取り出し、なければ無視するということなので、このままでは実現不可能ですね。次はfindを使うやり方を模索しよう。



findを使用した検索
#include <iostream>
#include <string>
#include <map>
using namespace std;
typedef map<string, int> Table;

int main(int argc, char **argv)
{
   Table tbl;

   // 挿入
   tbl["hoge"] = 123;
   tbl["piyo"] = 456;
   tbl["fuga"] = 789;

   // 検索
   Table::iterator ret = tbl.find("hoge");
   if(ret != tbl.end())
   {
      // 見つかった
      cout << "Key: " << ret->first;
      cout << ", Value: " << ret->second << endl;
   }
   else
   {
      // 見つからなかった
      cout << "Not Found." << endl;
   }

   return 0;
}
イテレータ宣言が長くなるのでmap<string, int>をTableという名前に定義しなおしています。個人的にはこういうやり方が好き。"hoge"に対応する値が見つかった場合はそいつを指すイテレータを、見つからなかった場合は末端tbl.end()を指すイテレータを返します。

ここでは、int値だけが欲しいのでit->secondで目的の値を取り出せます。it->firstだとキー値を取り出せます。

あとはこいつを関数化してあげればいいだけ。
/**
 * std::map<std::string, int>からキーに対応する値を取り出す。
 * @param out 取り出した値の格納先。見つからなかった場合は何も変更しない。
 * @param keyword キー値
 * @param tbl 検索するマップ
 * @return キーに対応する値がある場合は真。
 */
bool FindValueFromMap(int *out,
                      const string &keyword,
                      const Table &tbl)
{
   Table::const_iterator it = tbl.find(keyword);

   if(it != tbl.end())
   {
      *out = it->second;
      return true;
   }
   return false;
}
始めは戻り値をそのままintにしよう思っていたのですが、よく考えたら値が見つからなかった場合はどないすんねんとなったので、boolに変更。ポインタ経由で値をやり取りするようにしました。例外とかに変更したほうがいいのかなぁ。

んで、この関数はtemplateで汎化できるんじゃね? と思って以下のようなコードを書いたが、コンパイルが通らなかった。
template<typename KeyType, typename MappedType>
bool FindValueFromMapT(MappedType *out,
                       const KeyType &keyword,
                       const std::map<KeyType, MappedType> &table)
{
   std::map<KeyType, MappedType>::const_iterator it = table.find(keyword);
   if(it != table.end())
   {
      *out = it->second;
      return true;
   }
   return false;
}
エラーメッセージ
main.cpp: In function `bool FindValueFromMapT(MappedType*, const KeyType&, const std::map<KeyType, MappedType, std::less<_Key>, std::allocator<std::pair<const _Key, _Tp> > >&)':
main.cpp:60: error: expected `;' before "it"
main.cpp:61: error: `it' undeclared (first use this function)
main.cpp:61: error: (Each undeclared identifier is reported only once for each function it appears in.)
どうもイテレータ宣言方法で失敗しているっぽいが、よくわからん。まあここまでやる必要もなさそうなので、テンプレート化はあきらめた。



さらに追記。上記のテンプレートだがVC++2008でコンパイルしたところ何の問題もなく通った。呼び出しも予想通りの結果を返してくれた・・・。g++のコンパイルオプションあたりが怪しそう! ちょっと調べてみよう。

C++テンプレートのFAQという記事を発見。g++が厳しいというよりは、VC++が緩いというべきか。厳密なテンプレートの記述を行えばコンパイルは通りそう。



コンパイル通りました!

template<typename KeyType, typename MappedType>
bool FindValueFromMapT(MappedType *out,
                       const KeyType &key,
                       const std::map<KeyType, MappedType> &table)
{
   typename std::map<KeyType, MappedType>::const_iterator it = table.find(key);
   if(it != table.end())
   {
      *out = it->second;
      return true;
   }
   return false;
}
前回との違いは
typename std::map<KeyType, MappedType>::const_iterator it = table.find(key);
の部分、これだけ!なぜこれをつけるとうまくいくかについては、現在確認中ですが、dependant nameというのが関係してそう。英語なのでじっくり読んでいきます。

2 件のコメント:

  1. 「tbl.find("hoge")」で取得したイテレータがend()と違うかを判定されていますが、「tbl.count("hoge")」で登録されている個数が帰ってくるので、countの戻値によってデータが存在するか確認出来ます。

    返信削除
    返信
    1. 6年越しにレスが付いたことに感動を覚えておりますw
      確かに map::count() の方が、目的に合致しているしわかりやすいですね。
      勉強になりました。ありがとうございます!

      削除