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.