"动态atexit析构函数"中的"动态"是什么意思?

sha*_*oth 6 debugging visual-studio-2008 visual-c++

我最近将我的应用程序从VC++ 7移植到了VC++ 9.现在它有时在退出时崩溃 - 运行时开始调用全局对象析构函数,并且其中一个发生访问冲突.

每当我观察调用堆栈时,顶级函数是:

CMyClass::~CMyClass() <- crashes here
dynamic atexit destructor for 'ObjectName'
_CRT_INIT()
some more runtime-related functions follow
Run Code Online (Sandbox Code Playgroud)

问题是"动态atexit析构函数"中"动态"一词的含义是什么?它能为我提供任何其他信息吗?

Alo*_*lon 10

没有实际的代码很难确定确切的问题,但也许你可以在阅读之后自己找到它:

来自http://www.gershnik.com/tips/cpp.asp (链接已经死了,见下文)

atexit()和动态/共享库

C和C++标准库包括一个有时有用的函数:atexit().它允许调用者注册将在应用程序退出时(通常)调用的回调.在C++中,它还与调用全局对象的析构函数的机制集成,因此在给定的atexit()调用之前创建的内容将在回调之前被销毁,反之亦然.所有这一切都应该是众所周知的,并且它完全正常,直到DLL或共享库进入图片.

当然,问题在于动态库有自己的生命周期,通常可以在主应用程序之前结束.如果DLL中的代码将其自己的一个函数注册为atexit()回调函数,则最好在卸载DLL之前调用此回调.否则,在主应用程序退出期间将发生崩溃或更糟糕的事情.(为了使退出期间出现令人讨厌的崩溃是众所周知的难以调试,因为许多调试器在处理死亡进程时遇到问题).

在C++全局对象(如上所述,是atexit()的兄弟)的析构函数的上下文中,这个问题更为人所知.显然,支持动态库的平台上的任何C++实现都必须处理这个问题,并且一致的解决方案是在卸载共享库或应用程序退出时调用全局析构函数,以先到者为准.

到目前为止一直很好,除了一些实现"忘记"将相同的机制扩展到普通的旧atexit().由于C++标准没有提及动态库的任何内容,因此这些实现在技术上是"正确的",但这对于那些由于某种原因需要调用atexit()传递驻留在DLL中的回调的程序员来说无济于事.

在平台上我知道的情况如下.Windows上的MSVC,Linux和Solaris上的GCC以及Solaris上的SunPro都具有"正确"的atexit(),其工作方式与全局析构函数相同.但是,在撰写本文时,FreeBSD上的GCC有一个"破坏"的,它总是注册要在应用程序上执行的回调而不是共享库退出.然而,正如所承诺的那样,即使在FreeBSD上,全局析构函数也能正常工作.

你应该怎么做便携式代码?当然,一种解决方案是完全避免使用atexit().如果您需要它的功能,很容易用以下方式用C++析构函数替换它

//Code with atexit()

void callback()
{
    //do something
}

...
atexit(callback);
...

//Equivalent code without atexit()

class callback
{
public: 
    ~callback()
    {
        //do something
    }

    static void register();
private:
    callback()
    {}

    //not implemented
    callback(const callback &);
    void operator=(const callback &); 
};

void callback::register()
{
    static callback the_instance;
}

...
callback::register();
...
Run Code Online (Sandbox Code Playgroud)

这是以很多打字和非直观界面为代价的.值得注意的是,与atexit()版本相比,功能没有任何损失.回调析构函数不能抛出异常,但是atexit调用的函数也是如此.callback :: register()函数在给定平台上可能不是线程安全的,但是atexit()也是如此(C++标准目前在线程上是静默的,因此是否以线程安全的方式实现atexit()取决于实现)

如果您想避免上面的所有输入怎么办?通常有一种方法,它依赖于一个简单的技巧.我们需要做C++编译器注册全局析构函数所做的任何事情,而不是调用破坏的atexit().使用GCC和其他实现所谓的Itanium ABI(广泛用于非Itanium平台)的编译器,魔术咒语称为__cxa_atexit.以下是如何使用它.首先将下面的代码放在一些实用程序头中

#if defined(_WIN32) || defined(LINUX) || defined(SOLARIS)

    #include <stdlib.h>

    #define SAFE_ATEXIT_ARG 

    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG)) 
    {
        atexit(p);
    }

#elif defined(FREEBSD)

    extern "C" int __cxa_atexit(void (*func) (void *), void * arg, void * dso_handle);
    extern "C" void * __dso_handle;     


    #define SAFE_ATEXIT_ARG void *

    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG))
    {
        __cxa_atexit(p, 0, __dso_handle);
    }

#endif
And then use it as follows


void callback(SAFE_ATEXIT_ARG)
{
    //do something
}

...
safe_atexit(callback);
...
Run Code Online (Sandbox Code Playgroud)

__cxa_atexit的工作方式如下.它将回调注册在单个全局列表中,与非DLL感知atexit()的方式相同.但是它也将其他两个参数与它相关联.第二个参数只是一个很好的东西.它允许回调传递一些上下文(比如某个对象的这个),因此可以重复使用单个回调进行多次清理.第三个参数是我们真正需要的参数.它只是一个"cookie",用于标识应与回调关联的共享库.当卸载任何共享库时,其清理代码遍历atexit回调列表并调用(并删除)具有与要卸载的库关联的cookie匹配的cookie的任何回调.cookie的价值应该是多少?它可能不是DLL起始地址而不是它的dlopen()句柄.相反,句柄存储在由C++运行时维护的特殊全局变量__dso_handle中.

safe_atexit函数必须是内联的.这样它就可以选择调用模块使用的__dso_handle,这正是我们所需要的.

您是否应该使用此方法而不是上面的冗长且更便携的方法?可能不是,虽然谁知道你可能有什么要求.尽管如此,即使您没有使用它,也有助于了解事情是如何工作的,所以这就是为什么它包含在这里.