2002-02-20 Windows CEでSTL [長年日記]
■ Windows CEでSTL
Windows CEでSTLを使うという話は結構あって、STL for eMbedded Visual C++ - Windows CEとかがある。
ところが、これらのものはそのままでは特別な状況でもない限り使い物にならない。というのは、
1. STL(特にコンテナ)はC++例外に全面的に依存している
2. eMbedded VC++のコンパイラはC++例外が使えない
という条件があるため、1と2を考え合わせれば、「CEではSTLは使えない」という結論に達してしまう。
1はメモリ不足の時に問題になる。STLはメモリ不足が生じたときにstd::bad_allocがthrowされることを前提にしているから。たとえば、std::vector::push_back()しようとしてメモリが足りなくなったらどうするかというと、メモリを確保しようとした段階でstd::bad_allocがthrowされる。だから、push_back()の戻り値はvoidになっている。逆にいうと、eMbedded VC++のようにメモリ不足が生じたときに、newが0を返すようなコンパイラ/ライブラリとは一緒に使えない。
STLportの場合、operator newが0を返すと、実装側で捕まえて例外を投げようとする。__THROW_BAD_ALLOCというマクロがそれ。でも、eMbedded VC++は例外をサポートしていないので、結局そのマクロの中ではExitThreadを呼び出していて、その結果呼び出し元のスレッドを強制的に終了させて終わってしまう。
つまり、途中で突然終わっても差し障りのないプログラム(バッチ処理をするようなプログラムとか?)以外では、このままではSTLが使えない。メモリ不足で強制終了してしまっては困るので。
考えられる解決方法はといえば、
1. 関数のシグニチャを書き換えてエラーが起きたときに戻り値で判断できるようにする
2. スレッドローカル変数を用意して、メモリが確保できなかったらその変数に何らかの値を書き込んであとは何もせずに戻る
3. setjmpとlongjmpを使ってスタックを強制的に戻す
4. SEH (Structured Exception Handling)を使って呼び出し元に戻る
といったところ?
1をやってしまうと、すでにSTLではなくなってしまうので却下。2はなかなか良さそうだけれど、STL内部ではメモリを確保しようとして失敗したのにそのまま実行を続けるということは考えられていないので、あちこちに修正を入れまくらなくてはならないので却下。というわけで、STL本体になるべく修正を入れないでやれそうなのは3か4ということになる。
3も4も本質的には同じで呼び出された関数から一気に呼び出し元へ戻れる。致命的なのは、スタックが巻き戻されるときにローカルオブジェクトのデストラクタが呼び出されないこと。
ただし、逆にいうと、デストラクタ中で何かを行っているローカルオブジェクトがない限り思惑通りに動作する。また、デストラクタの中でメモリの開放だけを行っている場合、デストラクタが呼ばれなくても致命的なエラーにはならない。当然メモリリークが起きるわけだけれど、メモリ不足という致命的な状態から復帰できたと考えれば多少のメモリリークで済むなら御の字かなと。
問題になるのは、デストラクタでもっと仕事をしている場合。たとえば、コンストラクタでオブジェクトをロックしてデストラクタでアンロックしている場合、デストラクタが呼ばれないとオブジェクトがロックされたままの状態で残されてしまう。
ということを念頭において、STLportのソースを調べる。
とりあえずの目標は、std::vectorと各種のalgorithmが使えるようになること。さらに、コピーコンストラクタ/operator=()が例外を投げる可能性のあるオブジェクトはコンテナに格納しないという制約もつける。これらは、コンテナの伸張をするときにしばしば呼ばれるので、これらが例外を投げることを考慮するとかなり辛い。そんなオブジェクトを入れたいときにはポインタを入れることにする。
ここまでならば、std::vectorの実装でローカルオブジェクトのデストラクタに頼っている部分が殆どないため、殆ど手を入れずに実現できる。ただし、std::vectorのコピーコンストラクタとoperator=は使えない。このため、std::vectorをコピーしたい場合、
std::vector<int> dst; dst.resize(src.size()); std::copy(src.begin(), src.end(), dst.begin());
のようにして明示的にコピーする(ただし、resize()は例外を投げる可能性があるため、後述する処理が必要)。
やる必要があるのは、mallocが失敗したときに、::RaiseExceptionを使って適当なSEH例外を投げ、呼び出し元で捕まえることくらい。つまり、push_backを呼び出すときには、
#define EXCEPTION_CODE 0xE0000001 __try { v.push_back(1); } except (::GetExceptionCode() == EXCEPTION_CODE) { // メモリ不足が発生 }
のようにする。EXCEPTION_CODEは、::RaiseExceptionで投げる例外のID。ただし、毎回のようにこんなことを書くのは面倒なので、適当にテンプレートクラス/テンプレート関数を作って、例外を戻り値にマッピングして呼び出し元では戻り値のハンドリングになるようにする。
STLの例外が知りたくて<br>非常にためになりました。^^ありがとうございます。<br><br>my homepage<br>http://www3.to/shisui/