最初は質問を削除したくなりましたが、他の貧しい魂が同じ問題に遭遇した場合に備えて、調査結果を共有すると思いました. この問題は非常に不思議で、アプリケーションの複数回の実行では発生せず、テスト中にのみ発生し、テストのデバッグ中には発生しませんでした。
無邪気なこの機能
const char* BUILD_DATE = __DATE__;
std::stringstream ss(BUILD_DATE);
std::string month;
size_t year;
ss >> month;
ss >> year;
ss >> year;
C++/CLI dll で実装されました。詳細に入る前に、stringstream が月と年をどのように読み取るかを説明します。month変数 を構成する文字数を把握するss >> month
には、ss 文字列バッファーをスペースで区切る必要があります。現在のロケール、特にctypeと呼ばれるそのファセットを使用してそれを行う方法。ctype ファセットにはctype::isという関数があり、文字がスペースかどうかを判断できます。適切に動作する C++ アプリケーションでは、すべてが標準に従って機能します。ここで、何らかの理由でctype ファセットが破損したと仮定します。ビオラ、operator >>
何がスペースで何がそうでないかを判断できず、正しく解析できません。これはまさに私の場合に起こっていたことであり、以下に詳細を示します。
残りの回答は、Visual Studio 2010 によって提供される std c++ ライブラリと、C++/CLI での動作方法にのみ適用されます。
そのようないくつかのコードを検討してください
struct Foo
{
Foo()
{
x = 42;
}
~Foo()
{
x = 45;
}
int x;
};
Foo myglobal;
void SimpleFunction()
{
int myx = myglobal.x;
}
int main()
{
SimpleFunction();
return 0;
}
ここでmyglobalは、 main に入る前に初期化されることが保証された静的ストレージ期間を持つオブジェクトと呼ばれるもので、 SimpleFunction では常にmyx
が42として表示されます。myglobalの有効期間は、問題の有効期間中有効であるため、通常はプロセスごとと呼ばれるものです。Foo::~Fooデストラクタは、メインが戻った後にのみ実行されます。
C++/CLI と AppDomain に入る
msdn によるAppDomainは、アプリケーションが実行される分離された環境を提供します。C++/CLI の場合、 appdomain ストレージ期間と呼ぶオブジェクトの概念が導入されます。
__declspec(appdomain) Foo myglobal;
したがって、上記のように myglobal の定義を変更すると、myglobal.x が、スレッド ローカル ストレージのような別のアプリケーション ドメインで別の値になる可能性があります。したがって、静的期間の通常の C++ オブジェクトは、プログラムの初期化/終了時に初期化/消去されます。ここでは init/exit/cleaned を大まかに使用していますが、おわかりいただけたでしょうか。AppDomain ストレージのオブジェクトは、AppDomain のロード/アンロード中に初期化/消去されます。
典型的なマネージド プログラムはデフォルトの AppDomain のみを使用するため、プロセスごと/アプリドメインごとのストレージはほとんど同じです。
C++ では、初期化中に静的ストレージ期間のオブジェクトが、初期化されていない可能性がある静的ストレージ期間の他のオブジェクトを参照するという、静的初期化順序の大失敗は非常によくある間違いです。
ここで、プロセスごとの変数がドメインごとの変数を参照するとどうなるかを考えてみましょう。基本的に、AppDomain がアンロードされた後、プロセスごとの変数はジャンク メモリを参照します。元の問題とどう関係があるのか 疑問に思っている人は、もう少し我慢してください.
Visual Studio の use_facet の実装
std::use_facetは、ロケールから対象のファセットを取得するために使用されます。ファセットoperator <<
を取得するために使用さctype
次のように定義されます。
template <class Facet> const Facet& use_facet ( const locale& loc );
Facetへの参照を返すことに注意してください。VCによる実装方法は
const _Facet& __CRTDECL use_facet(const locale& _Loc)
{ // get facet reference from locale
_BEGIN_LOCK(_LOCK_LOCALE) // the thread lock, make get atomic
const locale::facet *_Psave =
_Facetptr<_Facet>::_Psave; // static pointer to lazy facet
size_t _Id = _Facet::id;
const locale::facet *_Pf = _Loc._Getfacet(_Id);
if (_Pf != 0)
; // got facet from locale
else if (_Psave != 0)
_Pf = _Psave; // lazy facet already allocated
else if (_Facet::_Getcat(&_Psave, &_Loc) == (size_t)(-1))
#if _HAS_EXCEPTIONS
_THROW_NCEE(bad_cast, _EMPTY_ARGUMENT); // lazy disallowed
#else /* _HAS_EXCEPTIONS */
abort(); // lazy disallowed
#endif /* _HAS_EXCEPTIONS */
else
{ // queue up lazy facet for destruction
_Pf = _Psave;
_Facetptr<_Facet>::_Psave = _Psave;
locale::facet *_Pfmod = (_Facet *)_Psave;
_Pfmod->_Incref();
_Pfmod->_Register();
}
return ((const _Facet&)(*_Pf)); // should be dynamic_cast
_END_LOCK()
}
ここで何が起こっているかというと、ロケールに関心のあるファセットを尋ねて、それを格納します。
template<class _Facet>
struct _Facetptr
{ // store pointer to lazy facet for use_facet
__PURE_APPDOMAIN_GLOBAL static const locale::facet *_Psave;
};
同じファセットを取得するための後続の呼び出しが高速になるように、ローカル キャッシュ_Psaveを使用します。use_facet の呼び出し元は、返されたファセットの寿命管理に責任を負わないため、これらのファセットはどのようにクリーンアップされますか。シークレットは、コメントqueue up lazy facet for destroyを含むコードの最後の部分です。は_Pfmod->_Register()
最終的にこれを呼び出します
__PURE_APPDOMAIN_GLOBAL static _Fac_node *_Fac_head = 0;
static void __CLRCALL_OR_CDECL _Fac_tidy()
{ // destroy lazy facets
_BEGIN_LOCK(_LOCK_LOCALE) // prevent double delete
for (; std::_Fac_head != 0; )
{ // destroy a lazy facet node
std::_Fac_node *nodeptr = std::_Fac_head;
std::_Fac_head = nodeptr->_Next;
_DELETE_CRT(nodeptr);
}
_END_LOCK()
}
struct _Fac_tidy_reg_t { ~_Fac_tidy_reg_t() { ::_Fac_tidy(); } };
_AGLOBAL const _Fac_tidy_reg_t _Fac_tidy_reg;
void __CLRCALL_OR_CDECL locale::facet::_Facet_Register(locale::facet *_This)
{ // queue up lazy facet for destruction
_Fac_head = _NEW_CRT _Fac_node(_Fac_head, _This);
}
かなり賢いですね。すべての新しいファセットをリンク リストに追加し、静的オブジェクト デストラクタを使用してそれらをすべて消去します。わずかな問題があることを除いて。_Fac_tidy_reg
_AGLOBAL としてマークされ、作成されたすべてのファセットがアプリドメインごとのレベルで破棄されることを意味します。
一方、最終的にper-processを意味するように拡張されるようにlocale::facet *_Psave
宣言されています。そのため、appdomain がクリーンアップされた後、削除されたファセット メモリを指す可能性があります。これはまさに私の問題でした。VS2010 単体テストが行われる方法は、 QTAgentと呼ばれるプロセスがすべてのテストを実行することです。これらのテストは、同じQTAgentによる異なる実行で異なる appdomain で行われているようです__PURE_APPDOMAIN_GLOBAL
per-process _Psave
処理する。以前のテスト実行の副作用を分離して、後続のテストに影響を与える可能性が最も高くなります。これは、ほぼすべての静的ストレージがスレッド/アプリドメイン レベルのいずれかである完全に管理されたコードでは問題ありませんが、プロセスごと/アプリドメインごとを誤って使用する C++/CLI では問題になる可能性があります。テストをデバッグできず、問題を見つけることができなかった理由は、UT インフラストラクチャがデバッグ用の新しいQTAgentプロセスを常に生成しているように見えるためです。これは、これらの問題のない新しい appdomain と新しいプロセスを意味します。