目次
ポインタ指向スタイルでプログラムを作成する際、欠くことができないのがポインタを格納できるコンテナです。
NULLでないポインタはその指し先の領域を持っているため、コンテナに格納する場合、 その領域の管理をどうするかが問題になります。
C++の標準テンプレートライブラリ(STL)は値指向を採用し、
ポインタを格納する場合はその管理を行わないません。
この場合、ユーザーが指し先の領域を必要に応じてコピーや破棄しなければなりませんが、
コンテナのコピー・コンストラクタなどで暗黙のうちにコピーが行われるケースにも対応が必要で、
開発も保守も非常に難しいコードになります。
そのため一般的には、STLコンテナにポインタを格納する場合、別に領域のメモリ管理を行うか、
xoops::shared_ptrのようなクラス化したポインタをわざわざ使用することになります。
これに対して、道具箱コンテナでは、ポインタの指し先を管理する3つのポリシーを導入することで対処します。
開発者はこの3つのポリシーを使い分けることになりますが、
これは以下に解説するように難しいことではありません。
それぞれの使用方法をスタイルとして身につけることで、効率よく安全なコードを開発することができます。
道具箱コンテナでは、<omt/pcontainer.h>ヘッダーで、 コンテナを宣言する際にテンプレート引数として使用される3つのメモリ管理ポリシーを定義しています。
Note:
コンテナ自体をコピーする場合と、copyポリシーのコンテナにポインタで指されたデータを格納する場合、
データのコピーには後述するdup関数が使用されます。
このdup関数は、デフォルトでは格納する領域がクラスの場合コピー・コンストラクターを、
文字列([const] char*/wchar_t*)の場合はNull値でなければ同じ長さの領域の確保とコピーを呼び出すように実装されています。
もちろんdup関数をオーバーライドして、独自の処理に対応することも可能です。
また、提供されているstoreポリシーとcopyポリシーは、領域の確保と破棄にnew/deleteオペレータ (文字列の場合はnew []/delete []オペレータ)を使用しますが、 たとえば共用メモリのようにヒープ以外の記憶域を使う場合には、 メモリ管理ポリシー・クラスを作成することで対応することが可能です。
ポインタ指向のプログラミングでは、メモリ領域を確保した場合、 その所有者をつねに明確にしておくことが重要です。 この点を念頭において、そのそれぞれのポリシーがどのようなケースで使用されるかを見ていきましょう。
storeポリシーでは、コンテナが格納されたポインタの指し先の所有者になります。
そのため、new演算子やファクトリ・メソッドなどから返る確保したデータを指すポインタを使ってデータを格納し、
その管理を任せてしまうことができます。
このstoreポリシーのコンテナを適切なブロック内に宣言して、処理中に発生するデータを一括して管理する方法は、
もっとも基本的なポインタ指向スタイルということができます。
典型的なコーディング例でみていくことにしましょう。
typedef plist<E*, store_policy<E*> > elist;
void setElements( elist& l, ... )
{
E* p = new E( ... );
...
l.enq( p );
}
void procElements( elist& l, ... )
{
elist::itor i( l );
for ( i.init(); i.cont(); i.next()) {
...
}
}
void someProc()
{
elist ls;
...
while ( ... ) {
...
setElements( ls, ... );
}
...
procElements( ls, ... );
// elements will be deleted on destructing 'ls'
}
Note:
以下、コーディング例ではnamespaceを省略しています。
実際のコードでは、以下のnamespace利用宣言を行う(プログラム・コードの場合)か、
道具箱内のクラス、関数などに、omt::で修飾する(ヘッダ・ファイルの場合)ことが必要になります。
using namespace omt;
この例では、someProcの中で、store_policyのリスト・コンテナ ls を宣言しています。
これは、someProcを終了する時に破棄されますが、この時、格納されたポインタが指すデータも
一緒にdelete(文字列の場合はdelete [])されます。
こうしたコードをスタイルとして身につければ、コンテナの宣言の際のポリシーに気をつけるだけで、
あとはデータのメモリ領域の管理を気にせずコーディングできるわけです。
ファクトリ・メソッドなどのように、確保したデータ領域を指すポインタを返す場合も、
このスタイルを使用することができます。しかしこの場合は一歩進めて、
ファクトリ・クラスのメンバーにstore_policyのコンテナを置き、
ファクトリー・メソッドが返すポインタを管理するようにすれば、
ファクトリの破棄とともに作成したデータを破棄することができます。
Note:
ファクトリー・クラスの変数はメモリ管理者としての役割を持つため、
通常は、作成されるデータの利用にあわせたブロック内に変数を宣言して使用します。
このとき、ファクトリー・メソッドはstaticでないメンバ関数になります
こうすれば、ファクトリ・メソッドのユーザーである開発者が領域管理の作業から開放されます。
class E {
...
public:
virtual ~E() { }
...
};
class D1 : public E { ... };
class D2 : public E { ... };
...
class eFactory
{
plist<E*, store_policy<E*> >l; m_pool;
public:
E* produce( ... )
{
E* p = new D1( ... );
...
m_pool.enq( p );
return p;
}
E* produce( .... ) { .... }
E* produce( ..... ) { ..... }
...
};
void someProc()
{
eFactory efactory;
...
E* p = efactory.produce( ... );
...
// elements will be deleted on destructing 'efactory'
}
Note:
道具箱コンテナのコンテナは、ポインタの指し先をメモリ管理ポリシーによって適切に処理できるため、ポインタ指向のプログラミング・スタイルで自然に使用することができます。
上のコードの m_pool のように、storeポリシーのコンテナに仮想の親クラスのポインタとして格納して、多相的(Polymorphic)な処理を行い、コンテナの破棄によって自動的に格納したポインタの指し先を破棄することができます。
まとめると、storeポリシーのコンテナの典型的な使い方は、
copyポリシーでは、ポインタの格納時に指し先もコピーします。そのため、referポリシーと同様に、
格納元のポインタが指し先の領域の所有者であるか否かに関係せず使用できます。
特にreferポリシーにあった、コンテナの破棄前に指し先を破棄してはいけないという制約がないという利点があります。
また、C言語インターフェースをもつライブラリでは、バッファに結果の値を返す関数がよく用いられます。
こうした関数を繰り返し呼んで、バッファ内のデータをコンテナに格納していきたい場合には、copyポリシーが便利です。
typedef plist<char*, copy_policy<char*> > szlist;
void storeAllResult( szlist& ls, ... )
{
while ( ... ) {
char buf[ BUFSIZE ];
...
snprintf( buf, BUFSIZE, "format-string", ... );
ls.enq( buf ); // string duplicated and stored into 'ls'
}
}
copyポリシーのコンテナの典型的な使い方は、
referポリシーは格納されるポインタの指し先を管理しません。
言い換えると、referポリシーのコンテナは格納したポインタの指し先領域の所有者になることはなく、
指し先領域の所有者も変更されません。
referポリシーを使用するのは、
typedef plist<int> intlist; // refer policy ( default for 2nd argument of toolkit container )
intlist l;
intlist::itor i( l );
l.enq( 1 );
l.enq( 2 );
l.enq( 3 );
for ( i.init(); i.cont(); i.next()) { printf( "%d\n", i.get()); }
Note:
referポリシーの動作はSTLコンテナと同じなので、ポインタの指し先の所有者であって、
格納後も引き続き所有者として破棄の面倒をみる場合に使用することも可能ですが、
コンテナの複製にデータの複製が対応しないなど開発・保守とも難しくなるので、おすすめはできません。
また、ポインタでなくクラスのデータをそのまま格納することも可能ですが、
STLコンテナのケースと同じく不要な暗黙のコピーが発生するなどの問題があるので、避けるべきです。
C++標準テンプレートライブラリでは、イテレータをポインタの拡張として利用できるよう工夫しています。
しかしコンテナによってはポインタのアナロジーが適用しにくいケースもあり、
ポインタとの互換性を保つための実装が、不要な複雑さを生み出す原因にもなっていると考えられます。
道具箱コンテナのコンテナは、ポインタのアナロジーにこだわらず、
実装時のコードがわかりやすくなるように、
以下のようなイテレータを用意しています。
なお、STLのイテレータと区別が容易なように、クラス名はiteratorではなく、itorとしています。
イテレータは、宣言時にコンテナを引数に初期化され、
通常
typedef plist<T*, store_policy<T*> > t_list;
t_list l;
...
t_list::itor i( l ); // initialized by container
for ( i.init(); i.cont(); i.next()) { // loop idiom
T* p = i.get(); // retrieve
...
if ( ... ) i.del(); // delete on calling i.next() or ~itor()
}
for ( i.init(); i.cont(); i.next()) { ... }
の形でループ処理を作ります。
ループ処理の中では、取得( i.get()
)、設定( i.set( p )
)、削除( i.del()
)、
などの処理が行えます。
コンテナがconstである場合、繰り返しにはitorのかわりにconst_itorを使用します。 const_itorはconstのコンテナで初期化できますが、ループ内での処理は取得に限られ、設定・削除を行うことはできません。
Note:
storeおよびcopyポリシーのイテレータによる削除処理の場合、コンテナは格納したポインタの所有者であるため、
ポインタの指し先を破棄する必要があります。
もし、この破棄をitor::del()
が呼び出された時点で実行して、
その後アプリケーションでこの領域を使用すると実行時エラーになります。
そのため、削除されたポインタの指し先の破棄は i.next()
の呼び出しまで遅延され、
ループ処理内で削除される指し先の領域を安全に操作することができます。
もし、itor::del() の呼び出し後に break でループを抜ける場合でも、itor の破棄のタイミングで指し先が削除されます。
itorの破棄より前に削除したい場合は、break の前に itor::next() を呼び出せばよいでしょう。
for ( i.init(); i.cont(); i.next()) {
if ( some_condition( i.get())) {
P p = i.del();
// ... able to handle data pointed by p ...
}
}
{
P p = i.del();
...
i.next(); // delete data pointed by p here
break;
}
Note:
イテレータを使用する場合のループの for ( i.init(); i.cont(); i.next()) { ... }
は、いつもこの形で使用されるため
以下のようにマクロ化して foreach( i ) { ... }
のように表現することが可能です。
#define foreach(i) for ((i).init();(i).cont();(i).next())
この表現はマクロ機能のみで出来ているため、
一部のC++ライブラリで行われているような特殊なプリプロセッサーを用いず、
C++の処理系の範囲内で使用することができます。
しかし、C++の制御構造(文法そのもの)を変更してしまうので、
コード自体の可読性(他の人が読むときの読みやすさ)は下がってしまいます。
したがって、ごくプライベートなコードの場合や、プロジェクト内のコーディングルールとして特に定めた場合を除いて、
多用すべきではありません。
ここでは、3つの道具箱コンテナを紹介します。
コンテナの実装は、用途に応じて多種多様なコンテナを用意する方法もあります。
しかし、個々のケースに最適なコンテナを選ぶために、
習得に時間がかかったり実装が煩雑になることは望ましくありません。
そこで道具コンテナでは、応用の利く少数のコンテナを用意する方針をとっています。
なお、解説で多用する「ポインタの指し先の領域に格納されるデータ」という表現は冗長なので、
以下ではこれを「データ」と呼ぶことにします。
plist | #include <omt/plist.h> |
データのリスト |
phash | #include <omt/phash.h> |
ハッシュ関数を使用したキーとデータの対応づけ |
ptree | #include <omt/ptree.h> |
2分探索木を使用した、順序づけをもつキーとデータの対応づけ |
リストを作るにはplistコンテナを使用します。
テンプレートの第2引数にとる Policy を省略すると、refer_policy<P> が使用されます。
plistはシングルリンク・リストとして実装されていますが、終端へのポインタをもつことで、
スタック(後入先出)としても、キュー(先入先出)としても使用できるように工夫されています。
これらの処理は要素数には影響されません。(オーダーO(0)の処理)
storeポリシーのコンテナに push() または enq() を行った場合、データの所有はコンテナ側に移るため、
渡せるポインタはプログラム内で new で確保したものに限られることに注意してください。
ブロック中やstaticな変数として宣言した自動的に破棄されるデータへのポインタを渡したり、
push() や enq() の後にプログラム側で delete を行う場合には、copyポリシーのコンテナを使います。
// stack operations
void push( const P& p ); // O(0). push to the stack
P pop(); // O(0). pop from the stack
// queue operations
void enq( const P& p ); // O(0). enque to the queue
P deq(); // O(0). deque from the queue
また、コンテナがstoreおよびcopyポリシーの場合、pop() および deq() を行うと、
戻り値のポインターが指すデータはコンテナの所有を離れるので、
プログラム側で delete することが必要な点に注意してください。
その他リストに対する以下の操作がメンバ関数として提供されます。
bool is_empty() const; // O(0)
P getfirst() const; // O(0)
P getlast() const; // O(0)
P setfirst( const P& p ); // O(0). returns old pointer if refer_policy, or NULL
P setlast( const P& p ); // O(0). returns old pointer if refer_policy, or NULL
P nth( size_t n ) const; // O(N). if n is out of length, returns NULL
size_t length() const; // O(N)
void clear(); // O(N). clear all elements and pointed data if needed.
plist& reverse() // O(N). returns *this;
void swap( plist& pl ); // O(0).
template<typename Q> void append( const plist<P,Q>& r );
template<typename Q> void append( const plist<P,Q>* p );
// O(N). enque all elements of the argument list
また、plist::itor
および plist::const_itor
がイテレータとして提供されます。
for ( i.init(); i.cont(); i.next())
ループ中で、
以下のメンバー関数を使ってリストの要素を操作できます。
const P const_itor::get();
P itor::get();
P itor::set( const P& p ); // returns old pointer if refer_policy, or NULL
void itor::ins( const P& p ); // insert before current iterator position
P itor::del(); // delay destory pointed area until itor::next() or ~itor() called
コーディング例は、「繰り返し処理」の節にあるこちらをごらんください。
plistを含め、道具箱コンテナはポインタ指向プログラミングの考え方にもとづいて、コピー・コンストラクタと
コピー代入演算子を提供していません。コンテナを複製する場合は、dup関数を明示的に使用します。
これによって、コンテナ全体(storeやcopyの場合は要素も含めて)の暗黙のコピーによる問題を防いでいます。
Note: <omt/plist.h>には、dup関数を実現するために、dup_fnが定義されています。
template<>
template<typename P, typename Policy>
struct dup_fn<plist<P,Policy>*>;
また、テンプレート関数として、2項等価演算子とswapが提供されています。
template<typename P, typename Policy>
bool operator== ( const plist<P,Policy>& a, const plist<P,Policy>& b );
template<typename P, typename Policy>
void swap( plist<P,Policy>& a, plist<P,Policy>& b );
ハッシュ・テーブルにキーを管理して、キーに対応するデータを格納するコンテナです。
データをキーから高速に(要素数に依存しない速度で)取り出すことができるため、
リストについで頻繁に使用されます。
テンプレートの第2引数以降は省略可能で、それぞれ以下のようにデフォルトが定義されています。
テンプレート引数 デフォルト値 P データのポインタの型 - PPolicy データのメモリ管理ポリシー refer_policy<P> Size ハッシュ・テーブルのサイズ 256 K キーの型 const char* KPolicy キーのメモリ管理ポリシー copy_policy<K> HashFn ハッシュ関数 hash_fn<K> EqlFn 等価関数 eql_fn<K>
データのメモリ管理ポリシーを指定しない場合は、他の道具箱コンテナと同様にrefer_policyになります。
ハッシュ・テーブルのサイズは、検索のパフォーマンスを重視する場合には、 格納要素数の3~5倍を目安に2の累乗の整数を指定するとよいでしょう。
ハッシュ・キーはテンプレートによる型の指定(K)だけでなく、キーのメモリ管理ポリシー(KPolicy)、
ハッシュ関数クラス(HashFn)、等価関数クラス(EqlFn)を指定することが可能です。
ハッシュは文字列のキーで使用されることが多いため、キーのメモリ管理ポリシーは
格納時に指定されたキーが保持されるようにcopy_policyがデフォルトになっています。
文字列をキーにする場合は、通常第4引数(K)以降を省略して使用します。
キーが格納するデータのクラスのメンバーの場合など、キーをコンテナで管理する必要がない場合には、
KPolicy=refer_policy<K>として使用するとよいでしょう。
ハッシュ関数クラスと等価関数クラスのデフォルトとなるテンプレートは、 <omt/common.h>に定義されています。
デフォルトのハッシュ関数クラスは、
キーが文字列型( char*, const char*, wchar_t*, const wchar_t* → Note 参照)の場合、
構成する文字コードを5倍しながら加算する単純で高速なハッシュ関数です。
他に、longやvoid*をキーにした場合もデフォルトで対応可能になっています。
また、デフォルトの等価関数クラスは、文字列の場合はstrcmp()/wstrcmp()が0になる場合にtrueを返します。
文字列以外のポインタの場合はポインタ(アドレス)の比較が行われます。
また、基本型など、それ以外の場合は等価演算子(==)が呼び出されるため、
クラスをキーにする場合は等価演算子(2引数の等価演算子関数)を定義しておけば、
等価関数クラスを実装する必要はありません。
Note:
道具箱コンテナでは、実用的な観点から、char*, const char*, wchar_t*, const wchar_t*はNULL文字
終端を持つ文字列を表す型として使用します。これは特に<omt/common.h>に定義されるテンプレート・クラスを
使用する際に、重要になります。文字列でなく、単に文字へのポインタを使用する場合は、
int*などを使用してください。
phashコンテナからのデータの取得
phashコンテナに格納されたデータは、find()とget()の2つのインターフェースで取得することができます。
find()は、参照用にデータへのポインタを取得する(戻値がconstになる)もので、
キーに対応するデータが格納されていなければ 0 が返ります。
const P find( const K& k ) const; // O(0)
phash<P, ...>::dref get( const K& k ) const; // O(0)
これに対してget()はphash<P, ...>::drefクラスを返します。
drefはハッシュ・テーブルに格納された(あるいはこれから格納する)データにアクセスするためのクラスで、
get() の戻り値を r とすると、itorクラスと同じ要領で、データの取得( r.get()
)、
設定( r.set( p )
)、削除( r.del()
)とキーの取得( r.key()
)ができます。
drefを介することは若干煩雑に感じるかもしれませんが、
たとえば以下のように1回の phash<>::get() の呼び出しで、
データのチェックと変更を行うことが可能になり、無駄なハッシュ関数の呼び出しを行わずにすみます。
また、以下に示すようにキャスト演算子の定義があるため、
通常はdrefを介していることを意識せずに(get() が find() と同様にデータへのポインタを返すものとして)
コードを書いてもかまいません。
コードを簡明にするために、dref には2つのキャスト演算子がメンバ関数として定義されています。
bool check( T* p ) { ... }
T* p;
phash<T*> h;
...
if ( phash<T*>::dref r = h.get( ky )) { // <-- (1)
...
if ( check( r )) { // <-- (2)
...
r.set( p ); // set data by phash::dref
}
}
上記のコード(1)で r = h.get( ky )
は、
if文の条件節にあるため dref の boolへのキャスト演算子が使用され、
ハッシュに登録されていることを確認するコードになっています。
また(2)では、check( ) の引数が T* であるため、dref から T* へのキャスト演算子が使用されて、
格納されているデータへのポインタが check( ) に渡されています。
この T* へのキャスト演算子によって、phash<>::get( ) の戻り値を、 そのまま T* 型の変数に代入することができます。
Note:
drefを使用した設定 r.set( p ) は、phashのメンバ関数 set( k, p )の動作と同様に、
r に対応する ky が phashコンテナに存在する場合にはそのデータを p と置き換え、
存在しない場合は p を新たに格納します。
したがって、必ずしも(1)のようにコンテナにkyが存在することを確認した上で、
使用するものではありません。
phashコンテナへのデータの格納
phashコンテナへのデータの格納は、set()、replace()、insert()の3つのインターフェースを使用します。
set()は、phashコンテナにkyが存在するか否かに関わらず dt を確実にコンテナに格納します。
P set( const K& ky, const P& dt ); // O(0)
bool replace( const K& ky, P& dt ); // O(0)
bool insert( const K& ky, const P& dt ); // O(0)
すなわち、ky が存在している場合は対応するデータを dt で置き換え、存在しない場合は新たに dt を格納します。
referポリシーでデータの置き換えが起きた場合は、古いデータへのポインタが戻されます。それ以外の場合戻り値はNULL(P())です。
replace()は、phashコンテナに ky が存在する場合に限って dt を置き換え、true を返します。 referポリシーでデータの置き換えが起きた場合は、dtには古いデータへのポインタが、それ以外の場合はNULL(P())が格納されます。 存在しない場合はコンテナは変更されず false が戻ります。
insert()は、phashコンテナに ky が存在しない場合に限って新たに dt を格納し、true を返します。 すでに存在する場合はコンテナは変更されず false が戻ります。
phashコンテナのデータ取得/設定以外のインターフェース
取得・設定以外に、削除、全格納データのクリア、ハッシュテーブルのサイズ、格納データ数、
他のphashコンテナとの入れ替えのインターフェースが提供されます。
P remove( const K& ky ); // O(0). equivalent with 'get( K ).del()'
void clear(); // O(N). clear all elements
size_t size() const; // O(0). return Size
size_t length() const; // O(N). return number of stored data
void swap( phash<P,PPolicy,Size,K,KPolicy,HashFn,EqlFn>& ph );
// O(0)
phashコンテナのイテレータおよびデータ参照
イテレータとしてitorおよびconst_itorが提供され、以下のメンバーでハッシュに格納されたキーとデータを操作できます。
const P itor::get() const;
const P const_itor::get() const;
const K itor::key() const;
const K const_itor::key() const;
P itor::set( const P& p ); // returns old pointer if refer_policy, or NULL
P itor::del(); // delay destory pointed area until next() or ~itor() called
phash<>::get( ky )の戻りとなるdrefも、以下のメンバーでハッシュに格納されたキーとデータを操作できます。
const P dref::get() const;
const K dref::key() const;
P dref::set( const P& p ); // returns old pointer if refer_policy, or NULL
P dref::del(); // delay destory pointed area until next() or ~itor() called
bool dref::check() const; // check if ky exists in the phash container
operator const P() const; // same as dref::get()
operator bool() const; // same as dref::check()
dref& operator=( const P&& dt ); // same as dref::set() except returns *this
バイナリ・ツリーにキーを管理して、キーに対応するデータを格納するコンテナです。
ハッシュ・テーブルとことなり、格納・検索にO(log N)~O(N)のコストがかかりますが、
イテレータを使用してキーの昇順または降順に処理ができる点に特長があります。
現在、道具箱コンテナにダブル・リンク・リストが定義されていないのは、
Note:
格納・検索を確実にO(log N)にするには、ツリーの深さをバランスさせる必要があります。
一般にアルゴリズムの教科書には、このバランスを格納・削除時に行うツリーが紹介されていますが、
実際のアプリケーションでは、格納がまとめて行われるケースも多く、ptreeではbalance()メンバ関数で
ツリー全体の深さの均一化を行うようにしています。
ptreeにはキーがソートされた状態のデータを連続して格納すると、ツリーのバランスが大きくくずれて、
O(N)の格納・検索コストに近づく(リンク・リストと効率が変わらなくなってしまう)という弱点があります。
そのため、キーの出現順序がランダムでない場合は、
データのある程度まとまった格納後に balance() を実行するようにしてください。
テンプレートの第2引数以降は省略可能で、それぞれ以下のようにデフォルトが定義されています。
テンプレート引数 デフォルト値 P データのポインタの型 - PPolicy データのメモリ管理ポリシー refer_policy<P> K キーの型 const char* KPolicy キーのメモリ管理ポリシー copy_policy<K> CmpFn 比較関数 comp_fn<K>
データのメモリ管理ポリシーを指定しない場合は、他の道具箱コンテナと同様にrefer_policyになります。
バイナリ/ツリーの場合も、キーの型(K)だけでなく、キーのメモリ管理ポリシー(KPolicy)、
比較関数クラス(CmpFn)を指定することが可能です。
キーのおよびキーのメモリ管理ポリシーは、それぞれ const char* と copy_policy がデフォルトになっています。
文字列をキーにする場合は、通常、第3引数(K)以降を省略して使用します。
キーが格納するデータのクラスのメンバーの場合など、キーをコンテナで管理する必要がない場合には、
KPolicy=refer_policy
比較関数は等価関数と似ていますが、strcmp()のように引数の大小を3つの整数値(-1, 0, 1)で表す関数です。 クラスのデフォルトとなるテンプレートは、 <omt/common.h>に定義されています。
ptreeコンテナへのキーの重複がないデータの格納と取得
ptreeは、キーの重複がある場合とない場合でインターフェースを使い分けて使用するように設計されています。
重複がない場合はphashと同様に以下のインターフェースで格納、取得、削除を行うことができます。
set()は、ky が存在している場合は対応するデータを dt で置き換え、存在しない場合は新たに dt を格納します。
referポリシーで値の置き換えが起きた場合は、古い値が戻されます。それ以外の場合戻り値はNULL(P())です。
P set( const K& ky, const P& dt ); // O(log N)~O(N)
bool replace( const K& ky, P& dt ); // O(log N)~O(N)
bool insert( const K& ky, const P& dt ); // O(log N)~O(N)
P find( const K& ky ) const; // O(log N)~O(N)
P remove( const K& ky ); // O(log N)~O(N)
replace()は、ptreeコンテナに ky が存在する場合に限ってデータを dt で置き換え、true を返します。 referポリシーで値の置き換えが起きた場合は、dtには古い値が、それ以外の場合はNULL(P())が格納されます。 存在しない場合はコンテナは変更されず false が戻ります。
insert()は、ptreeコンテナに ky が存在しない場合に限って新たに dt を格納し、true を返します。 すでに存在する場合はコンテナは変更されず false が戻ります。
find()は、参照用にデータを取得するもので、キーに対応するデータが格納されていなければ、NULLが返ります。
remove()は、格納されたデータを削除します。referポリシーで値の削除が起きた場合は、古い値が戻されます。 それ以外の場合戻り値はNULL(P())です。
上記のメンバ関数は、キーが重複するデータを格納しているコンテナに用いることもできますが、 kyに対応するデータが複数格納されている場合は、いずれか1つに対して動作するため、 意図とは異なる処理になることがあります。 そのため、キーの重複するデータを格納する場合は以下のインターフェースとイテレータを使用します。
ptreeコンテナへのキーが重複するデータの格納
set()が ky が存在する場合は置き換えを行うのに対して、put()は ky の存在に関わらず dt を ptreeコンテナに格納します。
この put()によって、キーが重複するデータの格納が可能になります。
void put( const K& ky, const T& dt ); // O(log N)~O(N)
キーが重複するデータの取得や削除の操作は、イテレータを通じて行います。
ptreeが提供するその他のメンバ関数
その他、以下の処理がメンバ関数として提供されます。
void balance(); // O(N). after calling it, retrieve operation will be O(log N)
bool is_empty() const; // O(0)
size_t length() const; // O(log N)~O(N)
void clear(); // O(N). clear all elements and pointed area if needed.
void swap( plist& pl ); // O(0).
ptreeのイテレータ
ptreeでは、格納データがキー順にソートされるため、イテレータによる順次処理が他のコンテナに較べ重要です。
まず、キーの昇順か降順かで、2種類の対称なイテレータ(itor, ritor)が提供されます。
この2つは処理の方向が逆である点を除いて、同様に扱うことができます。
また、イテレータの初期化でキーを指定して検索ができること、
比較メンバ関数によってキーの範囲を指定して処理ができること、などの特長があります。
格納されたすべてのデータを扱うには、リストやハッシュと同様にinit(), cont(), next()でループを作成します。
ptree<P> tr; ptree<P> tr;
ptree<P>::itor fi( tr ); ptree<P>::ritor ri( tr );
... ...
for ( fi.init(); fi.cont(); fi.next()) { for ( ri.init(); ri.cont(); ri.next()) {
// accendant order // discendant order
P p = fi.get(); P p = ri.get();
... ...
} }
イテレータの初期化の際にキーを指定するか、itor::find()/ritor::find()メンバ関数を使用すると、特定のキーの
位置にイテレータを置くことができます。以下のコードはいずれも、ky に対応するデータを含めて3個に順に処理を行います。
const char* ky = "key"; const char* ky = "key";
ptree<P> tr; ptree<P> tr;
// search here ptree<P>::itor fi( tr );
ptree<P>::itor fi( tr, ky ); ini i;
... ...
// search here
for ( int i = 0; for ( i = 0, fi.find( ky );
i < 3 && fi.cont(); i < 3 && fi.cont();
++i, fi.next()) { ++i, fi.next()) {
P p = fi.get(); P p = fi.get();
... ...
} }
上記の例では、kyに対応する要素が格納されていなければ処理は行われませんでした。
これに対してitor::find_ge()/itor::find_gt()(ritor::find_le()/ritor::find_lt())によって、
指定されたキー以上/キーを越える値(ritorの場合はキー以下/キー未満の値)から開始することも可能です。
また、このfind_xx()メンバ関数と、lt()/le()/ge()/gt()メンバ関数を組み合わせると、
以下のように、「"target"を越えるものすべて」「"first"以上"last"未満」のようにキーの範囲で処理を行うことも可能です。
ptree<P> tr; ptree<P> tr;
ptree<P>::itor fi( tr ); ptree<P>::ritor ri( tr );
... ...
for ( fi.find_gt( "target" ); for ( ri.init();
fi.cont()); ri.gt( "target" );
fi.next()) { ri.next()) {
// accendant order // discendant order
P p = fi.get(); P p = ri.get();
... ...
} }
for ( fi.find_ge( "first" ); for ( ri.find_lt( "last" );
fi.lt( "last" ); ri.ge( "first" );
fi.next()) { ri.next()) {
// accendant order // discendant order
P p = fi.get(); P p = ri.get();
... ...
} }
このように、イテレータでキーの範囲によってデータを扱う方法は、
キーに対して複数のデータを含むか否かに関係なく使用できる点で優れています。
また、キーの重複使用の場合、find(),eq()メンバ関数をinit(),cont()の代わりに用いて、
特定のキーに対応するデータをすべて処理することができます。
ptree<P> tr; ptree<P> tr;
ptree<P>::itor fi( tr ); ptree<P>::ritor ri( tr );
... ...
for ( fi.find( "dup-key" ); for ( ri.find( "dup-key" );
fi.eq( "dup-key" )); ri.eq( "dup-key" );
fi.next()) { ri.next()) {
P p = fi.get(); P p = ri.get();
... ...
} }
ループ処理中で、キーの取得(key())、データの取得(get())、データの格納または設定(set( dt ))、
データの削除(del())を行うインターフェースは、ハッシュと同様に提供されます。
以下にitor/ritorのインターフェースを列挙しておきます。
void itor::init(); void ritor::init();
void itor::next(); void ritor::next();
bool itor::cont() const; bool ritor::cont() const;
bool itor::eq( const K& ky ) const; bool ritor::eq( const K& ky ) const;
bool itor::lt( const K& ky ) const; bool ritor::lt( const K& ky ) const;
bool itor::le( const K& ky ) const; bool ritor::le( const K& ky ) const;
bool itor::ge( const K& ky ) const; bool ritor::ge( const K& ky ) const;
bool itor::gt( const K& ky ) const; bool ritor::gt( const K& ky ) const;
bool itor::ne( const K& ky ) const; bool ritor::ne( const K& ky ) const;
T itor::find( const K& ky ); T ritor::find( const K& ky );
T itor::find_ge( const K& ky ); T ritor::find_le( const K& ky );
T itor::find_gt( const K& ky ); T ritor::find_lt( const K& ky );
const K itor::key() const; const K ritor::key() const;
const T itor::get() const; const T ritor::get() const;
T itor::set( const T& dt ); T ritor::set( const T& dt );
T itor::del(); T ritor::del();
ポインタ指向スタイルのC++プログラムでは、特に必要がない場合、
クラス宣言でコピー・コンストラクターとコピー代入演算子をプライベート宣言して、
暗黙のデータのコピーを抑止します。そしてコピーが必要な際には明示的にコピーを行うことになりますが、
なんらかの標準的な方法を設けないと、プログラムごとに複製方法が異なって具合のわるいことになります。
道具箱コンテナでは、拡張可能な関数テンプレートdup_fn<T>と、dup()関数によって、
繁用される文字列(char*など)と、ポインターに指されたクラスのデータの複製を行う標準的な方法を提供します。
複製が必要なクラスにはdup_fnテンプレートを定義し、複製の際にはdup関数を呼び出します。
dup_fnテンプレートは、ポインタの指し先のデータの複製を格納するための領域をnew
で確保し、
その領域にデータを複製して、確保した領域のポインタを返す、関数クラスのテンプレートです。
関数オペレータから返されたポインタはnew(文字列の場合はnew[])で作成されたものと同様に、
使用後にdelete(delete[])を実行する必要があることに注意してください。
複製が必要なポインタ指向スタイルのクラスには、以下の要領でこのテンプレートの特殊化したものを定義します。
template<> struct dup_fn<MyClass*>
{
MyClass* operator()( const MyClass* p )
{
MyClass* p = new MyClass;
...
return p;
}
};
また値指向スタイルとの併用を考えるクラスには、コピー・コンストラクターを用意することでも対応が可能なように、
デフォルトで以下の定義が提供されています。
テンプレート引数がポインタになっているため、ポインター以外への呼び出しはエラーになります。
template<typename K> struct dup_fn<K*>
{
K* operator()( const K* p ) { return p ? new K( *p ) : 0; }
};
dup_fnには、デフォルトで文字列(char*, const char*, wchar_t*, const wchar_t*)の複製の定義がなされています。
内部処理はワイド文字列に対応するため、C++の標準ライブラリを使用していますが、
実際の動作はCの多数の処理系で定義されているstrdup()と同様で、複製文字列の領域確保、複製を行い、
領域へのポインターを返します。
template<> struct dup_fn<char*> { };
template<> struct dup_fn<const char*> { };
template<> struct dup_fn<wchar_t*> { };
template<> struct dup_fn<const wchar_t*> { };
dup_fnテンプレートは、テンプレート定義や関数ポインタを引数とするコードでは便利です。
しかし、データへのポインタを使って複製をするというごく普通の使用には、インスタンスの宣言をするか、
コンストラクターを呼び出す必要があるため、可読性を多少妨げることになります。
そのため、omt/common.hには、dup関数テンプレートが定義されており、
データのポインターを使って簡単に複製が行えます。
この際も、戻り値のポインターをdelete(文字列の場合はdelete[])を実行する必要があることに注意してください。
// utility - generic duplication
template
dup関数から文字列に対応したdup_fnテンプレートを使用すれば、
文字列の種類にわずらわされず複製が簡単に行えます。
#include <omt/common.h>
using namespace omt;
...
char* sp = dup( "Hello, world!" );
wchar_t* wp = dup( L"Wide Character String" );
...
delete [] sp;
delete [] wp;