How it works...

The singleton pattern has been around in C++ for several years, and it is arguably one of the most controversial patterns in all of C++ as its global nature introduces coupling in your application (similar to how global variables introduce coupling). The singleton pattern implements a single, global resource. Specifically, it creates an object that maintains global scope, while ensuring no copies of itself can exist. The debate as to whether or not the singleton pattern should be used in your code will not be answered in this book as it depends on your use case, but let's at least cover some advantages and disadvantages of this pattern.

Advantages: The singleton pattern provides a clearly defined interface for global resources that can only contain a single instance. Whether we like it or not, global resources exist in all of our applications (for example, heap memory). If such a global resource is needed, and you have a mechanism to handle coupling (for example, a mocking engine such as Hippomocks), the singleton pattern is a great way to ensure the global resource is managed properly.

Disadvantages: The following are the disadvantages:

  • The singleton pattern defines a global resource, and like any global resource (for example, a global variable), any code that uses a singleton object becomes tightly coupled with the singleton. Coupling, in objected-oriented design, should always be avoided as it prevents the ability to fake a resource your code might depend on, which limits flexibility when testing.
  • The singleton pattern hides dependencies. When inspecting an object's interface, there is no way to determine that the object's implementation depends on a global resource. Most argue that this can be handled with good documentation.
  • The singleton pattern maintains its state throughout the lifetime of the application. This is especially true (that is, the disadvantage is obvious) when unit testing as the singleton's state carries from one unit test to the next, which most consider a violation of what a unit test is.

In general, global resources should always be avoided. Period. To ensure that your code is properly written to enforce the singleton design pattern, if and when you need a single global resource. Let's discuss the following example.

Suppose you are writing an application for an embedded device, and your embedded device has an additional memory pool that you can map into your application (for example, device memory for a video or network device). Now, suppose you can only ever have one of these additional memory pools and you need to implement a set of APIs to allocate memory from this pool. In our example, we will implement this memory pool using the following:

uint8_t memory[0x1000] = {};

Next, we will implement a memory manager class to allocate memory from this pool, as follows:

class mm
{
uint8_t *cursor{memory};

public:
template<typename T>
T *allocate()
{
if (cursor + sizeof(T) > memory + 0x1000) {
throw std::bad_alloc();
}

auto ptr = new (cursor) T;
cursor += sizeof(T);

return ptr;
}
};

As shown in the preceding code, we have created a memory manager class that stores a pointer to the memory buffer that contains our single, global resource. We then create a simple allocation function that handles this memory out as needed (with no ability to free, which keeps the algorithm really simple).

Since this is a global resource, we create the class globally as follows:

mm g_mm;

Finally, we can use our new memory manager as follows:

int main(void)
{
auto i1 = g_mm.allocate<int>();
auto i2 = g_mm.allocate<int>();
auto i3 = g_mm.allocate<int>();
auto i4 = g_mm.allocate<int>();

std::cout << "memory: " << (void *)memory << ' ';
std::cout << "i1: " << (void *)i1 << ' ';
std::cout << "i2: " << (void *)i2 << ' ';
std::cout << "i3: " << (void *)i3 << ' ';
std::cout << "i4: " << (void *)i4 << ' ';
}

In the preceding example, we allocate four integer pointers and then output the address of our memory block and the addresses of the integer pointers to ensure the algorithm is working as intended, resulting in the following output:

As shown in the preceding, the memory manager properly allocates memory as needed.

The problem with the preceding implementation is that the memory manager is just a class like any other, meaning it can be created as many times as we want as well as copied. To better demonstrate why this is a problem, let's look at the following example. Instead of creating one memory manager, let's create two:

mm g_mm1;
mm g_mm2;

Next, let's use both of these memory managers as follows:

int main(void)
{
auto i1 = g_mm1.allocate<int>();
auto i2 = g_mm1.allocate<int>();
auto i3 = g_mm2.allocate<int>();
auto i4 = g_mm2.allocate<int>();

std::cout << "memory: " << (void *)memory << ' ';
std::cout << "i1: " << (void *)i1 << ' ';
std::cout << "i2: " << (void *)i2 << ' ';
std::cout << "i3: " << (void *)i3 << ' ';
std::cout << "i4: " << (void *)i4 << ' ';
}

As shown in the preceding, the only difference is we are now using two memory managers instead of one. This results in the following output:

As shown in the preceding, memory has been double allocated, which will likely result in corruption and undefined behavior. The reason this occurs is the memory buffer itself is a global resource—something we cannot change. The memory manager itself does nothing to ensure this scenario cannot happen and, as a result, the user of this API might accidentally create a second memory manager. Note that, in our example, we explicitly created a second copy, but a second copy could occur by simply passing the memory manager around, inadvertently creating copies along the way.

To address this issue, we must handle two specific scenarios:

  • Creating more than one instance of the memory manager
  • Copying the memory manager

To address both of these issues, let's now show the singleton pattern:

class mm
{
uint8_t *cursor{memory};
mm() = default;

As shown in the preceding, we start with the constructor being marked as private. Marking the constructor as private prevents the use of the memory manager from creating their own instances of the memory manager. Instead, to get an instance of the memory manager, we will use the following public function:

    static auto &instance()
{
static mm s_mm;
return s_mm;
}

This preceding function creates a static (that is, global) instance of the memory manager and then returns a reference to this instance. Using this function, the user of the API can only get an instance of the memory manager from this function, which always returns only a reference to the globally defined resource. In other words, there is no ability to create additional instances of the class without the compiler complaining.

The last step to creating the singleton class is the following:

    mm(const mm &) = delete;
mm &operator=(const mm &) = delete;
mm(mm &&) = delete;
mm &operator=(mm &&) = delete;

As shown in the preceding, the copy and move constructors/operators are explicitly deleted. This addresses the second issue. By removing the copy constructor and operator, there is no ability to create a copy of the global resource, ensuring that the class only exists as a single global object.

To use this singleton class, we would do the following:

int main(void)
{
auto i1 = mm::instance().allocate<int>();
auto i2 = mm::instance().allocate<int>();
auto i3 = mm::instance().allocate<int>();
auto i4 = mm::instance().allocate<int>();

std::cout << "memory: " << (void *)memory << ' ';
std::cout << "i1: " << (void *)i1 << ' ';
std::cout << "i2: " << (void *)i2 << ' ';
std::cout << "i3: " << (void *)i3 << ' ';
std::cout << "i4: " << (void *)i4 << ' ';
}

This results in the following output:

If we attempt to create another instance of the memory manager ourselves, we would get an error similar to the following:

/home/user/book/chapter11/recipe02.cpp:166:4: error: ‘constexpr mm::mm()’ is private within this context
166 | mm g_mm;

Finally, since the singleton class is a single, global resource, we can create wrappers to remove the verbosity, as in the following:

template<typename T>
constexpr T *allocate()
{
return mm::instance().allocate<T>();
}

This change can be used as follows:

int main(void)
{
auto i1 = allocate<int>();
auto i2 = allocate<int>();
auto i3 = allocate<int>();
auto i4 = allocate<int>();

std::cout << "memory: " << (void *)memory << ' ';
std::cout << "i1: " << (void *)i1 << ' ';
std::cout << "i2: " << (void *)i2 << ' ';
std::cout << "i3: " << (void *)i3 << ' ';
std::cout << "i4: " << (void *)i4 << ' ';
}

As shown in the preceding, the constexpr wrapper provides a simple means to remove the verbosity of our singleton class, something that would be difficult to do if the memory manager wasn't a singleton.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset