C++の新しいキャスト
Monday, April 2nd, 2007従来のキャストの問題点
異なる型への変換において、C/C++ではキャストが用いられます。
// intからlongへのキャスト int ival; int lval = (long)ival;
ご存知のとおり、キャストは非常に危険です。 本来ならば型の不一致によるコンパイルエラーをねじ伏せるのですから。
異なる型への変換において、C/C++ではキャストが用いられます。
// intからlongへのキャスト int ival; int lval = (long)ival;
ご存知のとおり、キャストは非常に危険です。 本来ならば型の不一致によるコンパイルエラーをねじ伏せるのですから。
Cではsizeofによって構造体がメモリ上で占める大きさ(バイト数)を知ることができます。
struct s{
int x;
int y;
};
...
cout << sizeof(s) << endl;
僕の愛用する処理系、Visual C++ 6.0では 8が得られました。intひとつにつき4byteを消費するからでしょう。
それではC++でのclassの大きさはどうでしょう。内包するメンバ変数それぞれの占めるバイト数の総和になるのでしょうか。
をオブジェクト指向の三本柱などと称しています。その中でも抽象データ型(あるいはデータの抽象化)はオブジェクト指向の最も基本的で重要な概念ではないかと考えます。
"データを抽象化する"とは、データをそれに対して適用できる操作の集合で定義することです。
簡単な例として"カウンタ"を考えてみましょう。カウンタには3つの操作:
を提供させることにします。
さて、このカウンタをCで実現するとどうなるでしょうか…
前作XMLを用いた状態遷移では状態遷移表をXMLで記述し、それをXMLパーサで解析して状態遷移表のふるまいを動的に再現すると言う試みを紹介しました。
今回はここからさらに発展させ、XMLで記述された状態遷移表に基づいて、そこに表現されたとおりに振る舞うC++/Javaのソースコードを生成することを考えます。つまりはSMCのXML版です。SMCでは構文解析/字句解析にyacc/lexを用いていましたが、XMLで記述することで、yacc/lexから解放されました。
前回用いた例”(門番のいる)ゲート”の状態遷移表を再度示します。
オブジェクト(クラス)のメンバ変数や関数の引数に値を用いるか、あるいは参照(ポインタ)を用いるかはプログラマの頭を悩ませます。
一般には値の方が良い、とされています。なぜなら値は参照(ポインタ)よりシンプルだからです。
ポインタが一方から他方へ引き渡されたとき、一つの実体を複数の場所で共有することになります。このときプログラマは実体のライフタイム(その実体をどこで/いつdeleteするか)を厳密に管理しなければなりません。複雑に絡み合ったポインタを矛盾なく解きほぐすのは相当に神経を使う作業となります。
しかしながら一方で値には問題があります。ひとつには値のコピーはコスト高となることがあるのです。オブジェクト内部に数多くのメンバを内在するときなど、そのコピーに必要な時間と手間はポインタに較べて非常に大きくなります。また、値渡しとはコピーを作ってそれを他方に引き渡すことになります。引き渡された側ではその本体(コピー元)にアクセスすることができず、値渡しそのこと自体が誤りである場合も少なくありません。
Handle-Bodyは値と参照(ポインタ)の双方のメリットを活かすイディオム(定石)です。
Handle-Bodyイディオムによって、ポインタを値のように扱うことが可能となり、オブジェクトの共有やライフタイム管理、そしてコストのかからないコピーといったことが実現できます。
[part-1]に引き続き、DBTools.h++を使ったRDBの操作法に関する解説を続けます。
[part-1]ではひとつのテーブルに対するレコードの追加/変更/削除/検索を行ないました。では、結合された複数のテーブルから検索条件を指定して読み出してみる事にしましょう。
僕のSTL本"Standard Template Library プログラミング"では、STLコンテナのサンプルとして、簡単な電話番号簿を紹介しています。この本の中ではひとつのアプリケーション(電話番号簿)を、STLが提供する様々なコンテナを使って実装してみました。
このアーティクルでは趣向を変えて、電話番号簿アプリケーションをSTL,MFCコレクション,そしてTools.h++を使った実装を試み、それぞれの違いをご覧にいれましょう。
電話番号簿は"電話番号(Key)と名前(Value)の対応表(Map)"です。電話番号簿の機能は:
の5つです。
ANSI/ISO standard C++(標準C++)のライブラリの一部として組み入れられたSTL。STLは数多くのライブラリの中でも最もポータビリティに優れたライブラリといえるでしょう。何といっても標準ですからね。
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;
}
map<Key,T>::value_type は pair<const Key, T>であるため、ストリームから読み出したKeyを代入できません。そこでconst_castによってconst Key& をKey&にキャストしています。
アルゴリズム copy と istream_iterator, insert_iterator を使ってストリームから読み出し、bookに登録しています。
直前の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 Visual C++(VC++)はWindows環境下でのC++アプリケーション開発環境として不動の地位を保っています。VC++がこれだけポピュラーな開発環境となった要因としてMFC(Microsoft Foundation Classes)の存在は外せません。MFCがなかったらVC++がこれほどまでに多くのプログラマに支持されることはなかったでしょう。
MFCの提供するコレクション(コンテナ)には様々なものがありますが、電話番号簿に適したクラスといえばCMapStringToStringおよびCMapでしょうか。
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;
}
CMapStringToString::Lookup(LPCTSTR k, CString& v): 検索を行ないます。keyを探し、見つかったら対応するvalueをvにセットしてくれます。
CStringはストリームに対して<<できません。const char*にキャストします。
MFCコレクションの要素を順に取り出すには、このような奇怪なやり方になるんですよね。
POSITION pos = book.GetStartPosition(); // 先頭位置を取得する
while ( pos ) {
book.GetNextAssoc(pos,key,value); // keyとvalueを手に入れ、posをひとつ進める
...
}
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 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> --------------------- 配列
これだけのコンテナを用意してくれています。
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;
}
RWBTreeDictionaryは参照ベースコンテナ、すなわち要素のポインタをコンテナ内に格納します。ですから、operator new() によってインスタンスをヒープから生成しなければなりません。
メモリ・リークを起こさぬよう、確実にdeleteしましょう。
残念なことにRWBTreeDictionaryはイテレータによる反復をサポートしていません。メソッドapplyToKeyAndValue()はコンテナ内の全要素に対し指定した関数を適用します。
メソッドclearAndDestroy()はコンテナ内の全要素をdeleteします。
おっと、最後に book.clearAndDestroy() するのを忘れてますね^^;
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;
}
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>はハッシュ表による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;
}
RWTValHashMap<K,V,H,EQ>の第3/4templateにはハッシュ関数と比較関数を与えます。
Tools.h++のtemplateコンテナは
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あたりが涼しげで好きですね。
Java(JDK 1.2.x)でも書いてみました。ObjectSpace社のライブラリJGL(Generic Collection Library for Java)のcom.objectspace.jgl.HashMap、そしてJDK1.2のサポートするjava.util.Hashtableによる実装です。
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 ) {}
}
}
/*
* 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 ) {}
}
}
おまけついでに、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;
}
Apache Xerces-Cは最も広く用いられているC++版XML-parserのひとつです。 Apache XMLのwebサイトhttp:://xml.apache.org/からリンクをたどればXerces-CのWin32バイナリ版が入手でき、Visual C++ 6.0で利用できます。 ですがこのバイナリ版にはひとつだけ困った問題があります。
Win32バイナリ版Xerces-Cをインストールし、その中のサンプル’DOMPrint’を使ってXMLをプリントしてみます。このときプリント対象となるXMLがshift_jisあるいはUNICODE(UTF-16/UTF-8)であれば何の問題もないのですが、たとえばeuc-jpやiso-2022-jpで書かれたDOMを正しくparseすることができません。
domprint -x=shift_jis sjis.xml <?xml version="1.0" encoding="shift_jis"?> <message>この文字列はshift_jisです</message>
domprint -x=shift_jis utf.xml <?xml version="1.0" encoding="shift_jis"?> <message>この文字列はUTF-16です</message>
domprint -x=shift_jis utf8.xml <?xml version="1.0" encoding="shift_jis"?> <message>この文字列はUTF-8です</message>
domprint -x=shift_jis euc.xml <?xml version="1.0" encoding="shift_jis"?> <message>、ウ、ホハクサホ、マeuc-jp、ヌ、ケ</message>[1]
domprint -x=shift_jis jis.xml Fatal Error at file "jis.xml", line 2, column 11 Message: Invalid character (Unicode: 0x1B) An error occured during parsing
入力文字列をUNICODEに変換する際、XML-parserが内部で利用しているWindows-APIがiso-2022-jpやeuc-jpに対応していないからです。
Xerces-Cに添付されているHTMLドキュメントには文字コード変換にIBMのICUを利用するようにライブラリを修正する手順が明記されていますが、それによるとperlおよびUNIX-コンパチブルなshellを必要とします。ここではperlやshellを使わずにVisual C++ IDEでXerces-CをICU対応させる手順を紹介します。
※ 以下に示す手順は、2001年5月現在入手可能な最新版:
に対応しています。
IBM-ICUのサイトからICU1.8をダウンロードし、適当なディレクトリに展開します。以降、ICUを展開したディレクトリを<ICU>と表します(ここでは m:\icu とします)。ICU-distributionに含まれる<ICU>\source\allinone\allinone.dswをVC++IDEで開き、プロジェクト’all’のRelease/Debugライブラリ(DLL)をビルドします。
ビルドが完了したら<ICU>\bin\icuuc.dll, <ICU>\bin\icuucd.dllおよび<ICU>\source\data\icudt18l.dllをPATHの通ったディレクトリにコピーしておきます。
Apache XMLのサイトからXerces-C 1.4.0 Win32バイナリ版をダウンロードし、適当なディレクトリに展開します。以降、Xerces-C Win32バイナリ版を展開したディレクトリを<XERCES>と表します。
Apache XMLのサイトからXerces-C 1.4.0 ソースコード版をダウンロードし、適当なディレクトリに展開します。以降、Xerces-C ソースコード版を展開したディレクトリを<XERCESSRC>と表します。
<XERCESSRC>\D:\Projects\Win32\VC6\xerces-all\xerces-all.dswをVC++IDEで開き、プロジェクト設定を以下のとおりに変更します。
<ICU>\include を追加します。XML_USE_WIN32_TRANSCODER を XML_USE_ICU_TRANSCODER に書き換えます。
<ICU>\lib および <ICU>\source\data を追加します。icuuc.lib(Debug版はicuucd.lib) および icudata.lib を追加します。
Win32TransService.hpp, Win32TransService.cpp を削除し、<XERCESSRC>\src\util\Transcoders\ICU\ICUTransService.hpp, <XERCESSRC>\src\util\Transcoders\ICU\ICUTransService.cpp を追加します。
ビルドが完了すれば <XERCESSRC>\Build\Win32\Vc6\Release および <XERCESSRC>\Build\Win32\Vc6\Debug に
xerces-c_1.lib, xerces-c_1_4.dll (Release版)xerces-c_1D.lib, xerces-c_1_4D.dll (Debug版)が生成されています。これらを <XERCES>\lib, <XERCES>\bin にコピーします。
さて、ICU-enabled な Xerces-C の構築に成功したか、さきほどうまくいかなかったencodingによるXMLを再度 DOMPrint に食わせてみましょう。
domprint -x=shift_jis euc.xml <?xml version="1.0" encoding="shift_jis"?> <message>この文字列はeuc-jpです</message>
domprint -x=shift_jis jis.xml <?xml version="1.0" encoding="shift_jis"?> <message>この文字列はiso2022-jpです</message>
ちょっとした応用を考えてみましょう。XML-parserがiso-2022-jpを受け付けてくれるので、XMLドキュメントをメール(SMTP)に投げ、POP3で受け取ってparseすることができるはずです。
適当なメールアカウントに対し
<?xml version='1.0' encoding='iso-2022-jp' ?> <message>このメッセージは電子メールに投げられたXMLから取り出されました</message>
を送信し、以下のプログラムを起動すると、<message>タグに囲まれたテキスト
このメッセージは電子メールに投げられたXMLから取り出されました
がコンソールに出力されます
// --- StdLib. (SourcePro Core)
#include <iostream>
#include <sstream>
#include <locale>
// --- SourcePro Network
#include <rw/network/RWSocketPortal.h> // RWSocketPortal
#include <rw/network/RWPortalIStream.h> // RWPortalIStream
#include <rw/network/RWWinSockInfo.h> // RWWinSockInfo
#include <rw/internet/util.h> // rwNormalizeLine()
#include <rw/internet/RWStreamCoupler.h> // RWStreamCoupler
#include <rw/pop3/RWPop3Agent.h> // RWPop3Agent
// --- Xerces-C
#include <util/PlatformUtils.hpp> // PlatformUtils
#include <parsers/DOMParser.hpp> // DOMParser
#include <sax/ErrorHandler.hpp> // ErrorHandler
#include <sax/SAXParseException.hpp> // SAXParseException
#include <dom/DOM.hpp> // DOM_xxx
#include <framework/MemBufInputSource.hpp> // MemBufInputSource
/*
* definitions
*/
#define POP3_SVR "POP3サーバアドレス"
#define POP3_USR "ユーザ名"
#define POP3_PWD "パスワード"
/*
* namespace
*/
using namespace std;
/*
* ErrorHandler for DOMParser
*/
class ErrorReporter : public ErrorHandler {
bool ok_;
public:
ErrorReporter() : ok_(true) {}
~ErrorReporter() {}
virtual void warning(const SAXParseException& ex)
{ }
virtual void error(const SAXParseException& ex)
{ ok_ = false; }
virtual void fatalError(const SAXParseException& ex)
{ ok_ = false; }
virtual void resetErrors() { ok_ = true; }
bool ok() const { return ok_; }
};
/*
* XML-documentをparseし、
* ドキュメント中の 'message' タグの内容を表示する
*/
int parse_message(const string& message) {
DOMParser parser;
ErrorReporter errReporter;
parser.setErrorHandler(&errReporter);
parser.setExitOnFirstFatalError(true);
MemBufInputSource source(reinterpret_cast<const unsigned char*>(message.data()),
message.length(),
L"application/xml");
parser.parse(source);
if ( !errReporter.ok() ) {
return 1;
}
DOM_Document document = parser.getDocument();
DOM_NodeList nodes = document.getElementsByTagName("message");
for ( unsigned i = 0; i < nodes.getLength(); ++i ) {
DOM_Node node = nodes.item(i);
if ( node.getNodeType() != DOM_Node::ELEMENT_NODE ) {
continue;
}
char* message = node.getFirstChild().getNodeValue().transcode();
cout << message << endl;
delete[] message;
}
return 0;
}
/*
* POP3からメールを取り出し、メッセージをparse_message()へ
*/
int get_mail() {
RWPop3Agent agent(POP3_SVR, POP3_USR, POP3_PWD);
int n = agent.messages();
for ( int i = 1; i <= n; ++i ) {
RWSocketPortal portal = agent.get(i);
RWPortalIStream istrm(portal);
RWCString text;
bool body = false;
do {
text.readLine(istrm, false);
text = rwNormalizeLine(text);
if ( text.isNull() ) {
body = true;
break;
}
} while ( text != "." );
ostringstream ostrm;
if ( body ) {
RWStreamCoupler couple;
couple(istrm, ostrm, pop3StreamFilter);
agent.removeMessage(i);
parse_message(ostrm.str());
}
}
return 0;
}
/*
* メイン
*/
int main(int argc, char* argv[]) {
locale::global(locale("japanese"));
try {
RWWinSockInfo info;
XMLPlatformUtils::Initialize();
get_mail();
} catch ( RWxmsg& rwer) {
cerr << "Exception : " << rwer.why() << endl;
} catch ( std::exception& stder ) {
cerr << stder.what() << endl;
}
XMLPlatformUtils::Terminate();
return 0;
}
近頃XMLがやたらとクローズアップされてきています。サーバ/サーバ間あるいはクライアント/サーバ間のインタフェースとしてXMLを用いることで様々な接続に対応できるってことで、e-commerceをはじめとしたITの基礎技術として脚光を浴びてきたということでしょう。(e-commerceについては僕には良くわからないし、そんなbigなシステムを構築するつもりはありません^^;)
XMLで遊んでいると様々な使いみちを思い付きます。アプリケーションの設定情報なんかをXMLで書くとか、状態遷移表をXMLで表現するとか…
XMLは階層構造を持ったデータを表現する汎用のデータ・フォーマットです。階層構造を持ったデータを表現できるのなら、オブジェクトをXMLで表現できるだろうと考えました。たとえば、
class person {
string name_; // 名前
int age_; // 年齢
public:
person(string n, int a) : name_(n), age_(a) {}
...
};
のようなオブジェクトがあったとき、person(”adam”,24) を
<person> <name>adam</name> <age>24</age> </person>
のようなXMLに変換すること、逆にXMLからpersonを生成することができるんじゃないか…というわけ。
これができればアプリケーションが扱うオブジェクトの集合をXML化でき、XMLによるアプリケーション間でのオブジェクトのやりとりが可能になります。
…というわけで、XMLによるオブジェクトの永続化の試みです。
※このアーティクルでは、Microsoft XML Parser (MSXML.dll) を用いますが、XML4C や Xerces などXML Parserでも考え方は同じです。
例として
class person {
string name_; // 名前
int age_; // 年齢
public:
person(string n, int a) : name_(n), age_(a) {}
...
};
をXML化することを考えます。そのために新たなメソッドvirtual void save(IXMLDOMNodePtr node) constを追加します。
※IXMLDOMNodePtrはMSXMLにおけるDOMNodeのスマート・ポインタです。
メソッド save は、自分自身のメンバに基づいて DOMNode を生成します。たとえば name_ == "adam" , age_ == 24 であるpersonオブジェクトからは
<person> <name>adam</name> <age>24</age> </person>
なるDOMNodeを生成します。
次に、生成された DOMNode を引数 node の子エレメントとして追加します。たとえば node が <root/> (あるいは<root></root>) であるとすると、
<root>
<person>
<name>adam</name>
<age>24</age>
</person>
</root>
となります。node が既に子エレメントを持っていた場合、その末っ子として追加します。
まず、メソッド save の冒頭では、
を行ないます。
次にメンバ変数name_から<name>adam</name> なるDOMNodeを作るには、
というステップを踏みます。
これでふたつの DOMNode :
<person></person><name>adam</name>ができたので、
を行ないます。
全く同様に age_ から<age>24</age> なるDOMNode を作り、[1]の子ノードとして追加します。
最後に、save の引数として与えられた node の子ノードとして[1]を追加します。ここまでを実装してみましょう:
void person::save(IXMLDOMNodePtr node) const {
assert(node != 0);
IXMLDOMDocumentPtr doc = node->ownerDocument;
IXMLDOMNodePtr elem = doc->createElement("person");
IXMLDOMNodePtr item;
IXMLDOMTextPtr text;
item = doc->createElement("name");
text = doc->createTextNode(name_.c_str());
item->appendChild(text);
elem->appendChild(item);
item = doc->createElement("age");
char buff[16];
_itoa(age_,buff,10);
text = doc->createTextNode(buff);
item->appendChild(text);
elem->appendChild(item);
node->appendChild(elem);
}
personの派生クラス:
class programmer : public person {
string lang_; // 言語
...
};
のメソッドsave(IXMLDOMNode node) constでは、ベースクラスのメソッドsaveを再帰的に呼び出します。すなわち、
<person>...</person> を追加する<language>...</language>を作る実装は以下のようになります:
void programmer::save(IXMLDOMNodePtr node) const {
assert(node != 0);
IXMLDOMDocumentPtr doc = node->ownerDocument;
IXMLDOMNodePtr elem = doc->createElement("programmer");
IXMLDOMNodePtr item;
IXMLDOMTextPtr text;
person::save(elem);
item = doc->createElement("language");
text = doc->createTextNode(lang_.c_str());
item->appendChild(text);
elem->appendChild(item);
node->appendChild(elem);
}
これによって、programmerからは以下のような DOMNode が生成されることになります:
<programmer>
<person>
<name>bill</name>
<age>31</age>
</person>
<language>BASIC</language>
</programmer>
最後に、XMLとしての体裁を整える、すなわち、
という処理を行なうことでXMLによる永続データの完成です。
int save(string file) {
// 空のドキュメントを生成する
IXMLDOMDocumentPtr doc("MSXML.DOMDocument");
doc->appendChild(
doc->createProcessingInstruction(
// ルートエレメントを生成する。
IXMLDOMNodePtr root = doc->createElement("root");
doc->appendChild(root);
// [1] personとprogrammerをrootにsave
cout << "save person & programmer¥n";
person("adam",24).save(root);
programmer("bill",20,"BASIC").save(root);
// [2] vector<programmer>を生成
vector<programmer> pv;
pv.push_back(programmer("bjarne",28,"C++"));
pv.push_back(programmer("wirth",30,"Modula-2"));
// [3] rootの子"programmers"を作り、
// その中にvector<programmer>をsave
cout << "save programmers¥n";
IXMLDOMNodePtr pvelm = doc->createElement("programmers");
for ( int i = 0; i < pv.size(); ++i )
pv[i].save(pvelm);
root->appendChild(pvelm);
// XMLファイルを出力
doc->save(file.c_str());
return 0;
}
上記のサンプルコードによって生成されたpersist.xmlをIE5でブラウズしたイメージを以下に示します:

読み込みは前述の書き込みと同じ順序で DOMNode をほどいてゆきます。
すなわち personのメソッド void load(IXMLDOMNodePtr node)では、
<name>...</name>)を取得する<age>...</age>)を取得する void person::load(IXMLDOMNodePtr node) {
assert(node != 0);
assert(node->nodeName == _bstr_t("person"));
IXMLDOMNodePtr item;
IXMLDOMNodePtr text;
item = node->firstChild;
text = item->firstChild;
name_ = static_cast<const char*>(_bstr_t(text->nodeValue));
item = item->nextSibling;
text = item->firstChild;
age_ = atoi(static_cast<const char*>(_bstr_t(text->nodeValue)));
}
同様に programmer::load(IXMLDOMNodePtr node) では:
<person>...</person>)を取得する<language>...</language>)を取得する void programmer::load(IXMLDOMNodePtr node) {
assert(node != 0);
assert(node->nodeName == _bstr_t("programmer"));
IXMLDOMNodePtr item;
IXMLDOMNodePtr text;
item = node->firstChild;
person::load(item);
item = item->nextSibling;
text = item->firstChild;
lang_ = static_cast<const char*>(_bstr_t(text->nodeValue));
}
永続データ(すなわちXML)からオブジェクトを復元するには:
という処理を行なうことになります。
int load(string file) {
IXMLDOMDocumentPtr doc("MSXML.DOMDocument");
doc->validateOnParse = VARIANT_FALSE;
doc->load(file.c_str());
IXMLDOMNodePtr root = doc->documentElement;
IXMLDOMNodePtr node;
// save:[1] でsaveした person/programmer を読み出す
cout << "¥nload person & programmer¥n";
int i;
person* p;
node = root->firstChild;
p = node->nodeName == _bstr_t("person") ? new person : new programmer;
p->load(node);
p->print(cout);
cout << endl;
delete p;
node = node->nextSibling;
p = node->nodeName == _bstr_t("person") ? new person : new programmer;
p->load(node);
p->print(cout);
cout << endl;
delete p;
// save:[3] でsaveしたvector<programmer>を読み出す
cout << "¥nload programmers¥n";
vector<programmer> pv;
node = node->nextSibling;
for ( node = node->firstChild; node != 0; node = node->nextSibling ) {
programmer p;
p.load(node);
pv.push_back(p);
}
for ( i = 0; i < pv.size(); ++i ) {
pv[i].print(cout);
cout << endl;
}
// saveされたpersonを出力
// このとき、programmerが内包するpersonも含む
cout << "¥nload persons (including programmers)¥n";
IXMLDOMNodeListPtr nodes = doc->getElementsByTagName("person");
for ( i = 0; i < nodes->length; ++i ) {
person p;
p.load(nodes->item[i]);
p.print(cout);
cout << endl;
}
return 0;
}
上記コードの実行結果は以下のようになります:

#include <iostream> // ostream
#include <string> // string
#include <vector> // vector
#include <cassert> // assert
#include <cstdlib> // atoi, _itoa
#import "msxml.dll" rename_namespace("msxml")
namespace msxml {
/*
* テストのための2つのクラス : person と programmer
*/
class person {
std::string name_;
int age_;
public:
person(std::string name, int age)
: name_(name), age_(age) {}
person() {}
virtual void save(IXMLDOMNodePtr node) const {
assert(node != 0);
IXMLDOMDocumentPtr doc = node->ownerDocument;
IXMLDOMNodePtr elem = doc->createElement("person");
IXMLDOMNodePtr item;
IXMLDOMTextPtr text;
item = doc->createElement("name");
text = doc->createTextNode(name_.c_str());
item->appendChild(text);
elem->appendChild(item);
item = doc->createElement("age");
char buff[16];
_itoa(age_,buff,10);
text = doc->createTextNode(buff);
item->appendChild(text);
elem->appendChild(item);
node->appendChild(elem);
}
virtual void load(IXMLDOMNodePtr node) {
assert(node != 0);
assert(node->nodeName == _bstr_t("person"));
IXMLDOMNodePtr item;
IXMLDOMNodePtr text;
item = node->firstChild;
text = item->firstChild;
name_ = static_cast<const char*>(_bstr_t(text->nodeValue));
item = item->nextSibling;
text = item->firstChild;
age_ = atoi(static_cast<const char*>(_bstr_t(text->nodeValue)));
}
virtual void print(std::ostream& strm) const {
strm << name_ << '(' << age_ << ')';
}
};
class programmer : public person {
std::string lang_;
public:
programmer(std::string name, int age, std::string lang)
: person(name,age), lang_(lang) {}
programmer() {}
virtual void save(IXMLDOMNodePtr node) const {
assert(node != 0);
IXMLDOMDocumentPtr doc = node->ownerDocument;
IXMLDOMNodePtr elem = doc->createElement("programmer");
IXMLDOMNodePtr item;
IXMLDOMTextPtr text;
person::save(elem);
item = doc->createElement("language");
text = doc->createTextNode(lang_.c_str());
item->appendChild(text);
elem->appendChild(item);
node->appendChild(elem);
}
virtual void load(IXMLDOMNodePtr node) {
assert(node != 0);
assert(node->nodeName == _bstr_t("programmer"));
IXMLDOMNodePtr item;
IXMLDOMNodePtr text;
item = node->firstChild;
person::load(item);
item = item->nextSibling;
text = item->firstChild;
lang_ = static_cast<const char*>(_bstr_t(text->nodeValue));
}
virtual void print(std::ostream& strm) const {
person::print(strm);
strm << ':' << lang_;
}
};
/*
* お試し
*/
using namespace std;
int save(string file) {
// 空のドキュメントを生成する
IXMLDOMDocumentPtr doc("MSXML.DOMDocument");
doc->appendChild(
doc->createProcessingInstruction(
// ルートエレメントを生成する。
IXMLDOMNodePtr root = doc->createElement("root");
doc->appendChild(root);
// [1] personとprogrammerをrootにsave
cout << "save person & programmer¥n";
person("adam",24).save(root);
programmer("bill",20,"BASIC").save(root);
// [2] vector<programmer>を生成
vector<programmer> pv;
pv.push_back(programmer("bjarne",28,"C++"));
pv.push_back(programmer("wirth",30,"Modula-2"));
// [3] rootの子"programmers"を作り、
// その中にvector<programmer>をsave
cout << "save programmers¥n";
IXMLDOMNodePtr pvelm = doc->createElement("programmers");
for ( int i = 0; i < pv.size(); ++i )
pv[i].save(pvelm);
root->appendChild(pvelm);
// XMLファイルを出力
doc->save(file.c_str());
return 0;
}
int load(string file) {
IXMLDOMDocumentPtr doc("MSXML.DOMDocument");
doc->validateOnParse = VARIANT_FALSE;
doc->load(file.c_str());
IXMLDOMNodePtr root = doc->documentElement;
IXMLDOMNodePtr node;
// [1] でsaveした person/programmer を読み出す
cout << "¥nload person & programmer¥n";
int i;
person* p;
node = root->firstChild;
p = node->nodeName == _bstr_t("person") ? new person : new programmer;
p->load(node);
p->print(cout);
cout << endl;
delete p;
node = node->nextSibling;
p = node->nodeName == _bstr_t("person") ? new person : new programmer;
p->load(node);
p->print(cout);
cout << endl;
delete p;
// [3] でsaveしたvector<programmer>を読み出す
cout << "¥nload programmers¥n";
vector<programmer> pv;
node = node->nextSibling;
for ( node = node->firstChild; node != 0; node = node->nextSibling ) {
programmer p;
p.load(node);
pv.push_back(p);
}
for ( i = 0; i < pv.size(); ++i ) {
pv[i].print(cout);
cout << endl;
}
// saveされたpersonを出力
// このとき、programmerが内包するpersonも含む
cout << "¥nload persons (including programmers)¥n";
IXMLDOMNodeListPtr nodes = doc->getElementsByTagName("person");
for ( i = 0; i < nodes->length; ++i ) {
person p;
p.load(nodes->item[i]);
p.print(cout);
cout << endl;
}
return 0;
}
int entry(string file) {
int result;
if ( result = save(file) ) return result;
if ( result = load(file) ) return result;
return result;
}
}
using namespace msxml;
int main(int argc, char* argv[]) {
if ( FAILED(CoInitialize(0)) ) return 1;
int result = entry("persist.xml");
CoUninitialize();
return result;
}
私たちの身の回りには、自分で直接できることと自分では直接できないこととがあります。朝おきて何気なく読む新聞も、新聞社という組織が作り、近くの販売所が毎日届けてくれます。私たちが新聞を読むときには、誰がどのようにして作っているかを知る必要はありません。
また、あなたがエレベータに乗るときも、あなたは自分のいる階でボタンを押せばいずれエレベータがやってくると期待しているのです。それは、世の中にあるエレベータという乗り物がおおむねその様な動きをしていることを知っているからです。
駅まで行き、電車に乗る場合でも同じです。電車は、どこかの鉄道会社が作ったダイヤに従って動いているのです。あなたが、どの電車はこのように動きなさい、と規定するわけではありません。
通りでタクシーを拾う場合もそうです。あるタクシーは客待ちをしており、また、あるタクシーは街の中を流して客を探しているでしょう。しかし、タクシーを利用するときに、すべてのタクシーがどのような動きをしているかを知らなくてもよいのです。しかも、たいていは、目的地までの道順をあなたが知らなくても、目的地にたどり着くことができます。
私たちひとりひとりが個人で知り得る知識や個人で直接行える事柄には限りがありますが、それにもかかわらず私たちがさまざまなことをなし得るのは、社会のしくみそのものが、私たちの意志とは無関係に、ある一定のルールにしたがって自立的に動いているからなのです。
エレベータも電車もタクシーもその使い方さえ知っていれば使える便利な「もの」なのです。これをオブジェクトと呼ぶことにしましょう。
さて、今度はソフトウェアの世界のことを考えてみましょう。普通のソフトウェアのシステムの世界で何かやろうとするときに、ひとつの仕事をサブルーチン(手続きの流れを記述したソフトウェアの構成要素)という形で記述することがあります。しかし、このサブルーチンは私たちの世界の中のエレベータや電車のようなシステムにはなっていません。なぜならサブルーチンは、それを使うときになって初めて生成され、それ以外のときには存在しないからです。
ソフトウェアの世界でエレベータに乗るということを考えた場合、いったいどのようなことになるのでしょうか。従来のソフトウェアの世界では、まずエレベータのサブルーチンが初期化されます。初期化に従って1階にいるとか10階にいるとかが決まり、サブルーチンはその状態に従わなければなりません。つまり、エレベータそのものの動きを使う人が知っていなければ、ソフトウェアの世界ではエレベータを使うことができないのです。また、既存のサブルーチンを寄せ集めて何かをするということもできず、何かをしたいときは新しくサブルーチンを作って初期化して使わなければならないのです。
果たしてこのやり方は本当に便利なのでしょうか。ソフトウェアの世界も大きくなってくると、プログラムを細部まで把握して管理することが困難になってきます。もし各サブルーチンが、現実の世界の中のエレベータや電車のように、自分自身を管理しているオブジェクトであったらどうでしょうか。
この場合には、各オブジェクトの使い方さえ把握していれば、それらの動き(規定されたルール)について細かく知らなくても各オブジェクトを使うことができます。ソフトウェアの世界でもオブジェクトが使えれば私たちの日常生活のようにシステムが構築でき、私たちにとっては非常に使いやすいシステムにすることができるのです。
人間の知的能力にはさまざまな側面があります。新しいものを作り出す創造的な能力、機械には不得手な言語理解やパターン認識の能力、数学などで要求される論理思考能力などさまざまです。日常生活の中では、私たちの思考や発言はいつも論理的であるとは言えません。むしろ情緒的で概念的でかなりあいまいなものです。実は論理的思考を長く続けることはかなり骨の折れることと言えるのです。
いままでのプログラミング言語はコンピュータの動作ルールに依存した特殊ともいえる論理一辺倒のものが大部分で、プログラムを組む際にも特殊な論理的思考能力が要求されています。人間がプログラムを組むときにエラーが発生し、デバッグが余儀なくされるのは、人間がこのような論理的思考を長く持続していくことを得手としていないことに起因しているといえるでしょう。
では、ソフトウェアの世界をもっと、人間の側に近づけることはできないでしょうか。コンピュータに何かをさせようとするとき、従来のプログラミング言語では、コンピュータの動きにそって個別の事象を記述しなければなりませんでした。つまり、先ほどの例を用いれば、エレベータに乗りたいときエレベータの設計・製造から起動方法に関する記述まですべてを行わなければ、エレベータに乗ることはできませんでした。
しかし、本来の目的はただエレベータに乗りたいわけですから、私たちの世界のように、エレベータに関する設計や製造などは考えずに、誰かが作っておいてくれているものを利用すればそれを呼ぶだけでエレベータに乗ることはできるはずです。また、こうしてエレベータに関して記述されたものは他のところでも利用できるのです。
このようなソフトウェアの世界が実現できたらどんなに素晴らしいことでしょうか。実はこれがオブジェクト指向が登場した理由の1つといえるのです。
では、もう少し具体的にソフトウェアの世界での「オブジェクト」はどのような存在であればよいのかを考えてみることにしましょう。それは、端的に言うならば、データの部分と手続きの部分が切り離されずにひとつになっているものと言えます。
実は現実の世界ではこの方がずっと自然な考え方です。これだけではわかりにくいので先程の例で考えてみましょう。
エレベータの場合、エレベータの箱(人が乗る部分)や各フロア、各フロアにあるエレベータの扉がデータとなり、モーターを回すとか、扉を開閉するという操作が手続きになります。データと手続きが一緒になっていると自己管理能力が出てきます。ソフトウェアについても、もっと人間に近いレベルで考えたほうが便利です。そのアプローチのひとつがオブジェクト指向なのです。これは、いつも人間が物を見ている見方と適合するものなのです。
オブジェクトという形にしておけば、ソフトウェアのモジュラリティが非常に良くなります。このモジュラリティが良いと言うことは、たとえて言うと、1つのシステムが積み木のように1つ1つを組み合わせてできているということです。モジュラリティが良くなると、メンテナンスがしやすくなります。古くなって調子の悪い積み木は、その外形が同じであれば、取り換えることができます。
このことはソフトウェアを財産化する道につながるのです。なぜなら、誰かが作った積み木をそのまま、他の人が使ってシステムを作り上げることができるからです。最高の品質を持つ積み木は、いろいろな人がシステムを作るときに利用することになります。これが財産になるのです。ここでは、このことについて、もう少し詳しく見ていきましょう。
これまでのプログラミング言語では、あるシステムを記述していく場合、いわば何もない状態から始めなければなりませんでした。プログラムを作るたびに、同じような目的のサブルーチンを作らなければならなかったのです。
しかし、オブジェクト指向型言語では、それまでにプログラムされたことがすべて蓄積として残されます。オブジェクトというデータ構造(データ+手続き)のために、部品として何度でも利用できるのです。
つまり、オブジェクト指向型言語におけるプログラミングでは、それまで培われてきたプログラムを部品として付け加えたり取り除いたり、また改良を加えたりすることが自由にできるのです。
この部品は、ハードウェアの部品と違って、部品と部品の結合がロジカルなものであるので、1つの部品が同時にいくつもの部品と結合することができるわけです。部品化が可能であると以下のようにいくつもの利点が生まれます。
よいソフトウェアは長く使われます。しかし、長く使っている間には手直しが必要になります。手直しの仕方もこれまでのやり方は、パッチワーク的なやり方で、つぎあてをしていました。
しかし、どんなによいソフトウェアでも、このような直し方をしているとしだいに複雑になってしまい、最後には手直しをするくらいなら新たにプログラム全体を作り上げるほうが楽である、というようなことになってしまいます。そんなことをしていては効率が悪いので、もっと本質的に異なるプログラムを作っておくことが必要なわけです。
たとえばデータベースの場合、最初にデータのフォーマットを決めてしまったとします。そのデータ構造にアクセスするような仕組みを今までのソフトウェア・システムで書いたとすると、そのカラム数を変えたくなったときに、その部分を変更すると、そのデータをアクセスしているソフトウェアをすべて書き換えなければならなくなりました。しかし、部品化しておけば、その部分だけを取り換えるだけでよいのです。
それでは、このような部品化はBASICやFORTRANのような手続き型言語ではできないのでしょうか。ソフトウェアを大きな部品から小さな部品へと順次細分化しながら作ってゆくトップダウン設計の手法を使えば、部品化はできます。オブジェクト指向ではなくてもモジュール化しておけば、上のルーチンとは関係なしに必要な部分だけ変えればよいようにできるのです。
しかし、手続き型言語には問題があります。では、どのような問題があるのでしょうか。
手続き型言語でソフトウェア・システムを作るやり方としては、モジュール化するやり方とモジュール化しないやり方の2通りがあります。モジュール化すると速度が遅くなるので、ある部分はモジュール化しないでおきましょうということもできます。そして、一度このようにモジュール化されていない部分がソフトウェア・システムの中に組み込まれてしまうと、あるサブルーチンが勝手気ままにあるデータを使用するという現象を起こし、そのシステムは全体としてみた場合、モジュール構造を持つことについての保証がまったくなくなってしまうのです。
ハードウェアの能力が低く、ソフトウェア・システムの処理速度が問題にされた時代には、モジュール化されずに作られたものが多かったようです。考えようによっては、手続き型言語には柔軟性があるということにもなりますが、実はこれがシステムに悪影響を与えることがあるのです。
オブジェクト指向言語の場合はどうでしょう。オブジェクト指向型言語では、必ずモジュール化しないとプログラムが書けないのです。そこがオブジェクト指向型言語と手続き型言語の大きな違いです。
もちろん、質の高いプログラマーはどのような言語を使用しても、ソフトウェアをモジュール化して、後の変更に対して強いシステムを書くでしょうが、入門したばかりのプログラマーではそういう書き方が必ずしもできるわけではありません。ここに落とし穴があるわけです。たとえば、作業を分担してプログラムを作った場合、ある部分はモジュール化されているのに、別の部分はモジュール化されていないということが起こります。
オブジェクト指向型言語では、モジュール化しないとプログラムが書けないということが、メンテナンスのしやすさを保証しているのです。メンテナンスしやすいということは、ソフトウェア・システムの作り方にも大きな変革をもたらしました。これが次に述べるプロトタイピングです。
プロトタイピングという手法により、これまでの要求仕様定義のように固定的な方法から、もっとダイナミックに仕様を変更していける道が開けたのです。そして、これを実現したのがオブジェクト指向型言語なのです。
「ソフトウェアについても、もっと人間に近いレベルで考えたほうが便利です。」と述べました。
これは実装(製造)だけにとどまらず、前工程である分析/設計においても同じです。ソフトウェアの開発とは、現実世界をモデル化し、さらにそのモデルを計算機世界にマッピングする作業といえます。
現実世界をモデル化する(何を作るかを定義する)作業がすなわち分析、モデルの計算機世界へのマッピング(どう作るかを決定する)作業が設計および製造です。であれば、現実世界から計算機世界までの一連の変換作業を「人間が物を理解するやり方」に近い、オブジェクト指向で通すのが自然です。
従来の設計手法では、機能モジュールに分割し、そしてそのそれぞれをブレークダウンしていました。この手法は機能を骨組みとしたやり方です。作成された設計書あるいはコードは機能を柱とした構造を持っています。ソフトウェアの変更や改造はほとんどの場合機能の変更/改造ですから、場合によってはソフトウェアの構造を根幹から揺るがすことになりまねません。
オブジェクト指向分析/設計では、そのシステムを構成する「もの」とその関連が骨組みとなります。機能的な変更/改造が「もの」そのもの、あるいは「もの」の間にある関連を大きく揺さぶることはまれです。オブジェクト指向によるソフトウェア開発が変更/改造に強いといわれるのは、機能を骨組としていないからです。
ソフトウェア開発の前工程、すなわち分析や設計におけるオブジェクト指向はどんな意味を持っているのでしょうか。
もちろん実装の過程でオブジェクト指向型言語を使用する場合においては、前段階からオブジェクト指向に基づいた分析/設計を行っておけばアイデアと実装とのギャップを小さくできる、言い換えれば「アイデアがそのままコードになる」境地に近づくことができるのはいうまでもないことですが、従来の手続き型言語で実装するにしてもオブジェクト指向分析/設計のメリットはあると考えます。
それは、オブジェクト指向型言語が実装面での再利用性を高めると同様に、オブジェクト指向分析/設計が分析/設計フェーズでの再利用に有効なのです。
たとえば「時計」を設計したとしましょう。時計には「時刻合せ」と「時を刻む」という大きな機能があります。時計の満たすべき基本的な条件と機能とをオブジェクト指向分析/設計できちんと定義しておけば、その後「タイムレコーダ」を設計する際に、時計の設計書が再利用できます。すなわち、「タイムレコーダは時計の一種である。基本的な条件/機能は時計の設計書を参照のこと。タイムレコーダを実現するにあたり、時計とは異なる項目、および追加すべき項目を以下に述べる」などと設計書の冒頭に書き、時計との差分/追加分のみを記述すればタイムレコーダ設計書のできあがりとなるならなんて楽でしょう。
いわば設計書のライブラリ化が可能となります。
オブジェクト指向と言う言葉がパソコン世界のいたる所に浸透してきておりその概念を知っていないと置いていかれると言う状況になってきました。
多くの資料がありプログラミング上での勉強の資料に事欠きませんがプログラミング上での資料ですからパソコンの世界でのオブジェクトとなると対象が広いだけに曖昧模糊としてつかみどころのないものになっています。
特定の言語に依存してしまう「オブジェクト指向言語」の資料ではその言語自体をマスターしていなくては理解できず言語を理解するにはオブジェクト指向の概念を理解していなければならないというパラドックスで初心者にはお手上げ状態になっています。
さて、プログラミング用語であると思われるオブジェクト指向をアプリケーションを使う側で知るべきかという点でオブジェクト指向の何たるかを知っているのと知らないのとでは天国と地獄の差が出てきます。
ですのでこの原稿では本来のプログラミングと言う観点からはピントのずれている箇所もありますし、オブジェクト指向を取り入れているアプリケーション、言語は列挙するだけで膨大な数になりその中で使用される用語も大同小異と言ったものから小同大異と言えるものまでありますのでこの原稿では広く使用されている用法を表記解説しますことをまずお断りします。
そしてオブジェクト指向の概念を解説するという大それた考えはまったくなく、おじさんたちへの理解の一歩を踏み出すためのきっかけとなれば幸いとの考えで書き連ねますからこの原稿の後それなりの本格的オブジェクト指向の書籍を読む事をお薦めいたします。
ではさっそくオブジェクト指向の勉強における最低限の知識習得にはいります。
まずオブジェクト指向全体に触れなくてはなりません、結論から言うと「オブジェクトを中心にする考え方」で今までの操作手順を中心にする考え方は手続き指向と言います。
オブジェクトと言う意味は「(知覚できる)物,又は物体」と言うことですが、使用方法から言うと広義の意味と狭義とがあります。
このことをわかり易く解説しますと
ディスプレイの画面上にある広さを持つ枠(土台)がありその中にボタンが一つ配置されボタンをマウスカーソルなどで押すと簡単なメッセージを表示すると言う簡単なプログラムがあるとします。
この例でボタンが表示されている土台部分をフォームと言います、そしてフォームもボタンもオブジェクトで、「ボタンが」押されるなどの行為に反応するプログラムがボタンの部分に関連して書き込まれていない限り「単にボタンを表示する」だけのものです(ボタンなどのパーツと言って良いものを狭義の意味でのオブジェクトと言うことがおわかりだと思います)
ボタンが押された時に実行されるものをメソッドと言いますがプログラムにおける「手続き」と言う意味です、このメソッドがボタンと言うオブジェクトに書き加えられことでプログラムが完成されます。
Visual BasicCやExcelなどを学んでいる方には周知のことですが、これらの言語やアプリケーションでは基本になるフォームもボタンも最初から用意されていますのでフォーム上にボタンを配置してボタンを押したら表示する文章をボタンに書き加えるだけで簡単にプログラムが完成します。
ぐだくだと頭からフォームやボタンの形態の記述はいりません、なぜこんな簡単なことでプログラムが完成するのでしょうか、それはオブジェクト(各パーツ)が全て独立しているからにほかなりません、「知覚される物」がオブジェクト指向の基本概念です。
先の「物又は物体」を広義でオブジェクトと言うのがおわかりと思います。
独立したオブジェクトがただ単に独立して動作するだけではなんのメリットもありませんし単純作業しかこなせないものでしかありません。
独立しつつも複雑な連携を実行するが、それをユーザーに感じさせない。これが求められているのです。
そのための考えが
という3つの概念です。
カプセル化とは「データとルーチンが一体化されている」ことを指します。プログラム言語ではレコード型(C言語などでは構造体となります)の定義は「異なったデータをひとかたまりとして扱う」と言う点でした、この異なったデータと言う部分を「データの代わりにメソッドや関数をも記述できる」としていただければ理解できると思います。
データとメソッドが単に一緒になっていればよいのではなくデータに対する操作方法がすべて記述されていなくてはなりません、ということはデータはメソッドで保護されメソッドを通してのみ操作できるということが必要になります。
データとメソッドを一つのものとして扱いさらにはデータにアクセスするにはデータと一体化されているメソッドを通してしか接することができません、これによってデータの内容を知らなくてもメソッドの使い方さえ分かればデータの操作ができます。
データの安全性が高まるのです、実にシンプルな考え方で、これ以上何物でもありません。
カプセル化と言う言葉に惑わされてはいけません基本理念は「データの保護」と言うことです、データの保護のためにメソッドが一緒にされ、さらに保護のためにメソッドを通さなければならないのです。
カプセル化の次は「継承」です、継承とは読んで字のごとく「継承=引き継ぐ」と言うことになります、なにがなにを引き継ぐかと言いますと今あるオブジェクトのデータとメソッドを別のオブジェクトが引き継ぎオブジェクト間で上位、下位と言う関係を作ることを言います。
元になるクラスを「BaseClass」と言いそこから派生したクラスを「DerivedClass」と言います。
ここで言うクラスはプログラム言語によって若干の違いがあります、カプセル化された一単位のデータとメソッドを指す考え方、そしていくつかの似通ったオブジェクトをまとめてクラスという表現をする考え方で通常こちらの考え方を「クラス」として使います。
似通ったオブジェクトをまとめて表現することをオブジェクト指向用語風に書くと
「機能や性質に共通点があるオブジェクトの総称」となります。
例えとして「乗り物−自動車−乗用車−『の中の大衆車クラス』にはサニー、カローラ、シビック等がある」と言う言い回しでこの場合のクラスと言う表現がわかると思います。
そしてこのクラスは階層構造を作る事が多く、オブジェクト指向の参考書などでは「系図」などとして取り上げられているますので簡単に理解できると思います。
さて継承は上位の特徴と言えるものを下位が引継ぐと言うことですが逆に言うと既にあるクラスを利用して新しいクラスを作ることができると言う事になります。
新たなクラスを作る場合、既に記述してあるクラスの機能を一々新たなクラス内で記述しなおすことは非能率でしかありません。
既にある機能を継承できれば新しい機能だけを継ぎ足すだけでいとも簡単に新たなクラスを作ることができることになります。
通常継承をインヘリタンスと表記しますが、この言葉自体はなんら意味を持ちません、単に継承を読み換えているだけと理解して下さい、ただし言葉上では単なる継承ですが機能上、自身のクラスに存在しないメソッドの実行を求められた時、上位クラスに定義されているメソッドの実行で代用する事を含んでいることに注意して下さい。
最初から設計をしっかりしておけばインヘリタンスを無理して取り入れる事もないのではと言う疑問も出てきますがインヘリタンスはオブジェクト指向における重要な要素となっていると言うことはそれなりの理由があっての事です。
ポットにミルと言う機能付け加えるだけでいとも簡単にミル付きコーヒーポットができます(物としては簡単ではないでしょうがが)ビデオカメラを継承してテレビと言う機能を付け加えれば新たな機種が出来上がります。
これらは物としての考えですがこれをプログラムなどに導入したと考えてください、オブジェクトで言うインヘリタンスの意味するところがおぼろげながら見えてきたはずです。
さらに便利なことに上位のオブジェクトで定義された機能を下位のオブジェクトで書き直すことができます、全て継承することもなく又継承したくない部分を書き換えてしまうことが許されておりそれをオーバーライド(override)と呼びます。
ここで継承=インヘリタンスをまとめますと
ということになります。
1と2は表裏一体の関係と言えます、新しいデータやメソッドを追加せずただ継承だけではなんら必要性が認められないからです。
3のオーバーライドもこの後で軽く触れる「多態性」では意味を失いますが現在は素直に受け取っていただいて結構です。
この便利なオブジェクト指向の特徴であるインヘリタンスも上位のクラスを一つしかもてないシングルインヘリタンスですのでマルチインヘリタンスに比べると不便と言えば不便です。
シングルインヘリタンスと聞きますと上位から下位まで一本の線でしかつながっていない直線構造の様に考えますが、シングルと言っても直系の上位クラスが一つと言う事で直系の下位クラスは任意の個数を持つことができます。
しかしインスタンスのラインが1本と考えるとわかりやすいので系図をもって説明していきます。
車 | | ←トランクリッドを取る ↓ ピックアップ・トラック | | ←荷台にルーフをつける ↓ バン・ワゴン | | ←四輪駆動にする ↓ RV | | ←ボディ鋼板を厚くする ↓ 軽装甲車
と、強引に現実の製品系列を無視していますがその意図は理解していただけると思います、すべての製品が自動車と言う機能を継承していますし途中で追加された機能も下位の製品では継承しています。
先のオーバーライドをこの図式で説明しますと「荷台にルーフをつける」で金属製ではなくキャンバス地にすると言った感じで機能を付け替える(オーバーライド)ことで感じがつかめるはずです。
インヘリタンスにおける直系上位は一つだけですが直系下位は任意の個数定義できますのでこの自動車系図を当てはめますと
上位 車 ┌───┤ ルーフを取る→| |←トランクリッドを取る ↓ ↓ オープンカー ピックアップ・トラック │ │ キャンバス→| |←荷台にルーフをつける ルーフ | ↓ | バン・ワゴン 四輪駆動→| | ↓ |←四輪駆動にする ジープ ↓ | RV スクリューを→| |←ボディ鋼板を厚くする つける | ├−−−┐ ↓ | |←砲をつける 水陸両用車 ↓ |←キャタピラに変える 軽装甲車 | | ↓ ジープは登録商標です 戦車
と言ったとこでしょうか、各名称は適当につけていることと下位の枝分かれが2つだけですが単なる紙面の都合だけですとお断りしておきます、しかし下へいくほど軍用の形態になっていくと言うのは不思議ですね車の機能から言ってスポーツカーと軍用車が両極の形と言う事なんでしょうか。
この例でも全ての下位は車と言う機能をインヘリタンスしていますことがおわかりと思います。しかしこの個々の製品は外見上見分けがつきますがオブジェクトの概念から言えばその以前の機能は表面的にまったくわからないと言うことに注意してください。(もっとも最近の車は外見だけでは区別がつかないものが多くなっていますけど)
さらに多重継承としての概念が有ります、いままではすべて上からの継承は一つだけてしたが、例えばラジオカセットは別の観点から見るとラジオにカセットテープという二つの機能が継承され合体してできたものでラジオにテープレコーダーが付属したのではなく両方の機能が融和してできたものとみなす事ができます。
この例からも分かるとおり複数のクラスからデータやメソッド等を継承していることを「多重継承」と言いますが単なる言葉としておぼえておいてくださるだけでそれ以上は初級入門者には必要ないでしょう。
なぜなら多重継承を系図化すると
No1−−−−−−− | | | | No2 No3−−−−−−− | | | | | | No4 No5 No6 | | | | −−−−−−−No7ーーーーーーNo8 | |
と言う感じになります、この系図でNo8に対する直接の親(Base)はNo7とNo6になり両方のオブジェクトやメソッドなどがぶつかる場合がでてきます。
この様な場合に「なにをさせようとしているのか」「両方をさせようとしているのか」「両方の時どちらを優先させるか」などと言う問題が出てきます。
これらは非常に高度のテクニックと細心の注意が必要ですから初級入門者には難題となりますので言葉としてとどのようなものかだけを覚えておけばよいでしょう。
オブジェクト指向プログラムにおける最大の難所である多態性ですが、この拙文はオブジェクト指向プログラムの勉強ではなく「オブジェクト全体」の入門解説ですから深くは触れませんがある程度の事を知っていても損はないので軽く触れます。
多態性というまったくもって意味の通らない日本語のもとはポリモルフィズムと言う言葉でポリモルフィズムとはPolymorphismと書きギリシャ語のpoly=多くの、とplymorphism=意味の要素、が合わさってできた言葉で「同名異型」という意味です。
そして導きだされることは「異なる型に対して同名の操作を行う事ができる」となり逆の書き方をすると「異なる機能を持つ物が同じ名前をもつことができる」と言う事になります。
先のインヘリタンスでもメソッドで同じ名前を定義するオーバーライドということができましたしオーバーライドと同一の様ですがまったく違っております。
さきでのオーバーライドでは同じ名前を使うことにより上位クラスの機能は下位クラスの機能に書き換えられてしまいます、しかしポリモルフィズムにおいては同一の名前をつけても機能が書き換えられません。
「同一の名前でも機能が違う」
と言うことです
このことがどんなメリットがあるかと言うことは初級程度のプログラムではなかなか難しいところがありますが、構造化プログラムの特質である、ルーチンの部品化を突き詰めていく結果に出てきたのでは思われます。
すなわちプログラム上では必要となる知識ではあるが、単純にオブジェクト指向の入門と言う立場では特に必要がある言葉だろうか、と言う結論がでてしまう身も蓋もないことになります。
初級におけるポリモルフィズムの意味は
上位のクラスから下位のクラスまで一つの動作に同じ名前をつけてそれぞれのクラス内では別々の動作をさせる、そしてポリモルフィズムを実現するのがバーチャルメソッドで、仮想的に記述することによりそれらのメソッドの参照先をコンパイル時に決めず実行時に決めるとなります。
簡潔に言えばポリモルフィズムが「戦略」でバーチャルメソッドが「戦術」と言えます。