cliext::collection_adapter 使用上の注意
以前 SLT/CLR の collection_adapter に関して注意を書いたことがあったが,『STL/CLRツアーガイド : CodeZine』を読んでいて再び collection_adapter が気になった.今回はもう少し掘り下げて調べてみる.
結論から言えば,以下の点に注意する必要がある.
- 当たり前だが,イテレータとポインタを混同しないこと.
- cliext::collection_adapter::begin() は入力イテレータ(input_iterator)を返す.この入力イテレータへの操作は,列挙子(IEnumerator) の操作に変換される.
- collection_adapter は IEnumerator の破棄を自動化していない.基となる IEnumerator が IDisposable を実装する場合,ユーザは自分の責任で IEnumerator の Dispose メソッドを呼び出さなければならない.
C++ のイテレータについて復習
C++ のイテレータは,ポインタのような単なる値とはみなせないことがある.これは STL/CLR に限らない話.
典型的な入力イテレータである,std::istream_iterator の振る舞いを見てみよう.
std::istream_iterator<int> input(std::cin); std::istream_iterator<int> iend; { // これは input の位置を退避したことにはならない std::istream_iterator<int> input2 = input; // 以下の操作は,input2 が次にポイントする位置に影響を与える if( input != iend ) ++input; if( input != iend ) ++input; if( input != iend ) ++input; }
cliext::collection_adapter が返すイテレータの種類
ここから本題.
cliext::collection_adapter は,.NET のコレクションインターフェイスを STL/CLR コレクションに変換するアダプタである.
MSDN Library によれば,cliext::collection_adapter は型引数によって 8 通りに特殊化されている.
Specialization
Description Description IEnumerable Sequence through elements. ICollection Maintain group of elements. IList Maintain an ordered group of elements. IDictionary Maintain a set of {key, value} pairs. IEnumerable<Value> Sequence through typed elements. ICollection<Value> Maintain group of typed elements. IList<Value> Maintain ordered group of typed elements. IDictionary<Value> Maintain a set of typed {key, value} pairs.
特殊化によって,cliext::collection_adapter がサポートするイテレータの種類も異なる.それを調べてみる.
#include <cliext\algorithm> #include <cliext\adapter> using namespace System; template<typename T> void print_collection_adapter_iterator_type() { Console::WriteLine("{0} : collection_adapter<{1}>::iterator", (typename cliext::collection_adapter<T>::iterator::iterator_category::typeid), (typename T::typeid)->Name); } int main() { { using namespace System::Collections::Generic; print_collection_adapter_iterator_type<IEnumerable<String^>>(); print_collection_adapter_iterator_type<ICollection<String^>>(); print_collection_adapter_iterator_type<IList<String^>>(); print_collection_adapter_iterator_type<IDictionary<String^,String^>>(); } { using namespace System::Collections; print_collection_adapter_iterator_type<IEnumerable>(); print_collection_adapter_iterator_type<ICollection>(); print_collection_adapter_iterator_type<IList>(); print_collection_adapter_iterator_type<IDictionary>(); } return 0; }
結果.
cliext.input_iterator_tag : collection_adapter<IEnumerable`1>::iterator cliext.input_iterator_tag : collection_adapter<ICollection`1>::iterator cliext.random_access_iterator_tag : collection_adapter<IList`1>::iterator cliext.input_iterator_tag : collection_adapter<IDictionary`2>::iterator cliext.input_iterator_tag : collection_adapter<IEnumerable>::iterator cliext.input_iterator_tag : collection_adapter<ICollection>::iterator cliext.random_access_iterator_tag : collection_adapter<IList>::iterator cliext.input_iterator_tag : collection_adapter<IDictionary>::iterator
まとめてみる.
型引数 | イテレータカテゴリ |
---|---|
IEnumerable | 入力イテレータ |
ICollection | 入力イテレータ |
IList | ランダムアクセスイテレータ |
IDictionary | 入力イテレータ |
IEnumerable<Value> | 入力イテレータ |
ICollection<Value> | 入力イテレータ |
IList<Value> | ランダムアクセスイテレータ |
IDictionary<Key,Value> | 入力イテレータ |
すなわち,型引数が IList/IList<Value> となる場合を除き,collection_adapter は入力イテレータを返すコンテナである.
操作の対応関係
IEnumerator 操作とイテレータ操作がどのように対応付けられるのか,collection_adapter<IEnumerable<string>> の場合について見てみる.
IEnumerable<System::String^>^ enumerable; IEnumerator<System::String^>^ e = enumerable.GetEnumerator(); cliext::collection_adapter<IEnumerable<System::String^>> X(enumerable); cliext::collection_adapter<IEnumerable<System::String^>>::iterator i = X.begin(); cliext::collection_adapter<IEnumerable<System::String^>>::iterator end = X.end();
このとき操作の対応関係は以下のようになる.
collection_adapter イテレータ操作 | .NET 列挙子操作 |
---|---|
i = X.begin(); | e = enumerable.GetEnumerator(); e.MoveNext(); |
++i; | e.MoveNext(); |
i._Myenum.Dispose(); | e.Dispose(); |
注意として,C++/CLI では IDisposable.Dispose() を直接呼び出す代わりに,delete 演算子を使用する.これについては次にコード例を示す.
collection_adapter 使用時に,列挙子の確定論的破棄を行うコード例
.NET には列挙に関してデザインパターンが存在し,IEnumerator の確定論的破棄まで考慮されている.collection_adapter でこれを実現するコード例を示す.
cliext::collection_adapter<IEnumerable<System::String^>> X(enumerable); cliext::collection_adapter<IEnumerable<System::String^>>::iterator i; IDisposable^ ptr = nullptr; try { i = X.begin(); // X がひとつも要素を持たなかったとき, // i._Myenum は既に null で上書きされているため,Dispose が呼び出せない. // (列挙子によっては終端到達と共に MoveNext 内でリソース解放を行うものがある. // この場合,Dispose を呼び出さなくてもリソース解放は完了している) ptr = dynamic_cast<IDisposable^>(i._Myenum); // do something } finally { if(ptr != nullptr) { delete ptr; // Dispose メソッド呼び出しに対応 ptr = nullptr; } }
コメントに書いた通り,列挙対象がひとつも要素を持たなかった場合が厄介だ.この場合 IDisposable.Dispose は呼び出せない.
ただし IEnumerator の実装にも色々ある.MoveNext で false を返すついでにリソース解放まで行う IEnumerator なら問題ない.実際調べてみるとそのような IEnumerator は多い.さらに調べてみると,C# の yield 構文の仕様に,最後の MoveNext でリソース解放を行うとある.つまり C# の yield 構文を使って実装された IEnumerator であれば問題なかったわけだ.