管理单例析构函数

7
以下是一个我经常见到的实现单例模式的小例子:
#include <iostream>

class SingletonTest {
private:
  SingletonTest() {}
  static SingletonTest *instance;
  ~SingletonTest() {
    std::cout << "Destructing!!" << std::endl;
  }

public:
  static SingletonTest *get_instance()  {
    if(!instance) instance = new SingletonTest;
    return instance;
  }
};

SingletonTest *SingletonTest::instance = 0;

int main(int argc, char *argv[]) {
  SingletonTest *s = SingletonTest::get_instance();

  return 0;
}

我对这个问题的主要担忧是我的单例模式的析构函数没有被调用。
我可以改成使用(C++0x?)shared_ptr 的实例,这个方法很有效,但问题在于我的析构函数必须是公开的。
我可以添加一个静态的 'cleanup' 方法,但这会打开用户出错的可能性(即忘记调用它)。而且在面对(未处理的)异常时也不能进行适当的清理。
有没有一种常见的策略/模式,可以实现延迟实例化,自动调用析构函数,并仍然允许我保持析构函数私有?

为什么你想要在单例上调用析构函数? - Luchian Grigore
@Luchian Grigore:例如,它可以维护一些需要处理的外部资源。 - sharptooth
C++ Report杂志上有一篇名为“杀死Singleton”的好文章,作者是John Vlissides。你可以在这里找到全文:http://sourcemaking.com/design%255Fpatterns/to%255Fkill%255Fa%255Fsingleton。 - Gene Bushuyev
谢谢@Gene - 我会阅读的。 - sje397
6个回答

20

虽然不是直接回答,但这个答案对于评论来说太长了——为什么不用下面的方式实现单例:

class SingletonTest {
private:
  SingletonTest() {}
  ~SingletonTest() {
    std::cout << "Destructing!!" << std::endl;
  }

public:
  static SingletonTest& get_instance()  {
    static SingletonTest instance;
    return instance;
  }
};

现在你有一个在退出时将被析构的懒汉式单例...它的可重入性并不比你的代码差...


1
@sje397,我觉得这可能是我唯一会使用它们(静态局部变量)的时候,但通常最好的建议是尽可能避免全局变量... ;) - Nim
1
+1 我从不使用成员变量,而是使用静态局部变量。这也避免了多线程初始化的问题。 - Antonio Pérez
2
@Antonio - 怎么做?(我的意思是它如何避免多线程初始化问题,上面的代码不是线程安全的)。 - Nim
2
@Antonio:在C++03中,这是非标准的,但只是因为C++03标准没有提到线程。任何一个好的编译器都已经做到了线程安全。如果方法被内联(而不是在.cpp文件中定义),我在Windows(DLL)上遇到了这个惯用法的问题,因为每个DLL都有自己的局部静态变量...不过这是VC++03,所以事情可能已经改变了。 - Matthieu M.
我尝试过并发现~SingletonTest()必须是公共的,否则在gcc中无法编译。 - Deqing
显示剩余6条评论

2
你可以编写一个析构函数并在对象构造函数内调用atexit()进行注册。然后,当C++运行时取消初始化模块时,在main()之后的某个时间点调用你的析构函数。那个加粗和斜体是因为你对它的确切调用时间控制不太严格,这可能会导致析构顺序混乱 - 要小心。

这个函数可以是一个静态私有方法吗?否则它就像将析构函数设为公共的一样。 - sje397
@sje397:是的,如果它有匹配的签名。 - sharptooth

1

你可以将shared_ptr(或更适合的scoped_ptr)添加为友元,以允许其访问你的私有析构函数。

请注意,还有系统的atexit()函数,它可以注册一个在应用程序结束时调用的函数。你可以将你的单例的静态函数传递给它,该函数只需执行delete instance;

请注意,通常最好将要成为单例的类与其单例性分开。特别是在测试和/或需要双例时。 :)

顺便说一下,尽量避免懒惰初始化。在启动时按照确定的顺序初始化/创建你的单例。这样可以使它们正确关闭并解决依赖关系,避免出现意外情况。(我曾经陷入循环单例地狱...比你想象的要容易...)


我在某个地方读到过,'scoped_ptr' 在 c++0x 中被删除了...? - sje397
@Macke:我无法让朋友方法正常运行链接。我做错了什么? - Björn Pollex
1
@Björn:我的错。实际上是由checked_delete()执行删除操作,所以你需要将其设置为友元。查看错误消息指向的源代码。 - Macke
@Björn:你需要做类似这样的事情-->> "friend void boost::checked_delete<>( Test* x );" -- 然而,由于编译器不支持,如http://drdobbs.com/184403853所述,我只能选择-->> "template<class T> friend void boost::checked_delete(T * );" - pestophagous

1

你可以通过传入一个具有访问析构函数权限的删除器(例如作为SingletonTest成员定义的类)来使用带有私有析构函数的shared_ptr

然而,在销毁单例对象时,你需要非常小心,以确保它们在被销毁后不再被使用。为什么不直接使用普通的全局变量呢?


我想保持懒加载实例化。 - sje397

1
如果您将执行实际delete操作的类声明为友元(让它成为shared_ptr<SingletonTest>或某种默认删除器),则您的析构函数可以是私有的。虽然我不认为将其设置为私有是必要的。

将析构函数设为私有的原因与将构造函数设为私有的原因相同:以便对象的生命周期不受其他代码的影响。 - sje397
@sje397:这是一个高尚的目标,但我认为显而易见的是,由单例类返回的引用可能无法转换为指针并删除。如果使用您的API的人员做出这样的事情,您将很难使整个API对他们“安全”(特别是因为C++也允许在const对象上调用delete)。 - smerlin

0
第一个问题是:你是否想要单例被销毁。 销毁单例可能会导致销毁顺序问题;而且由于你正在关闭,析构函数不必要维护程序不变量。唯一需要运行单例的析构函数的时间是它管理系统不会自动清理的资源,例如临时文件。否则,最好不要调用它的析构函数。
鉴于此,如果您希望调用析构函数,则有两个选择:在instance函数中将单个对象声明为静态局部变量,或者使用std::auto_ptr或类似的东西,而不是原始指针作为指向它的指针。

2
叫我过于执着吧,但我讨厌把事情留给未完成或操作系统处理。我认为应该解决依赖关系方面的困难。 - sje397
@sje397 C++ 单例模式的整个目的在于解决初始化依赖关系的顺序。这意味着依赖关系可能是一个问题。你无法避免让操作系统处理很多事情;例如,它会回收你的内存(包括静态内存和代码所在的内存)。 - James Kanze
系统不会自动为您完成许多事情,例如停止生成的进程、正确终止线程、更新数据库、释放许可证、保存程序状态等等。 - Gene Bushuyev
@Gene Bushuyev 当然可以,但是有多少个单例处理这些问题呢?例如,在调用“exit”之前正确终止线程;您不能在静态析构函数中保存状态,因为许多状态已经消失了;而其他所有问题都与应用程序中的特定活动相关,并且不应由单例管理。 - James Kanze

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