在C++/CLI代码中从std :: stringstream读取std :: string的问题

par*_*mar 0 c++ c++-cli stringstream

我有一段代码来读取构建日期和月份.这__DATE__是在预定义宏中定义的

const char* BUILD_DATE = __DATE__;
std::stringstream ss(BUILD_DATE);
std::string month;
size_t year;
ss >> month;
ss >> year;
ss >> year;

char buffer[1024];
sprintf(buffer, "Read year=[%d] month=[%s] date=[%s]", year,month.c_str(),BUILD_DATE);
Run Code Online (Sandbox Code Playgroud)

当它正常工作时,通常是缓冲区

读取年份= [2013]月= [3月]日期= [2013年3月9日]

但在某些运行中它是

读取年份= [0]月= [M]日期= [2013年3月9日]

要么

读取年份= [2013]月= [3月]日期= [2013年3月9日]

基本上年份是0或月份有额外的空间.

该项目是在Windows 7上使用Microsoft Visual Studio 2010 SP1的x64/CLR版本.

我很难过为什么偶尔会发生这种情况.我是否正确使用stringstream?

par*_*mar 5

我最初想要删除这个问题,但我想我会分享我的发现以防其他一些可怜的灵魂遇到同样的问题.这个问题非常神秘,从未发生在我的应用程序的多次运行中,只发生在测试时,而且从未在调试测试时发生过.

这种无辜的功能

const char* BUILD_DATE = __DATE__;
std::stringstream ss(BUILD_DATE);
std::string month;
size_t year;
ss >> month;
ss >> year;
ss >> year;
Run Code Online (Sandbox Code Playgroud)

是在C++/CLI DLL中实现的.在我详细介绍细节之前,让我解释一下stringstream如何读取月份年份.要计算月份变量 ss >> month需要多少个字符,需要按空格分解ss字符串缓冲区.它通过使用当前语言环境来实现它的方式,特别是它的一个方面,称为ctype.ctype facet具有名为ctype :: is的函数,可以判断字符是否为空格.在一个表现良好的C++应用程序中,一切都按照标准工作.现在我们假设由于某种原因ctype facet被破坏了.Viola,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;
}
Run Code Online (Sandbox Code Playgroud)

这里myglobal就是你所谓的静态存储持续时间对象,保证在输入main之前初始化,在SimpleFunction中你总是看到myx42.myglobal的生命周期是我们通常称为每个进程的生命周期,因为它在问题的生命周期内是有效的.在富::〜富析构函数只有在主运行已经恢复.

输入C++/CLI和AppDomain

根据msdn的 AppDomain为您提供应用程序执行的隔离环境.对于C++/CLI,它引入了我称之为 appdomain存储持续时间的对象的概念

 __declspec(appdomain)   Foo myglobal;
Run Code Online (Sandbox Code Playgroud)

因此,如果您像上面那样更改了myglobal的定义,那么myglobal.x可能会在不同的appdomains中成为不同的值,就像线程本地存储一样.因此,在程序的初始化/退出期间初始化/清除静态持续时间的常规C++对象.我在这里使用init/exit/clean非常松散,但是你明白了.在加载/卸载AppDomain期间初始化/清除appdomain存储的对象.

典型的托管程序仅使用默认的AppDomain,因此每个进程/每个appdomain存储几乎相同.

在C++中,静态初始化顺序fiasco是一个非常常见的错误,其初始化期间静态存储持续时间的对象引用可能尚未初始化的静态存储持续时间的其他对象.

现在考虑当每个进程变量引用每个域变量时会发生什么.基本上在卸载AppDomain之后,每进程变量将引用垃圾内存.对于那些想知道它与原始问题有什么关系的人,请多跟我一点.

Visual studio use_facet实现

std :: use_facet用于从语言环境中获取感兴趣的方面.它被用来operator <<获得ctype方面.它被定义为

template <class Facet> const Facet& use_facet ( const locale& loc );
Run Code Online (Sandbox Code Playgroud)

请注意,它返回对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()
    }
Run Code Online (Sandbox Code Playgroud)

这里发生的事情是我们向locale询问感兴趣的方面并将其存储起来

   template<class _Facet>
    struct _Facetptr
    {   // store pointer to lazy facet for use_facet
    __PURE_APPDOMAIN_GLOBAL static const locale::facet *_Psave;
    };
Run Code Online (Sandbox Code Playgroud)

本地缓存_Psave,以便后续调用获得相同的方面更快.use_facet的调用者不负责返回的facet生命管理,所以如何清理这些facet.秘密是代码的最后一部分,注释排队等待销毁.该_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);
}
Run Code Online (Sandbox Code Playgroud)

很聪明吧.将所有new'd facet添加到链接列表,并使用静态对象析构函数清除它们.除了有一点问题._Fac_tidy_reg被标记为_AGLOBAL意味着在每个appdomain级别上销毁所有创建的构面.

locale::facet *_Psave另一方面,被宣告__PURE_APPDOMAIN_GLOBAL这似乎最终扩大到内涵每个进程.因此,在清理appdomain后,per-process _Psave可能会指向已删除的分面内存.这正是我的问题.VS2010单元测试的方式是一个名为QTAgent的过程运行所有测试.这些测试似乎是在同一QTAgent流程的不同运行中的不同appdomains中完成的.最有可能隔离先前测试运行的副作用,以进行后续测试.对于完全托管的代码来说,这一切都很好,几乎所有的静态存储都是线程/ appdomain级别,但对于使用per-process/per-appdomain的C++/CLI来说,这可能是一个问题.我之所以无法调试测试并且发现问题的原因是因为UT基础结构似乎总是产生一个新的QTAgent进程用于调试,这意味着新的appdomain和一个没有这些问题的新进程.