3

我已经有一些使用指针的单例类:

class Logger
{
public:

    static Logger* Instance()
    {
        if (!m_pInstance) m_pInstance = new Logger;
        return m_pInstance;
    }
private:
    Logger();
    Logger(Logger const&);
    Logger& operator=(Logger const&);
    static Logger* m_pInstance;
};

但是,有一种更简单的方法,使用引用:

class Logger
{
public:
    static Logger& Instance()
    {
        static Logger theLogger;
        return theLogger;
    }
private:
    Logger();
    Logger(Logger const&);
    Logger& operator=(Logger const&);
    ~Logger();
};

阅读文章C++ Singleton design pattern,它警告第二种方式:

[潜在陷阱]:由于对象的预期寿命,这种形式的单例可能会出现问题。如果一个单例在另一个中实例化,则必须敏锐地意识到析构函数的调用顺序。

但我无法理解,有人可以告诉我一个不好的用法,我应该避免它吗?

4

1 回答 1

7

确实,这两种选择都存在问题。

典型的单身人士

class Logger
{
public:

    static Logger* Instance()
    {
        if (!m_pInstance) m_pInstance = new Logger;
        return m_pInstance;
    }
private:
    Logger();
    Logger(Logger const&);
    Logger& operator=(Logger const&);
    static Logger* m_pInstance;
};

此代码不是线程安全的,因此调用new Logger可能会发生多次(在不同的线程中)。它还会泄漏Logger实例(因为它从未被删除),如果应该执行析构函数,这可能是一个问题。

迈耶的单身人士

class Logger
{
public:
    static Logger& Instance()
    {
        static Logger theLogger;
        return theLogger;
    }
private:
    Logger();
    Logger(Logger const&);
    Logger& operator=(Logger const&);
    ~Logger();
};

Meyer's Singleton 的问题确实是由于对象的破坏。从 main 返回后,所有全局变量的析构函数将按照构造这些全局变量1的相反顺序运行。这就好像那些全局变量是在堆栈上构建的。

如果一个对象是在你的 Logger 之前构造的,并试图在它自己的析构函数中使用它;那么它将使用一个已经被破坏的对象,导致未定义的行为。

1更准确地说,他们的构造函数以相反的顺序完成。


最简单的替代方法是重新访问典型的 Singleton

class Logger {
public:
    static Logger& Instance() {
        static Logger* L = new Logger;
        return *L;
    }
private:
    ...
};

这现在是线程安全的,但仍然泄漏。但实际上这里需要泄漏,因为它保证该对象比任何其他将调用其析构函数的对象保持存活的时间更长。警告:如果你在析构函数中有什么事情要做,它永远不会运行。

还有其他变体,例如 Alexandrescu 的 Phoenix Singleton 会从灰烬中复活,如果您在它死后需要它;然而,在销毁过程中同时获得线程安全安全行为是很难实现的。

于 2013-03-09T16:27:32.753 回答