【问题标题】:C++ singleton vs completely static objectC ++单例与完全静态对象
【发布时间】:2011-04-19 22:54:41
【问题描述】:

假设我们只需要在我们的项目中拥有某个类的一个实例。有几种方法可以做到这一点。

我想比较一下。请你复习一下我的理解。

1) 经典单例模式

2) 完全静态的类(所有方法和成员都是静态的)。


据我了解,差异如下:

a) 未定义跨不同单元的静态成员的初始化顺序。 因此,完全静态成员初始化不能使用来自其他模块的任何静态成员/函数。而单例则没有这个问题。

b) 我们必须为 Singleton 的 getInstance() 处理线程。但是,完全静态的类没有这个问题。

c) 对方法的访问看起来有点不同。 Foo::bar(); vs Foo::getInstance()->bar(); 一般情况下单例可以返回NULL,表示对象构造有问题,而静态类不能。

d) 类的定义看起来有点笨拙,有一堆静态类的静态变量。

我错过了什么吗?

【问题讨论】:

  • 然后有选项(3) 只需创建对象的一个​​实例。 真的,仅此而已。如果你只需要一个实例,为什么只创建一个实例这么难?使用 C++ 编写代码时,您创建的对象是否通常比您需要的多?
  • 单例是否会在连续调用getInstance()时返回不同的值?如果不是,那么关注 (c) 就没有实际意义,因为调用看起来不像 Foo::getInstance()->bar();;它看起来像块开头的单个 Foo* foo(Foo::getInstance()); 和之后的 foo->bar();
  • @James McNellis - 如果他真的对getinstance() 有线程问题,那么他在同步创建他的一个实例时也会遇到同样的问题。如果没有逻辑所有者,您将一个实例放在哪里?你可以创建一个,但是如果你这样做并且解决了创建线程问题,你基本上已经滚动了一个单例。
  • @T.E.D.你错过了重点:詹姆斯说没有必要尝试通过某种模式来强制唯一性,当你可以在某个地方创建一个对象而不强制任何东西(比如说,在main,在线程之前)并完成它。

标签: c++ class static singleton


【解决方案1】:

无论你称它为 Singleton 还是 Monostate 或任何花哨的名称……令人讨厌的本质是你有一个对象实例,并且对它进行了多次写入:全局变量,无论其伪装如何,都是 邪恶的

需要一个独特的实例的想法通常很笨拙。大多数时候,您真正需要的是共享相同实例的通信部分。但是另一组部件可以完美地使用另一个实例而不会出现问题。

任何声称需要全局变量的代码都是高度可疑的。使用它可能看起来更简单,但让我们面对现实吧,您可以完美地将对象传递给每个函数,这会使它们的签名复杂化,但它仍然可以工作。

但是,我承认,使用全局变量似乎更简单......直到您注意到问题:

  • 多线程受到威胁
  • 可测试性降低了,因为一项测试可能会影响随后的一项测试
  • 依赖性分析极其复杂:当您从子方法中拉入全局时,很难知道您的方法依赖于什么状态...

现在,就单例而言,多线程创建在 C++0x 之前的 C++ 中不可用(当可以使用静态局部变量时),因此您只需要在一个线程中创建它并延迟访问之前:实例化总的来说,这是你最好的选择。

破坏可能会造成混乱,因为 Singleton / Static 的生命可能会在其他人完成之前结束,然后它是未定义的行为。这是典型的Logger 单例。通常的策略是无耻地泄露...

在那之后,如果你还想要一个,祝你好运,这就是这个社区能为你做的一切。

【讨论】:

  • 大声笑。即使我们在 cmets 争论,对此 +1。完全同意。
  • +1:你的回复比我的好,所以我删除了。
  • 您使用 std::cout/std::cerr 还是将 iostream 传递给每个函数/对象以防它需要报告错误?
  • @Martin:我有一个我使用的 DiagnosticHandler 类;我实例化它的单个实例,并将指向它的引用或智能指针(取决于应用程序)传递给任何需要它的对象。它可能使用std::cout 来发出错误(它是高度可配置的),但该全局的使用对应用程序代码是隐藏的。它工作得很好,在我看来比使用全局变量要干净得多。
  • @Martin Becket:对于我的宠物项目,我肯定会这样做(在调试期间),否则我更喜欢引用,因为我可以简单地将输出重定向到文件。在工作中,我们使用日志库将消息发送到另一台服务器,并且可能依赖于某个地方的单例......但我不知道(或关心)因为我没有维护它。
【解决方案2】:

您忽略的另一个选项是namespace's。

namespace xyz {
namespace {
    int private_variable;
}

int get_pv() {
    return private_variable;
}
}

从功能上讲,这将类似于您的选项 #2,但您不能意外“删除”这个东西。您不能意外地创建它的实例。它只是相关的全球可访问数据和功能的集合。您甚至可以(如我的示例)拥有“私有”成员和函数。

当然用法是这样的:

int x = xyz::get_pv();

【讨论】:

    【解决方案3】:

    您可能会补充:静态对象可以抛出异常。可执行文件将无法启动,并且很难很好地调试/处理。

    【讨论】:

    • 如果天真实现很难调试。如果使用工厂或服务定位器模式正确完成,则在调试方面没有问题。
    • 在不需要的地方付出了很多努力。创建一个全局变量就完成了。
    【解决方案4】:

    您也可以使用 Borg 模式,这在 C++ 中会稍微复杂一些,除非您可以访问共享指针类。这个想法是可以实例化任意数量的 Borg 类,但它们的状态在所有实例之间共享。

    【讨论】:

    • 这也叫Monostate
    【解决方案5】:

    假设我们只需要在我们的项目中拥有某个类的一个实例。有几种方法可以做到这一点。

    更好的解决方案:

    作为参数传递给所有必需函数的 main 变量将是另一个变量。

    a) 未定义跨不同单元的静态成员的初始化顺序。因此,完全静态成员初始化不能使用来自其他模块的任何静态成员/函数。而单例则没有这个问题。

    如果单例的构造函数/析构函数访问其他全局静态生命周期变量,则确实存在此问题。

    b) 我们必须为 Sigleton 的 getInstance() 处理线程。但是,完全静态的类没有这个问题。

    真的不是问题吗?如果您知道它,只需在代码中添加适当的锁。

    c) 对方法的访问看起来有点不同。 Foo::bar(); vs Foo::getInstance()->bar();一般情况下,sigleton可以返回NULL来标识对象的构造有问题,而静态类不能。

    我会让我的 getInstance() 返回一个引用。那么指针是否为 NULL 就没有歧义了。它要么工作要么抛出异常。这也导致了在实例上调用正确销毁的设计(不要将此作为使用 Singleton 的建议,如果可能,我会避免使用它(但如果你确实使用使其整洁)。

    d) 类的定义看起来有点笨拙,有一堆静态类的静态变量。

    没有比正确编写单例更笨拙的了。

    这两种方法的问题在于它们都在访问global mutable state,因此其他对象对这些“单一实例”对象的使用对用户是隐藏的。这可能会导致测试出现问题(TDD 需要能够模拟外部功能,但global mutable state 会阻止测试人员(轻松地)模拟外部依赖项)。

    任何非 POD 的对象都有一个可能引发异常的构造函数。因此,对于全局命名空间中的对象,这意味着可以在进入 main() 之前抛出异常(这可能导致难以找到错误(如果您有很多全局对象(您必须在任何地方放置断点))。但是延迟评估的单例也存在同样的问题;如果在第一次使用时抛出,您如何纠正这个问题,以便后续尝试不会抛出?每次检索单例时,您的应用程序会继续抛出吗?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-17
      • 1970-01-01
      相关资源
      最近更新 更多