cliext::collection_adapter 使用上の注意

以前 SLT/CLR の collection_adapter に関して注意を書いたことがあったが,『STL/CLRツアーガイド : CodeZine』を読んでいて再び collection_adapter が気になった.今回はもう少し掘り下げて調べてみる.
結論から言えば,以下の点に注意する必要がある.

  • 当たり前だが,イテレータとポインタを混同しないこと.
  • cliext::collection_adapter::begin() は入力イテレータ(input_iterator)を返す.この入力イテレータへの操作は,列挙子(IEnumerator) の操作に変換される.
    • ただし IList/IList<T> が型引数の場合,collection_adapter::begin() は random_access_iterator を返す.この random_access_iterator は整数でコレクション要素をポイントし,列挙子(IEnumerator)は使用しない.
  • collection_adapter は IEnumerator の破棄を自動化していない.基となる IEnumerator が IDisposable を実装する場合,ユーザは自分の責任で IEnumerator の Dispose メソッドを呼び出さなければならない.
    • collection_adapter の実装には問題がある.collection_adapter が使用する入力イテレータは,終端に到達すると,内部に保持している IEnumerator を null でクリアする.ここでコレクションがひとつも要素を持たない場合を考えてみよう.collection_adapter::begin() から返されたイテレータは,生成した 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 であれば問題なかったわけだ.