2

Yesterday I asked a question regarding singletons and templates (Meta programming with singleton), and that kicked off quite a debate about singleton use. I'm personally not a fan of them, but for my particular problem I can't figure out an alternative. I'd like to describe my problem and would love feedback on ways I can create a robust solution.

Background: The software I'm working on has been around for 15 years and spans multiple exe's and dll's (it's for Windows); I've been working on it for 6 months.

I have a class, Foo that's in a shared library. Foo is an object with a very small lifetime (~5 seconds), and can be created in any thread, any process, and at any time. I'm now extending Foo with new functionality, and a requirement is a function called FooInit() must be run before any Foo objects can be executed, and FooDestroy() when the process exits.

The problem is, creation of Foo objects is arbitrary - any part of the code can, and does, call:

Foo* foo = new Foo();

boost::call_once inside Foo's ctor works for FooInit(), but doesn't help me solve the problem of calling FooDestroy(). Referencing counting Foo doesn't help, because there may be [0,n] in memory at any one time with more to be created, so I can't call FooDestroy() when the count gets to 0.

My current solution is to create and use a "FooManager" singleton inside of Foo's ctor. The singleton will call FooInit(), and at some time in the future FooDestroy() will eventually be called. I'm leaning towards this solution because it appears to be the safest and lowest risk.

Any feedback is appreciated.

Community
  • 1
  • 1
BigHands79
  • 229
  • 1
  • 18
  • To me it seems that the problem isn't so much about when to call `FooInit`, that is pretty straight-forward. The problem is when to call `FooDestroy`. Except on program shutdown, will there ever be a safe point where it can be called, where you know there won't be any more `Foo` instances created? – Some programmer dude Jan 16 '14 at 13:55
  • @Joachim No, each process is different and there is no safe point where I know no more Foo instances will be created. – BigHands79 Jan 16 '14 at 13:57
  • The classic solution to replacing singletons is dependency injection. Can you modify the Foo constructor to accept a `FooInitContext` (something returned by FooInit, whatever that is) as an input parameter? – utnapistim Jan 16 '14 at 14:13
  • @utnapistim I could, but then what calls FooInit, and what guarantees it's only called once. Moreover, where is FooDestroy() called? – BigHands79 Jan 16 '14 at 14:20
  • I don't get why the `Foo` destructor cannot itself call `FooDestroy()`. Or is it `FooDestroy` that actually destroys the object? (I mean that would make sense given the name) – John Dibling Jan 16 '14 at 14:21
  • @John My example function name was poor. FooDestroy() cleans up from FooInit(). FooInit() creates multiple resources that must persist for the duration of the process, and Foo objects require those resources. – BigHands79 Jan 16 '14 at 14:23
  • I see. So `FooDestroy()` is actually only called once during the program's run, at shutdown time? – John Dibling Jan 16 '14 at 14:24
  • @John Yes, that's correct. – BigHands79 Jan 16 '14 at 14:26
  • Gotcha. Ok, I think I can provide an idea for you now. – John Dibling Jan 16 '14 at 14:26
  • Can you change the definition of `Foo`? – John Dibling Jan 16 '14 at 15:43
  • @John Yes, I can change the definition of Foo – BigHands79 Jan 16 '14 at 16:00

1 Answers1

2

If it is absolutely critical that the FooInit() and FooDestroy() must not be invoked multiple times, and you work with a masochists programmer who cannot help but shoot his or herself in the foot, then the functions themselves need to be written to be idempotent, with FooInit() registering FooDestroy() to be invoked upon program termination via std::atexit().

On the other hand, if FooInit() and FooDestroy() cannot be modified, and your coworkers still have both feet, then there are a few alternatives. Before delving into them, lets briefly examine some arguments often made against singletons:

  • It is the violation of the single responsibility principle. FooManager is responsible for invoking FooInit() on construction and FooDestroy() on destruction via RAII. Upon making it a singleton, FooManager would have the additional responsibility of controlling its creation and lifecycle.
  • They carry state. Unit testing can become difficult, as one unit test may affect a different unit test.
  • It hides dependencies as the singleton may be globally accessed.

One solution is to use dependency injection. With this approach, Foo's constructor would be modified to accept FooManager, and FooManager would:

  • Provide an idempotent init() method that would only invoke FooInit() once.
  • Invoke FooDestroy() during destruction.

The dependencies become explicit, and the lifetime of FooManager controls when FooDestroy() is invoked. To keep the examples simple, I have opted to not cover thread safety, but here is a basic example where state is reset between unit test by managing FooManager's lifetime with scope:

#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);
  }
}

Which produces the following output:

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

If modifying Foo's construction and controlling FooManager's lifetime and how it is passed around creates too much risk, then a compromise may be to hide the dependency via a global. However, to split responsibilities and avoid carrying state, the globally available FooManager's lifetime can be managed by another type, such as a smart pointer. In the following code:

  • FooManager is responsible for invoking FooInit() upon construction and FooDestroy() upon destruction.
  • FooManager's creation is managed via an auxiliary function.
  • FooManager's lifetime is managed with a global smart pointer.
#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;
  }
}

Which produces the same output as the first example.

With all that said, neither a singleton nor other solutions prevent FooInit() from being invoked multiple times, but they all provide a way for FooDestroy() to be invoked upon program termination. While a singleton may provide a safe and low risk solution for the current problem, it comes at a cost. The consequences of a singleton may create more technical debt than other solutions, and this debt may need to be payed off to solve future problems.

Tanner Sansbury
  • 48,187
  • 8
  • 101
  • 156