G++ 在销毁静态变量的顺序上存在问题

10

我有以下类试图实现一个通用的 Singleton。

struct BaseObject
{
   virtual ~BaseObject() {}
};
class _helper
{
private:
    template<typename T> friend class Singleton;
    set<BaseObject*> _s;
    static _helper& _get()
    {
        static _helper t;
        return t;
    }
    _helper()
    {
        cout<<" _helper ctor"<<endl;
    }
    ~_helper()
    {
        cout<<" _helper dtor"<<endl;
        //assert(_s.empty());
    }
};

// Singleton<foo>::Instance() returns a unique instance of foo
template <typename T>
class Singleton : virtual private T
{
public:
    static T& Instance()
    {
        static Singleton<T> _T;
        return _T;
    } 

private:
    Singleton()
    {
        cout<<"inserting into helper "<<typeid(T).name()<<" ptr "<<this<<endl;
        assert(!_helper::_get()._s.count(this));
        _helper::_get()._s.insert(this);
    }
    ~Singleton()
    {
        cout<<"erasing from helper "<<typeid(T).name()<<" ptr "<<this<<endl;
        assert(_helper::_get()._s.count(this));
        _helper::_get()._s.erase(this);
    }
};

现在,如果我调用Singleton< bar>::Instance(),然后调用Singleton< foo>::Instance(),我应该看到以下输出:
 inserting into helper 3bar ptr 0x509630  
 _helper ctor  
 inserting into helper 3foo ptr 0x509588  
 erasing from helper 3foo ptr 0x509588  
 erasing from helper 3bar ptr 0x509630  
 _helper dtor  

然而,在某些情况下,我看到以下内容:

 inserting into helper 3bar ptr 0x509630  
 _helper ctor  
 inserting into helper 3foo ptr 0x509588  
 erasing from helper 3bar ptr 0x509630  
 _helper dtor  
 erasing from helper 3foo ptr 0x509588  

请注意,在第二种情况下,barfoo的销毁顺序与它们构造的顺序相同。当foobar单例在共享库(.so)中作为静态引用实例化时,似乎会发生这种情况。
static bar& b = Singleton<bar>::Instance();   
static foo& f = Singleton<foo>::Instance();   

有什么想法是它为什么会这样做?

2
它们是否在同一翻译单元中? - GManNickG
1
我必须在这里提出一些个人意见。这是单例模式的错误应用。单例模式应严格保留给那些在程序中必须是独立的对象,而不是应用于仅在程序中存在一次的某个对象。如果您需要一个通用的单例模式设施,则应从中继承,使对象无法存在于多个实例中。如果您需要全局变量,请使用一个。单例模式!=全局变量。 - Edward Strange
4
标准要求构造和销毁的顺序相反,并且对于单个翻译单元中的全局静态定义,它还规定了定义的顺序必须保持不变。对于函数静态变量,构造的顺序是函数调用的顺序,但销毁的顺序仍然应该与构造的顺序相反。 - David Rodríguez - dribeas
5
在C++11中,由于线程静态对象的存在,措辞有点复杂,但在C++03中,3.6.3p1非常清晰:对于初始化的静态存储期对象,析构函数(12.4)[...] 这些对象按照它们的构造函数完成顺序或动态初始化完成顺序的相反顺序被销毁。没有提到不同的翻译单元。重要的信息是,虽然跨翻译单元的构建顺序未定义,但销毁顺序保证是相反的。 - David Rodríguez - dribeas
3
如果你仔细阅读最后一个转储,你会发现顺序是不一致的:在Singleton<foo>Singleton<bar>之前构造了Singleton<bar>,并且在Singleton<foo>Singleton<bar>之前析构了Singleton<bar>,而它应该是这样的(缩写):S<bar>,S<foo> => ~S<foo>,~S<bar>。那就是不一致性所在,对于这些运行,实现似乎没有产生正确的终止代码。 - David Rodríguez - dribeas
显示剩余11条评论
2个回答

1

如果单例和辅助程序位于不同的翻译单元或不同的共享对象中,则可能会发生这种情况。请记住,很难预测模板实例在哪个翻译单元中结束。还要记住,每个共享对象都可以获得自己的一个实例,例如Singleton<foo>::_T。因此,您拥有某种每个共享对象的单例(在我看来不是非常有用)。

请注意,在最后一个对象从_helper中删除之前,其会被销毁(注意顺序)。这将导致程序退出时崩溃。是的,我遇到了这种情况。您需要在_helper类中实现对象计数器,以便它不会在至少有一个对象与其注册时被销毁。或者,在堆上分配所有单例,并在其生命周期结束时让_helper销毁它们。

更新: 如果两个静态对象由同一动态库拥有,则该销毁顺序逆转可能不会发生。否则肯定会发生。 在这里建议程序员不要越过动态库边界导出静态对象。


2
即使在那种情况下也不应该发生。跨越TU边界的构造顺序是未定义的,但标准规定销毁顺序是适当构造函数(或动态初始化)完成的相反顺序。也就是说,给定两个静态对象ab在两个翻译单元中,标准要求有两种可能的顺序:a,b,~b,~ab,a,~a,~b - David Rodríguez - dribeas
正是我所想的情况。 - Yuvraj Dhillon
2
标准对动态加载库没有任何规定。实际上,当这样一个库被卸载时,它所"拥有"的所有静态对象都会被销毁,而不管其他.so/.dlls中的静态对象是如何构造的。 - n. m.
我没有使用dlopen / dlclose来加载我的共享库。我认为常规的.so用法不会有这个问题。 - Yuvraj Dhillon
理论上可能存在两个不同的销毁顺序,一个用于 dlopen,另一个用于“常规”使用。但实际上并不是这样做的。我知道,我在生产代码中遇到了这种行为。一个静态变量被移动到另一个库中,突然间所有东西在终止时都开始转储核心。 - n. m.

0

static _helper t; 据我所知,定义了一个地址。

static _helper& _get()
{
    static _helper t;
    return t;
}

但看起来你正在尝试将它用于两个不同的对象。

无论如何,我从未见过模板被用于单例。在你的情况下,似乎你正在尝试销毁一个单例。这也是我之前没有见过的。单例通常只创建一次,并保留到你离开程序(并且在你离开时仍然分配内存)。

否则,您可能需要考虑使用共享指针或侵入式引用计数对象?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接