読者です 読者をやめる 読者になる 読者になる

STL/CLR の collection_adapter は要注意

.NET

STL/CLR には,cliext::collection_adapter というクラスが導入され,IEnumerable<T> をラップして C++イテレータとして扱うことが出来るようになりました.以下のエントリで επιστημη さんも書かれていますが,たとえば STL の algorithm に適用したい場合などに便利と.

しかし,この cliext::collection_adapter,あんまり深く考えずに使うと IEnumerable<T> を実装していれば必ず IDisposable である理由 を台無しにしてしまうので,結構使い方が難しいと思うのですよ.
たとえば,C++/CLI で以下のような 5 パターンの ForEach メソッドを作ってみます.

using namespace System;
using namespace System::Collections::Generic;

#include <cliext\list>
#include <cliext\algorithm>
#include <cliext\adapter>

namespace STLCLRTest {

    public ref class Util
    {
    public:
        static void ForEach1(IEnumerable<System::String ^>^ strlist, Action<System::String ^>^ action)
        {
            for each(System::String ^ str in strlist)
            {
                action(str);
            }
        }
        static void ForEach2(IEnumerable<System::String ^>^ strlist, Action<System::String ^>^ action)
        {
            cliext::collection_adapter< IEnumerable<System::String ^> > itr(strlist);
            cliext::for_each(itr.begin(), itr.end(), action );
        }
        static void ForEach3(IEnumerable<System::String ^>^ strlist, Action<System::String ^>^ action)
        {
            cliext::collection_adapter< IEnumerable<System::String ^> > itr(strlist);
            cliext::for_each(itr.begin(), itr.end(), action );
            delete itr;
        }
        static void ForEach4(IEnumerable<System::String ^>^ strlist, Action<System::String ^>^ action)
        {
            cliext::collection_adapter< IEnumerable<System::String ^> > itr(strlist);
            try
            {
                cliext::for_each(itr.begin(), itr.end(), action );
            }
            finally
            {
            }
            delete itr;
        }
        static void ForEach5(IEnumerable<System::String ^>^ strlist, Action<System::String ^>^ action)
        {
            cliext::collection_adapter< IEnumerable<System::String ^> > itr(strlist);
            try
            {
                cliext::for_each(itr.begin(), itr.end(), action );
            }
            finally
            {
                delete itr;
            }
        }
    };
}

んではこれを C# から使ってみましょう.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

class Program
{
    static IEnumerable<string> TestItr
    {
        get
        {
            try
            {
                yield return "1";
                yield return "2";
            }
            finally
            {
                Console.WriteLine("Finish!");
            }
        }
    }

    static void Main(string[] args)
    {
        var funcs = new Action<IEnumerable<string>, Action<string>>[]
                {
                    STLCLRTest.Util.ForEach1,
                    STLCLRTest.Util.ForEach2,
                    STLCLRTest.Util.ForEach3,
                    STLCLRTest.Util.ForEach4,
                    STLCLRTest.Util.ForEach5,
                };
        foreach (var func in funcs)
        {
            try
            {
                Console.WriteLine(func.Method.Name);
                func(TestItr, str => { throw new Exception(); });
            }
            catch {}
        }

    }
}

列挙中に例外が発生したとき,果たして C# イテレータの finally が呼ばれるかというテストです.これはすなわち,列挙中断時に IEnumerator<T>.Dispose が確定的に呼び出されるか,ということに帰着されます.
んで,結果はこの通り.Orcas beta 2 調べです.

ForEach1
Finish!
ForEach2
ForEach3
ForEach4
ForEach5
Finish!

なんか予想に反して ForEach5 で Dispose が呼ばれているのが気になりますが,多分バグだろうということでスルーしつつ*1,この結果を見る限り STL/CLR は,IEnumerable<T> のセマンティクスに敬意を払っていないんじゃないか,というわけです.

*1:connect サイトにはフィードバックを入れておきましたのでご安心を.追記.「お前のコードがバグってる」言われました.まあそうなんでしょうが.「C++/CLI……恐ろしい子!」