昨天我问了一个有关单例和模板(Meta programming with singleton)的问题,这引发了有关单例使用的广泛争论。我个人不喜欢它们,但是对于我的特殊问题,我找不到其他选择。我想描述我的问题,并希望就如何创建可靠的解决方案提供反馈。

背景:我正在使用的软件已经存在15年了,它涵盖了多个exe和dll(适用于Windows);我已经做了6个月了。

我有一个类,Foo在共享库中。 Foo是一个寿命非常短(约5秒)的对象,可以在任何线程,任何进程以及任何时间创建。我现在用新功能扩展Foo,并且要求在执行任何Foo对象之前必须运行名为FooInit()的函数,并在进程退出时运行FooDestroy()。

问题是,创建Foo对象是任意的-代码的任何部分都可以并且确实可以调用:

Foo* foo = new Foo();

Foo的ctor中的boost::call_once适用于FooInit(),但是并不能帮助我解决调用FooDestroy()的问题。引用计数Foo并没有帮助,因为在任何时候内存中可能都有[0,n]并有更多要创建的内容,因此当计数变为0时,我无法调用FooDestroy()。

我当前的解决方案是在Foo的ctor中创建和使用“FooManager”单例。单例将调用FooInit(),并在将来的某个时候最终调用FooDestroy()。我倾向于此解决方案,因为它似乎是最安全,风险最低的解决方案。

任何反馈表示赞赏。

最佳答案

如果绝对不能多次调用FooInit()FooDestroy()是绝对关键的,并且您与受虐狂程序员合作,他们不得不忍受自己的脚步,则需要使用FooInit()将函数本身写为等幂的注册FooDestroy()以在程序终止时通过std::atexit()调用。

另一方面,如果无法修改FooInit()FooDestroy(),并且您的同事仍然双脚,则有一些替代方案。在深入研究它们之前,让我们简要检查一些经常针对单例的论点:

  • 违反了single responsibility principleFooManager负责通过RAII调用结构上的FooInit()和销毁上的FooDestroy()。将其设置为单例后,FooManager将承担控制其创建和生命周期的额外责任。
  • 它们带有状态。单元测试可能会变得困难,因为一个单元测试可能会影响另一单元测试。
  • 它隐藏依赖项,因为可能会全局访问单例。

  • 一种解决方案是使用dependency injection。通过这种方法,Foo的构造函数将被修改为接受FooManager,而FooManager将:
  • 提供幂等的init()方法,该方法仅调用一次FooInit()
  • 在销毁期间调用FooDestroy()

  • 依赖性变得很明显,并且FooManager的生存期控制了FooDestroy()的调用时间。为了使示例简单,我选择不介绍线程安全性,但这是一个基本示例,其中通过使用作用域管理FooManager的生存期来重置单元测试之间的状态:
    #include <iostream>
    #include <boost/noncopyable.hpp>
    
    // Legacy functions.
    void FooInit()    { std::cout << "FooInit()"    << std::endl; }
    void FooDestroy() { std::cout << "FooDestroy()" << std::endl; }
    
    /// @brief FooManager is only responsible for invoking FooInit()
    ///        and FooDestroy().
    class FooManager:
      private boost::noncopyable
    {
    public:
    
      FooManager()
        : initialized_(false)
      {}
    
      void init()
      {
        if (initialized_) return; // no-op
        FooInit();
        initialized_ = true;
      }
    
      ~FooManager()
      {
        if (initialized_)
          FooDestroy();
      }
    
    private:
      bool initialized_;
    };
    
    /// @brief Mockup Foo type.
    class Foo
    {
    public:
      explicit Foo(FooManager& manager)
      {
        manager.init();
        std::cout << "Foo()" << std::endl;
      }
    
      ~Foo()
      {
        std::cout << "~Foo()" << std::endl;
      }
    };
    
    int main()
    {
      // Some unit test that creates Foo objects.
      std::cout << "Unit Test 1" << std::endl;
      {
        FooManager manager;
        Foo f1(manager);
        Foo f2(manager);
      }
    
      // State is not carried between unit test.
    
      // Some other unit test that creates Foo objects.
      std::cout << "Unit Test 2" << std::endl;
      {
        FooManager manager;
        Foo f3(manager);
      }
    }
    

    产生以下输出:

    Unit Test 1
    FooInit()
    Foo()
    Foo()
    ~Foo()
    ~Foo()
    FooDestroy()
    Unit Test 2
    FooInit()
    Foo()
    ~Foo()
    FooDestroy()
    

    如果修改Foo的构造并控制FooManager的生存期以及如何传递它会带来太大的风险,那么折衷办法可能是通过全局隐藏依赖项。但是,为了划分职责并避免携带状态,可以通过其他类型(例如智能指针)来管理全局可用的FooManager的生存期。在下面的代码中:
  • FooManager负责在构造时调用FooInit(),在破坏时调用FooDestroy()
  • FooManager的创建通过辅助功能进行管理。
  • FooManager的生存期由全局智能指针管理。

  • #include <iostream>
    #include <boost/noncopyable.hpp>
    #include <boost/scoped_ptr.hpp>
    
    // Legacy functions.
    void FooInit()    { std::cout << "FooInit()"    << std::endl; }
    void FooDestroy() { std::cout << "FooDestroy()" << std::endl; }
    
    namespace detail {
    
    /// @brief FooManager is only responsible for invoking FooInit()
    ///        and FooDestroy().
    class FooManager
      : private boost::noncopyable
    {
    public:
      FooManager()  { FooInit();    }
      ~FooManager() { FooDestroy(); }
    };
    
    /// @brief manager_ is responsible for the life of FooManager.
    boost::scoped_ptr<FooManager> manager;
    
    /// @brief Initialize Foo.
    void init_foo()
    {
      if (manager) return; // no-op
      manager.reset(new FooManager());
    }
    
    /// @brief Reset state, allowing init_foo() to run.
    void reset_foo()
    {
      manager.reset();
    }
    
    } // namespace detail
    
    /// @brief Mockup Foo type.
    class Foo
    {
    public:
      Foo()
      {
        detail::init_foo();
        std::cout << "Foo()" << std::endl;
      }
    
      ~Foo()
      {
        std::cout << "~Foo()" << std::endl;
      }
    };
    
    int main()
    {
      // Some unit test that creates Foo objects.
      std::cout << "Unit Test 1" << std::endl;
      {
        Foo f1;
        Foo f2;
      }
    
      // The previous unit test should not pollute other unit test.
      detail::reset_foo();
    
      // Some other unit test that creates Foo objects.
      std::cout << "Unit Test 2" << std::endl;
      {
        Foo f3;
      }
    }
    

    产生与第一个示例相同的输出。

    综上所述,单例和其他解决方案都不能阻止FooInit()多次调用,但是它们都提供了一种在程序终止时调用FooDestroy()的方法。虽然单例可以为当前问题提供安全且低风险的解决方案,但这样做是有代价的。单例的后果可能会比其他解决方案产生更多的技术债务,并且可能需要偿还该债务以解决 future 的问题。

    关于c++ - 任意对象的单例替代,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/21163490/

    10-10 13:20