0

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

我很困惑为什么偶尔会发生这种情况。我是否错误地使用了字符串流?

4

2 回答 2

5

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

这个看起来很无辜的功能

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 如何在这里读取月份年份。要弄清楚有多少个字符构成月份变量 ss >> month,需要用空格分隔 ss 字符串缓冲区。它使用当前语言环境的方式,特别是它的一个方面,称为ctype。ctype facet 有一个叫做ctype::is 的函数,它可以判断一个字符是否是空格。在一个表现良好的 C++ 应用程序中,一切都按照标准工作。现在让我们假设由于某种原因ctype facet被破坏了。中提琴,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

根据 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 的调用者不负责返回的分面生命周期管理,那么这些分面是如何清理的。秘诀是代码的最后一部分,带有注释排队延迟方面进行破坏。最终_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 意味着所有创建的方面都在每个应用程序域级别上被销毁。

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

于 2013-03-11T00:40:07.010 回答
0

我建议尝试这个来查看实际的日期字符串:

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

或者使用调试器单步执行并在创建变量后查看ss它。

于 2013-03-09T18:46:43.510 回答