c++ - 任意对象的单例替代方案

标签 c++ boost

昨天我问了一个关于单例和模板的问题( 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/

相关文章:

c++ - 用指针反转字符串是如何工作的

c++ - Windows 中的原始套接字实现?

c++ - 拥有一个整数矩阵 MxN 如何将它们分组为具有 boost 几何形状的多边形?

c++ - 为 char 数组添加 Boost 文件系统路径

c++ - 如何将谓词传递给算法

c++ - 转换为联编文件

c++ - knuth morris pratt + boost + circular_buffer

c++ - 免费功能和单元测试

c++ - Boost C++ 1.60 中的 `gregorian has not been declared` 错误

c++ - Linux 有没有办法使用 C/C++ 永久设置 IP 地址?