如何确保辛格尔顿不会被过早销毁

How to ensure Singleton is not destroyed prematurely?

本文关键字:何确保 确保      更新时间:2023-10-16

在我的项目中,我正在与大约 4 个单身人士合作,以 Scott Meyer 的方式制作。其中之一:

LevelRenderer& LevelRenderer::Instance()
{
    static LevelRenderer obj;
    return obj;
}

现在,其中两个单例,LevelRendererLevelSymbolTable相互交互。例如,在此方法中:

void LevelRenderer::Parse(std::vector<std::string>& lineSet)
{
    LevelSymbolTable& table = LevelSymbolTable::Instance();
    /** removed code which was irrelevant **/
    // for each line in lineSet
    BOOST_FOREACH(std::string line, lineSet)
    {
        // for each character in the line
        BOOST_FOREACH(char sym, line)
        {
            /** code... **/
            // otherwise
            else
            {
                sf::Sprite spr;
                // Used LevelSymbolTable's Instance here...
                table.GenerateSpriteFromSymbol(spr, sym);
                // ^ Inside LevelRenderer
                /** irrelevant code... **/
            }
        }
    }
}

现在,虽然问题还没有发生。我害怕的是,如果在我调用GenerateSpriteFromSymbol之前LevelSymbolTable实例已经被销毁怎么办?

由于我使用了 Scott Meyer 方式,因此单例实例由堆栈分配。因此,保证使用上次创建的第一次销毁规则进行销毁。现在,如果LevelSymbolTable的实例是在LevelRenderer的实例之后创建的,它将在LevelRenderer的实例之前被销毁,对吗?因此,如果我调用一种LevelRenderer内部LevelSymbolTable的方法(特别是在LevelRenderer的析构函数中),我将踩在未定义的行为土地上。

正如我之前所说,这个问题在调试时实际上并没有发生,纯粹是我的假设和猜测。那么,我的结论正确吗?LevelSymbolTable有可能在LevelRenderer之前被销毁.如果是这样,有什么办法摆脱这个烂摊子吗?

您不必担心这里的任何事情。 static 关键字保证它从初始化到程序退出时可用。因此,您可以在初始化静态变量后的任何时候调用静态变量。

此外,您还引用了 LevelSymbolTable,而不是局部变量。这就是类名后面的与号的含义。所以你可以在本地使用它,但它实际上是"指代"存在于其他地方的真实对象。因此,当方法退出时,引用将超出范围,但它引用的对象不会。

*好吧,您可能需要担心一件事。在析构函数中,您应该只清理任何内存或文件引用或您有权处理的其他性质的东西。我不知道你为什么要在析构函数中调用其他对象。

定义对象之间的所有权关系。要么有LevelSymbolTable作为LevelRenderer的成员:

class LevelRenderer {
    LevelSymbolTable symbolTable;
public:
    static LevelRenderer& getInstance();
    ~LevelRenderer() { /* can use symbolTable here */ }
};

或者创建一个同时包含SymbolTableRenderer的单例Level

class Level {
    SymbolTable symbolTable;
    Renderer levelRenderer;   // note the order here
public:
    static Level& getInstance();
private:
    /* have LeverRenderer save reference to symbol table,
       now renderer can use symbol table anywhere */
    Level() : levelRenderer(symbolTable)
    { /* ... */ }
};

编辑:或者完全摆脱单例。了解为什么单身人士不好。我不知道您的应用程序的结构,但据我所知,您可以Level为一个知道如何呈现自身并具有其符号表的普通类。并使其生存期连接到它应该在应用程序中表示的级别。

静态实例将在程序开始时(在 main 之前)创建,并在结束时(在 main 之后)进行清理,您不能依赖清理它们的任何特定顺序。也就是说,如果您有两个实例(为了简单起见,让我们将它们设为全局实例)

class one {
  one() {}
  ~one() {}
};
class two {
  two() {}
  ~two() {}
};
one the_one;
two the_other;
int main() {
  ...
  return 0;
}

您不能也不应该假设the_onethe_other的构造函数或析构函数中处于活动状态。(反之亦然。

但是,您可以依赖它们在任何其他成员函数中处于活动状态,并且在 main 本身中处于活动状态。

您在

问题中提出的场景不太可能发生,因为Parse可能在程序仍处于活动状态时被调用。只有当程序即将退出时,才会调用析构函数。

在您的评论中,您指出了一种略有不同的担忧,即全局析构函数相互依赖性。如果您有全局对象向某个全局容器注册自身,则实际上可能会发生这种情况。您可能期望对象会从容器中删除自身,并且容器会弹出对象。

解决此问题的一种方法是允许容器取得向其注册的对象的所有权。这意味着注册到全局容器的是动态分配的实例,而不是 Scott Meyer 的单例实例。然后,全局容器将负责在调用其全局析构函数时清理已注册的项。