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

Problems reading std::string from std::stringstream in C++/CLI code

本文关键字:std string 读取 问题 stringstream C++ 代码 CLI      更新时间:2023-10-16

我有一段代码可以像这样读取构建日期和月份。这里__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);

当工作正常时,缓冲区通常为

读取年份=【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生成。

我不明白为什么这种情况偶尔会发生。我是否错误地使用字符串流?

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

这个无辜的功能

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中实现的。在我深入了解细节之前,让我解释一下字符串流是如何在此处读取月份年份的。若要计算month变量ss >> month的组成字符数,需要按空格分隔ss字符串缓冲区。它使用当前语言环境,特别是其中一个名为ctype的方面来实现这一点。ctype方面有一个名为ctype::is的函数,它可以判断字符是否为空格。在一个性能良好的C++应用程序中,一切都是按照标准工作的。现在让我们假设由于某种原因ctype方面已损坏。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;
}

这里myglobal是您所称的对象,其静态存储持续时间保证在输入main之前初始化,在SimpleFunction中,您将始终看到myx42myglobal的生存期是我们通常称之为每个进程的,因为它在问题的生存期内有效。Foo::~Foo析构函数只有在main返回后才会运行。

输入C++/CLI和AppDomain

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

__declspec(appdomain)   Foo myglobal;

因此,如果您像上面那样更改myglobal的定义,那么在不同的应用程序域中,myglobal.x可能会是不同的值,有点像线程本地存储。因此,静态持续时间的常规C++对象在程序的初始化/退出过程中被初始化/清理。我在这里使用init/exit/cleaned非常松散,但你已经明白了。appdomain存储的对象在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的调用方不负责返回的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);
}

很聪明,对。将所有新的facet添加到链表中,并使用静态对象析构函数将它们全部清除。只是有一个小问题。_Fac_tidy_reg被标记为_AGLOBAL,这意味着所有创建的facet都在每个appdomain级别上被销毁。

另一方面,locale::facet *_Psave被声明为__PURE_APPDOMAIN_GLOBAL,它似乎最终扩展为每个进程的含义。因此,在清理appdomain之后,per-process _Psave可能会指向已删除的分面内存。这正是我的问题。VS2010单元测试的发生方式是一个名为QTAgent的进程运行所有测试。这些测试似乎是在不同的应用程序域中由同一QTAgent进程在不同的运行中完成的。最有可能隔离之前测试运行的副作用,以影响后续测试。对于完全托管的代码来说,这一切都很好,因为几乎所有的静态存储都是线程/应用程序域级别的,但对于不正确使用每个进程/每个应用程序域的C++/CLI来说,这可能是一个问题。我之所以永远无法调试测试并发现问题,是因为UT基础设施似乎总是产生一个新的QTAgent进程进行调试,这意味着一个新应用程序域和一个没有这些问题的新进程。

我建议尝试查看实际日期字符串:

cout << "Raw date: " << ss.str() << "n";

或者使用调试器,在创建ss变量后查看它。