株式会社エス・スリー・フォー

7つの電話番号簿

電話番号簿を作る…

僕のSTL本"Standard Template Library プログラミング"では、STLコンテナのサンプルとして、簡単な電話番号簿を紹介しています。この本の中ではひとつのアプリケーション(電話番号簿)を、STLが提供する様々なコンテナを使って実装してみました。

このアーティクルでは趣向を変えて、電話番号簿アプリケーションをSTL,MFCコレクション,そしてTools.h++を使った実装を試み、それぞれの違いをご覧にいれましょう。

電話番号簿は"電話番号(Key)と名前(Value)の対応表(Map)"です。電話番号簿の機能は:

  1. 電話番号と名前の組を登録する
  2. 電話番号から名前を検索する
  3. 電話番号(と名前の組)を削除する
  4. すべての電話番号(と名前の組)を削除する
  5. 登録された電話番号と名前の組をファイルに書き込む/読み出す

の5つです。

Standard C++ : STL

ANSI/ISO standard C++(標準C++)のライブラリの一部として組み入れられたSTL。STLは数多くのライブラリの中でも最もポータビリティに優れたライブラリといえるでしょう。何といっても標準ですからね。

map<Key,T>

STLが提供するmap<Key,T>は、電話番号簿を実装するのに最適のコンテナで、2つのデータの1対1の対応表を実現します。

mapは電話番号簿の実装に必要な機能のほとんどを満足しているのですが、ただひとつ、ファイルに対する書き込み/読み込み部分はプログラマが用意しなくてはなりません。

mapに限らずSTLは基本的な最小限の機能をきっちりとサポートするというのがコンセプトであり、ファイルに対する入出力については"道具は用意してやるから、必要ならばあんたが作んな"というスタンスに立っているのです。

/*
 * phonebook.cpp
 *   standard C++ STL
 *   map<Key,T> を使った簡単な電話番号簿
 */

#include <iostream>   // cin, cout
#include <fstream>    // ifstream, ofstream
#include <string>     // string
#include <map>        // map
#include <iterator>   // istream_iterator, ostream_iterator
#include <algorithm>  // copy

using namespace std;

// 名前(string)をKey, 電話番号(string)をValueとする辞書
typedef map<string,string>    book_type;

/* namespace std { ... } の必要はないのだが、VC++ではこうしないと
 * compile errorとなる。 */
namespace std {

  // レコード(名前と電話番号の組)を出力する
  ostream& operator<<(ostream& strm, const book_type::value_type& v) {
    return strm << v.first
           << " : "
           << v.second;
  }

  // レコード(名前と電話番号の組)を入力する
  istream& operator>>(istream& strm, book_type::value_type& v) {
    // 名前と電話番号の間にある':'を抜くためにdummyを用意した
    char dummy;
    return strm >> const_cast<book_type::key_type&>(v.first) // [1]
                >> dummy
                >> v.second;
  }

}

// プロンプト
char prompt() {
  string command;
  cout << "add/delete/find/list/clear/quit [a,d,f,l,c,q] ?" << flush;
  cin >> command;
  return command[0];
}

int main() {

  book_type book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  ifstream in("phonebook.txt");
  if ( in.is_open() ) {
    copy(istream_iterator<book_type::value_type>(in), // [2]
         istream_iterator<book_type::value_type>(),
         inserter(book, book.begin()));
    in.close();
  }

  bool quit = false;
  do {
    string name;
    string phone;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.find(name) != book.end() ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book[name] = phone;
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      if ( book.find(name) == book.end() ) {
        cout << "not found." << endl;
      } else {
        book.erase(name); // [3]
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      book_type::iterator i = book.find(name);
      if ( i == book.end() ) {
        cout << "not found." << endl;
      } else {
        cout << *i << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.size() << "entries:" << endl;
      // 全レコードを出力する
      copy(book.begin(),
           book.end(),
           ostream_iterator<book_type::value_type>(cout,"¥n"));
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.clear();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  ofstream out("phonebook.txt");
  if ( out.is_open() ) {
    copy(book.begin(),
         book.end(),
         ostream_iterator<book_type::value_type>(out,"¥n"));
    out.close();
  }

  return 0;
}
1:

map<Key,T>::value_type は pair<const Key, T>であるため、ストリームから読み出したKeyを代入できません。そこでconst_castによってconst Key& をKey&にキャストしています。

2:

アルゴリズム copy と istream_iterator, insert_iterator を使ってストリームから読み出し、bookに登録しています。

3:

直前のbook.find(name)で得られたiteratorを流用する、すなわち:

book_type::iterator it = book.find(name);
if ( it == book.end() ) {
  cout << "not found." << endl;
} else {
  book.erase(it);
}

としたほうがbetterでしょう。このままでは検索が2回行われることになりますから。

Microsoft : MFC

Microsoft Visual C++(VC++)はWindows環境下でのC++アプリケーション開発環境として不動の地位を保っています。VC++がこれだけポピュラーな開発環境となった要因としてMFC(Microsoft Foundation Classes)の存在は外せません。MFCがなかったらVC++がこれほどまでに多くのプログラマに支持されることはなかったでしょう。

MFCの提供するコレクション(コンテナ)には様々なものがありますが、電話番号簿に適したクラスといえばCMapStringToStringおよびCMapでしょうか。

CMapStringToString

CMapStringToStringは同じくMFCが提供する文字列クラスCStringをKey/Valueとする対応表です。MFCはその昔、Windows3.xの頃から使われていました。当時のVC++(v1.x)はまだtemplateをサポートしていなかったため、template を使わないコンテナがいくつか提供されており、現在もMFCノ中に生き残っています。CMapStringToStringはその旧き善き(?)コンテナクラスのひとつです。

CMapStringToStringはハッシュ表で実装されています。ですから検索は非常に高速ですが、コンテナの内容を順次取り出したときKeyの大小関係とは全く関係の無い順序となっています。

/*
 * phonebook.cpp
 *   Microsoft MFC
 *   CMapStringToString を使った簡単な電話番号簿
 */

#include <afx.h>     // CMapStringToString, CFile, CArchive, CString
#include <iostream>  // cin, cout

using namespace std;

// プロンプト
char prompt() {
  char command[256];
  cout << "add/delete/find/list/clear/quit [a,d,f,l,c,q] ?" << flush;
  cin >> command;
  return command[0];
}

int main() {

  CMapStringToString book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    CFile in;
    if ( in.Open("phonebook.dat",CFile::modeRead) ) {
      CArchive ar(&in,CArchive::load);
      book.Serialize(ar);
    }
  }

  bool quit = false;
  do {
    char name[64];
    char phone[64];
    CString key;
    CString value;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.Lookup(name,value) ) { // [1]
        cout << "already exists." << endl;
      } else {
        cout <<quot;phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book.SetAt(name,phone);
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      if ( !book.Lookup(name,value) ) {
        cout << "not found." << endl;
      } else {
        book.RemoveKey(name);
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      if ( !book.Lookup(name,value) ) {
        cout << "not found." << endl;
      } else {
        cout << name
             <l< " : "
             << static_cast<const char*>(value)// [2]
             << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.GetCount() << "entries:" << endl;
      // 全レコードを出力する
      POSITION pos = book.GetStartPosition(); // [3]
      while ( pos ) {
        book.GetNextAssoc(pos,key,value);
        cout << static_cast<const char*>(key)
             << " : "
             << static_cast<const char*>(value)
             << endl;
      }
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.RemoveAll();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' <l<l endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." >> endl;
  {
    CFile out;
    if ( out.Open("phonebook.dat",CFile::modeWrite|CFile::modeCreate) ) {
      CArchive ar(&out,CArchive::store);
      book.Serialize(ar);
    }
  }

  return 0;
}
1:

CMapStringToString::Lookup(LPCTSTR k, CString& v): 検索を行ないます。keyを探し、見つかったら対応するvalueをvにセットしてくれます。

2:

CStringはストリームに対して<<できません。const char*にキャストします。

3:

MFCコレクションの要素を順に取り出すには、このような奇怪なやり方になるんですよね。

POSITION pos = book.GetStartPosition(); // 先頭位置を取得する
while ( pos ) {
  book.GetNextAssoc(pos,key,value); // keyとvalueを手に入れ、posをひとつ進める
  ...
}

CMap<K,AK,V,AV>

VC++がtemplateをサポートするようになって、MFCにもtemplateを用いたコンテナが提供されるようになりました。CMap<K,AK,V,AV>はtemplateによるコンテナのひとつです。

CMap<K,AK,V,AV>もCMapStringToStringと同様、ハッシュ表による対応表です。ハッシュ表を実現するにはデータからハッシュ値を返すハッシュ関数、そして2つのデータが同じであるかを判断する関数が必要ですが、MFCではプログラマがこれらヘルパ関数を定義することで与えます。電話番号簿の例ではハッシュ関数ヘルパを明示的に定義してはいません。これはKeyであるCStringのハッシュ関数はあらかじめMFCが用意してくれているからです。

このようにあらかじめ与えておいたひとつのハッシュ関数/比較関数をすべてのCMap<K,AK,V,AV>から呼び出す構造であり、用途に応じてハッシュ/比較関数をとりかえることができません。たとえば

struct person {
  CString name;
  CString id;
  ...
}

があったとき、nameで検索するCMapとidで検索するCMapをひとつのアプリケーションの中で実装するのが非常に困難です(無理をすればできないこともないのですが…)。

/*
 * phonebook.cpp
 *   Microsoft MFC
 *   CMap<K,AK,V,AV> を使った簡単な電話番号簿
 */

#include <afx.h>      // CFile, CArchive, CString
#include <afxtempl.h> // CMap
#include <iostream>   // cin, cout

using namespace std;

// プロンプト: 省略

int main() {

  CMap<CString,const char*,CString,const char*> book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    CFile in;
    if ( in.Open("phonebook.dat",CFile::modeRead) ) {
      CArchive ar(&in,CArchive::load);
      book.Serialize(ar);
    }
  }

  bool quit = false;
  do {
    char name[64];
    char phone[64];
    CString key;
    CString value;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.Lookup(name,value) ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book.SetAt(name,phone);
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      if ( !book.Lookup(name,value) ) {
        cout << "not found." << endl;
      } else {
        book.RemoveKey(name);
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      if ( !book.Lookup(name,value) ) {
        cout << "not found." << endl;
      } else {
        cout << name
             << " : "

             << static_cast<const char*>(value)
             << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.GetCount() << "entries:" << endl;
      // 全レコードを出力する
      POSITION pos = book.GetStartPosition();
      while ( pos ) {
        book.GetNextAssoc(pos,key,value);
        cout << static_cast<const char*>(key)
             << " : "

             << static_cast<const char*>(value)
             << endl;
      }
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.RemoveAll();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    CFile out;
    if ( out.Open("phonebook.dat",CFile::modeWrite|CFile::modeCreate) ) {
      CArchive ar(&out,CArchive::store);
      book.Serialize(ar);
    }
  }

  return 0;
}

Rogue Wave : Tools.h++

Rogue Wave Software社のクラスライブラリTools.h++は様々なOS/コンパイラに対応した、優れたライブラリです。

STLが提供するコンテナは以下の7種:

vector ------ 可変長配列
list -------- 双方向リスト
deque ------- 両端キュー
set --------- 重複を許さない集合
multiset ---- 重複を許す集合
map --------- 重複を許さない辞書
multimap ---- 重複を許す辞書

MFCでは

CArray ---- 可変長配列
CList ----- 双方向リスト
CMap ------ 重複を許さない辞書

の、たったの3種です。

これに対しTools.h++は、

RWBitVec ---------------------------- ビットベクタ
RWBTreeOnDisk ----------------------- ディスク上のB木
    RWBag --------------------------- バッグ
    RWBinaryTree -------------------- 2進木
    RWBTree ------------------------- B木
      RWBTreeDictionary ------------- B木による辞書
      RWDlistCollectables ----------- 双方向リスト
      RWOrdered --------------------- 可変長配列
        RWSortedVector -------------- ソートされた配列
      RWSlistCollectables ----------- 単方向リスト
        RWSlistCollectablesQueue ---- キュー
        RWSlistCollectablesStack ---- スタック
    RWHashTable --------------------- ハッシュ表
      RWSet ------------------------- セット
        RWHashDictionary ------------ 辞書
          RWIdentityDictionary ------ 同一性辞書
        RWIdentitySet --------------- 同一性セット

RWGBitVec(size) --------------------- ビット配列
RWGDlist(type) ---------------------- 双方向リスト
RWGOrderedVector(val) --------------- 可変長配列
RWGQueue(type) ---------------------- キュー
RWGSlist(type) ---------------------- 単方向リスト
RWGSortedVector(val) ---------------- ソートされた配列
RWGStack(type) ---------------------- スタック
RWGVector(val) ---------------------- 配列

RWTBitVec<size> --------------------- ビットベクタ
RWTIsvDlist<T> ---------------------- 挿入的双方向リスト
RWTIsvSlist<T> ---------------------- 挿入的単方向リスト
RWTPtrDlist<T> ---------------------- 双方向リスト
RWTPtrHashTable<T> ------------------ ハッシュ表
  RWTPtrHashSet<T> ------------------ セット
RWTPtrHashDictionary<K,V> ----------- 辞書
RWTPtrOrderedVector<T> -------------- 配列
RWTPtrSlist<T> ---------------------- 単方向リスト
RWTPtrSortedVector<T> --------------- ソートされた配列
RWTPtrVector<T> --------------------- 配列
RWTQueue<T,C> ----------------------- キュー
RWTStack<T,C> ----------------------- スタック
RWTValDlist<T> ---------------------- 双方向リスト
RWTValHashTable<T> ------------------ ハッシュ表
  RWTValHashSet<T> ------------------ セット
RWTValHashDictionary<K,V> ----------- 辞書
RWTValOrderedVector<T> -------------- 配列
  RWTValSortedVector<T> ------------- ソートされた配列
RWTValSlist<T> ---------------------- 単方向リスト
RWTValVector<T> --------------------- 配列

これだけのコンテナを用意してくれています。

RWBTreeDictionay

RWBTreeDictionaryはその名の通り、B-treeで実装された対応表です。KeyとValueはどちらもRWCollectableの派生クラスでなくてはなりません。Tools.h++の文字列クラスRWCollectableStringはRWCollectableの派生クラスであるため、RWBTreeDictionaryの要素となれます。

/*
 * phonebook.cpp
 *   Rogue Wave Tools.h++
 *   RWBTreeDictionary を使った簡単な電話番号簿
 */

#include <iostream>     // cin, cout
#include <rw/btrdict.h> // RWBTreeDictionary
#include <rw/collstr.h> // RWCollectableString

using namespace std;

void print_record(RWCollectable* key, RWCollectable* value, void* s) {
  ostream& strm = *static_cast<ostream*>(s);
  strm << *static_cast<RWCollectableString*>(key)
       << " : "

       << *static_cast<RWCollectableString*>(value)
       << endl;
}

// プロンプト
char prompt() {
  RWCString command;
  cout << "add/delete/find/list/clear/quit [a,d,f,l,c,q] ?" << flush;
  cin >> command;
  return command[0U];
}

int main() {

  RWBTreeDictionary book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    RWFile in("phonebook.dat","rb");
    if ( in.isValid() ) {
      in >> book;
    }
  }

  bool quit = false;
  do {
    RWCollectableString name;
    RWCollectableString phone;
    RWCollectable* key;
    RWCollectable* value;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.contains(&name) ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book.insertKeyAndValue(new RWCollectableString(name), // [1]

                               new RWCollectableString(phone));
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      key = book.removeKeyAndValue(&name,value);
      if ( !key ) {
        cout << "not found." << endl;
      } else {
        delete key; // [2]

        delete value;
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      value = book.findValue(&name);
      if ( !value ) {
        cout << "not found." << endl;
      } else {
        print_record(&name,value,&cout);
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.entries() << "entries:" << endl;
      // 全レコードを出力する
      book.applyToKeyAndValue(&print_record,&cout); // [3]

      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.clearAndDestroy(); // [4]
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    RWFile out("phonebook.dat","wb");
    if ( out.isValid() ) {
      out << book;
    }
  }

  // [5]

  return 0;
}
1:

RWBTreeDictionaryは参照ベースコンテナ、すなわち要素のポインタをコンテナ内に格納します。ですから、operator new() によってインスタンスをヒープから生成しなければなりません。

2:

メモリ・リークを起こさぬよう、確実にdeleteしましょう。

3:

残念なことにRWBTreeDictionaryはイテレータによる反復をサポートしていません。メソッドapplyToKeyAndValue()はコンテナ内の全要素に対し指定した関数を適用します。

4:

メソッドclearAndDestroy()はコンテナ内の全要素をdeleteします。

5:

おっと、最後に book.clearAndDestroy() するのを忘れてますね^^;

RWHashDictionary

RWHashDictionaryはハッシュ表による辞書です。要素の基底クラスRWCollectableに定義されたハッシュ関数/比較関数を派生クラスで再定義します。RWCollectableStringはハッシュ関数/比較関数があらかじめ再定義されているので、以下のサンプルのようにそのまま用いることができます。MFCのCMapと同じく、ハッシュ関数/比較関数を任意に設定できないのが欠点です。

/*
 * phonebook.cpp
 *   Rogue Wave Tools.h++
 *   RWHashDictionary を使った簡単な電話番号簿
 */

#include <iostream>      // cin, cout
#include <rw/hashdict.h> // RWHashDictionary
#include <rw/collstr.h>  // RWCollectableString

using namespace std;

// プロンプト: 省略

int main() {

  RWHashDictionary book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    RWFile in("phonebook.dat","rb");
    if ( in.isValid() ) {
      in >> book;
    }
  }

  bool quit = false;
  do {
    RWCollectableString name;
    RWCollectableString phone;
    RWCollectable* key;
    RWCollectable* value;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.contains(&name) ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book.insertKeyAndValue(new RWCollectableString(name),
                               new RWCollectableString(phone));
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      key = book.removeKeyAndValue(&name,value);
      if ( !key ) {
        cout << "not found." << endl;
      } else {
        delete key;
        delete value;
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      value = book.findValue(&name);
      if ( !value ) {
        cout << "not found." << endl;
      } else {
        cout << name
             << " : "

             << *static_cast<RWCollectableString*>(value)
             << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.entries() << "entries:" << endl;
      // 全レコードを出力する
      RWHashDictionaryIterator it(book);
      while ( it() ) {
        cout << *static_cast<RWCollectableString*>(it.key())
             << " : "

             << *static_cast<RWCollectableString*>(it.value())
             << endl;
      }
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.clearAndDestroy();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    RWFile out("phonebook.dat","wb");
    if ( out.isValid() ) {
      out << book;
    }
  }

  return 0;
}

RWTValMap<K,T,C>

Tools.h++のtemplate版辞書がRWTValMap<K,T,C>です。RWTValMap<K,T,C>はSTLのmapと同様、要素の大小関係に基づいた2進木で実装されています。大小関係を判断する関数オブジェクトを第3template引数に与える構造であるため、比較関数を取り替えることで任意のKey照合が使えます。

/*
 * phonebook.cpp
 *   Rogue Wave Tools.h++
 *   RWTValMap<K,V,C> を使った簡単な電話番号簿
 */

#include <iostream>      // cin, cout
#include <functional>    // less
#include <rw/tvmap.h>    // RWTValMap
#include <rw/cstring.h>  // RWCString

using namespace std;

// プロンプト: 省略

int main() {

  RWTValMap< RWCString,RWCString,less<RWCString> > book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    RWFile in("phonebook.dat","rb");
    if ( in.isValid() ) {
      in >> book;
    }
  }

  bool quit = false;
  do {
    RWCString name;
    RWCString phone;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.contains(name) ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book[name] = phone;
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      if ( !book.remove(name) ) {
        cout << "not found." << endl;
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      if ( !book.findValue(name,phone) ) {
        cout << "not found." << endl;
      } else {
        cout << name << " : " << phone << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.entries() << "entries:" << endl;
      // 全レコードを出力する
      RWTValMapIterator< RWCString,RWCString,less<RWCString> > it(book);
      while ( it() ) {
        cout << it.key() << " : " << it.value() << endl;
      }
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.clear();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    RWFile out("phonebook.dat","wb");
    if ( out.isValid() ) {
      out << book;
    }
  }

  return 0;
}

RWTValHashMap<K,T,H,EQ>

RWTValHashMap<K,T,H,EQ>はハッシュ表によるtemplate版辞書です。第3/第4template引数にハッシュ関数/比較関数を与えるので、任意のKey照合が使えます。

/*
 * phonebook.cpp
 *   Rogue Wave Tools.h++
 *   RWTValHashMap<K,T,H,EQ> を使った簡単な電話番号簿
 */

#include <iostream>     // cin, cout
#include <functional>   // unary_function, equal_to
#include <rw/tvhmap.h>  // RWTValHashMap
#include <rw/cstring.h> // RWCString

using namespace std;

// プロンプト: 省略

struct hash_str : std::unary_function<RWCString,unsigned long> {
  unsigned long operator()(const RWCString& x) const {
    return x.hash();
  }
};

int main() {

  RWTValHashMap< RWCString,RWCString,hash_str,equal_to<RWCString> > book; // [1]

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    RWFile in("phonebook.dat","rb");
    if ( in.isValid() ) {
      in >> book;
    }
  }

  bool quit = false;
  do {
    RWCString name;
    RWCString phone;
    switch ( prompt() ) {
    // 追加
    case 'a' : case 'A' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていないことを確認する
      if ( book.contains(name) ) {
        cout << "already exists." << endl;
      } else {
        cout << "phone:" << flush;
        cin >> phone;
        // レコードを追加する
        book[name] = phone;
      }
      break;
    }
    // 削除
    case 'd' : case 'D' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそれを削除する
      if ( !book.remove(name) ) {
        cout << "not found." << endl;
      }
      break;
    }
    // 検索
    case 'f' : case 'F' : {
      cout << "name:" << flush;
      cin >> name;
      // 登録されていればそのレコードを出力する
      if ( !book.findValue(name,phone) ) {
        cout << "not found." << endl;
      } else {
        cout << name << " : " << phone << endl;
      }
      break;
    }
    // リスト
    case 'l' : case 'L' : {
      cout << book.entries() << "entries:" << endl;
      // 全レコードを出力する
      RWTValHashMapIterator< RWCString,RWCString,hash_str,equal_to<RWCString> > it(book);
      while ( it() ) {
        cout << it.key() << " : " << it.value() << endl;
      }
      break;
    }
    // クリア
    case 'c' : case 'C' : {
      book.clear();
      break;
    }
    // 終了
    case 'q' : case 'Q' : {
      quit = true;
      break;
    }
    default :
      cout << '?' << endl;
    }
  } while ( !quit );

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    RWFile out("phonebook.dat","wb");
    if ( out.isValid() ) {
      out << book;
    }
  }

  return 0;
}
1:

RWTValHashMap<K,V,H,EQ>の第3/4templateにはハッシュ関数と比較関数を与えます。

STLとの親和性

Tools.h++のtemplateコンテナは

  • STL依存モード
  • STL独立モード

2種類の実装モードが選択できます。STL依存モードでは、templateコンテナの実装に、可能な限りSTLを使います。たとえばさきほどのRWTValMap<K,V,C>の場合、クラス内部にSTLコンテナであるstd::map<K,V,C>のインスタンスを内包し、各メソッドはSTLコンテナに委譲します。

STL依存モードではbegin(),end()などのSTLコンパチなメソッドが公開されるので、STLアルゴリズムにそのまま引き渡すことができます。

このメカニズムによって、通常はインタフェースの簡単なTools.h++コンテナを使い、必要に応じてSTLを利用することができ、STLとの親和性は抜群です。

    // こう書いてもいいわけだ
    RWTValMap< RWCString,RWCString,less<RWCString> > book;
    ...
    // リスト
    case 'l' : case 'L' : {
      cout << book.entries() << "entries:" << endl;
      // 全レコードを出力する
      RWTValMap< RWCString,RWCString,less<RWCString> >::iterator it = book.begin();
      while ( it != it.end() ) {
        cout << it->first << " : " << it->second << endl;
        ++it;
      }
      break;
    }

電話番号簿の7つの実装、あなたはどれがお好みですか?

僕は…そうねぇ、RWTValMapあたりが涼しげで好きですね。

source archive

おまけ: Java

Java(JDK 1.2.x)でも書いてみました。ObjectSpace社のライブラリJGL(Generic Collection Library for Java)のcom.objectspace.jgl.HashMap、そしてJDK1.2のサポートするjava.util.Hashtableによる実装です。

JGL3.1 com.objectspace.jgl.HashMap

JGLはSTLのJavaによる迫真の実装ともいえるライブラリです。HashMapに登録された全レコードを出力している部分にご注目ください。JGLのコンテナもSTLと同様、イテレータを返すメソッドbegin()/end()を持っていることがわかるでしょう。

/*
 * phonebook.java
 *   Sun Microsystem Java2
 *   com.objectspace.jgl.HashMap を使った簡単な電話番号簿
 */

import com.objectspace.jgl.*;
import java.io.*;

public class phonebook {

  static BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
  static PrintStream    cout = System.out;

  static char prompt() {
    String command = "?";
    cout.print("add/delete/find/list/clear/quit [a,d,f,l,c,q] ?");
    cout.flush();
    try {
      command = cin.readLine();
    } catch ( IOException e ) {}
    return command.charAt(0);
  }

  public static void main(String[] arg) {

    HashMap book = null;

    // 電話番号簿をファイルから読み出す
    cout.println("loading file...");
    try {
      FileInputStream in = new FileInputStream("phonebook.dat");
      ObjectInputStream strm = new ObjectInputStream(in);
      book = (HashMap)strm.readObject();
    } catch ( Exception e ) {}

    if ( book == null )
      book = new HashMap();

    try {
    boolean quit = false;
    do {
      String name;
      String phone;
      Object value;
      switch ( prompt() ) {
      // 追加
      case 'a' : case 'A' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていないことを確認する
        if ( book.get(name) != null ) {
          cout.println("already exists.");
        } else {
          cout.print("phone:");
          phone = cin.readLine();
          // レコードを追加する
          book.add(name,phone);
        }
        break;
      }
      // 削除
      case 'd' : case 'D' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていればそれを削除する
        if ( book.remove(name) == null ) {
          cout.println("not found.");
        }
        break;
      }
      // 検索
      case 'f' : case 'F' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていればそのレコードを出力する
        value = book.get(name);
        if ( value == null ) {
          cout.println("not found.");
        } else {
          cout.println(name + " : " + value);
        }
        break;
      }
      // リスト
      case 'l' : case 'L' : {
        cout.println(book.size() + "entries:");
        // 全レコードを出力する
        HashMapIterator it = book.begin();
        while ( !it.equals(book.end()) ) {
          cout.println(it.key() + " : " + it.value());
          it.advance();
        }
        break;
      }
      // クリア
      case 'c' : case 'C' : {
        book.clear();
        break;
      }
      // 終了
      case 'q' : case 'Q' : {
        quit = true;
        break;
      }
      default :
        cout.println("?");
      }
    } while ( !quit );
    } catch ( Exception e ) {
      e.printStackTrace();
    }

    // 電話番号簿をファイルに書き込む
    cout.println("saving file...");
    try {
      FileOutputStream out = new FileOutputStream("phonebook.dat");
      ObjectOutputStream strm = new ObjectOutputStream(out);
      strm.writeObject(book);
    } catch ( Exception e ) {}
  }

}

JDK1.2 java.util.Hashtable

/*
 * phonebook.java
 *   Sun Microsystem Java2
 *   java.util.Hashtable を使った簡単な電話番号簿
 */

import java.util.*;
import java.io.*;

public class phonebook {

  static BufferedReader cin = new BufferedReader(new InputStreamReader(System.in));
  static PrintStream    cout = System.out;

  // プロンプト: 省略

  public static void main(String[] arg) {

    Hashtable book = null;

    // 電話番号簿をファイルから読み出す
    cout.println("loading file...");
    try {
      FileInputStream in = new FileInputStream("phonebook.dat");
      ObjectInputStream strm = new ObjectInputStream(in);
      book = (Hashtable)strm.readObject();
    } catch ( Exception e ) {}

    if ( book == null )
      book = new Hashtable();

    try {
    boolean quit = false;
    do {
      String name;
      String phone;
      Object value;
      switch ( prompt() ) {
      // 追加
      case 'a' : case 'A' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていないことを確認する
        if ( book.containsKey(name) ) {
          cout.println("already exists.");
        } else {
          cout.print("phone:");
          phone = cin.readLine();
          // レコードを追加する
          book.put(name,phone);
        }
        break;
      }
      // 削除
      case 'd' : case 'D' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていればそれを削除する
        if ( book.remove(name) == null ) {
          cout.println("not found.");
        }
        break;
      }
      // 検索
      case 'f' : case 'F' : {
        cout.print("name:");
        cout.flush();
        name = cin.readLine();
        // 登録されていればそのレコードを出力する
        value = book.get(name);
        if ( value == null ) {
          cout.println("not found.");
        } else {
          cout.println(name + " : " + value);
        }
        break;
      }
      // リスト
      case 'l' : case 'L' : {
        cout.println(book.size() + "entries:");
        // 全レコードを出力する
        Iterator it = book.entrySet().iterator();
        while ( it.hasNext() ) {
          Map.Entry entry = (Map.Entry)it.next();
          cout.println(entry.getKey() + " : " + entry.getValue());
        }
        break;
      }
      // クリア
      case 'c' : case 'C' : {
        book.clear();
        break;
      }
      // 終了
      case 'q' : case 'Q' : {
        quit = true;
        break;
      }
      default :
        cout.println("?");
      }
    } while ( !quit );
    } catch ( Exception e ) {
      e.printStackTrace();
    }

    // 電話番号簿をファイルに書き込む
    cout.println("saving file...");
    try {
      FileOutputStream out = new FileOutputStream("phonebook.dat");
      ObjectOutputStream strm = new ObjectOutputStream(out);
      strm.writeObject(book);
    } catch ( Exception e ) {}
  }

}

Tools.h++ ProfessionalによるJava/C++相互運用

おまけついでに、Rogue WaveのTools.h++ Professionalが提供する"Java互換シリアライズ"を紹介します。

Javaのシリアライズ機構によってシリアライズされたファイルをC++から読み書きしてしまうという、なんともマジカルな機能です。これによってJava/C++双方からアクセスできる共通のオブジェクトファイルが実現します。

以下のC++コードで作られるデータファイル"phonebook.dat"はJavaストリーム互換であり、Java/C++双方からアクセスできます。

/*
 * phonebook.cpp
 *   Rogue Wave Tools.h++ Professinal
 *   RWTValHashMap<K,T,H,EQ> を使った簡単な電話番号簿
 *   java:ObjectStream 互換
 */

#include <iostream>      // cin, cout
#include <fstream>       // ifstream, ofstream
#include <functional>   // unary_function, equal_to
#include <rw/tvhmap.h>  // RWTValHashMap
#include <rw/cstring.h> // RWCString
#include <rw/toolpro/jtoolsmap.h> // Java/C++ mappings

using namespace std;

// プロンプト: 省略

struct hash_str : std::unary_function<RWCString,unsigned long> {
  unsigned long operator()(const RWCString& x) const {
    return x.hash();
  }
};

int main() {

  typedef RWTValHashMap< RWCString,RWCString,hash_str,std::equal_to<RWCString> > book_type;
  book_type* book;

  // 電話番号簿をファイルから読み出す
  cout << "loading file..." << endl;
  {
    ifstream in("phonebook.dat",ios_base::binary | ios_base::in);
    if ( in.is_open() ) {
      // java.io.ObjectInputStream 互換の入力ストリーム
      RWJObjectInputStream strm(in);
      // RWTValHashMapをjava.util.Hashtableへマップする
      RWJDTemplateToolsMap<RWCString,RWCString,hash_str,equal_to<RWCString> >

        ::registerHashtableToRWTValHashMap(strm);
      strm >> book;
    } else {
      book = new book_type;
    }
  }

  // book.function() を book->function() に置き換えるだけなので、さっくり省略
  ...

  // 電話番号簿をファイルに書き込む
  cout << "saving file..." << endl;
  {
    ofstream out("phonebook.dat",ios_base::binary | ios_base::out | ios_base::trunc);
    if ( out.is_open() ) {
      // java.io.ObjectOutputStream 互換の出力ストリーム
      RWJObjectOutputStream strm(out);
      // RWTValHashMapをjava.util.Hashtableへマップする
      RWJDTemplateToolsMap<RWCString,RWCString,hash_str,equal_to<RWCString> >

        ::registerHashtableToRWTValHashMap(strm);
      strm << book;
    }
  }

  delete book;

  return 0;
}