How it works...

One of the oldest and most widely used features of C++ is C++ templates. Like inheritance, C++ templates are not generally described as a form of type erasure, but they are. Type erasure is nothing more than the act of removing or, in this case, ignoring type information.

Unlike the C language, however, type erasure in C++ generally attempts to avoid removing type information in favor of working around a type's strict definition while retaining type safety. One way to accomplish this is through the use of C++ templates. To better explain this, let's start with a simple example of a C++ template:

template<typename T>
T pow2(T t)
{
return t * t;
}

In the preceding example, we have created a simple function that calculates the power of two for any given input. For example, we can call this function as follows:

std::cout << pow2(42U) << '
'
std::cout << pow2(-1) << ' '

When the compiler sees the use of the pow2() function, it automatically generates the following code for you (behind the scenes):

unsigned pow2(unsigned t)
{
return t * t;
}

int pow2(int t)
{
return t * t;
}

As shown in the preceding code snippet, the compiler creates two versions of the pow2() function: a version that takes an unsigned value and returns an unsigned one, and a version that takes an integer and returns an integer. The compiler created these two versions because the first time we used the pow2() function, we provided it with an unsigned value, while the second time we used the pow2() function, we provided it with int.

As far as our code is concerned, however, we don't actually care what type the function is provided, so long as the type that is provided can successfully execute operator*(). In other words, both the user of the pow2() function and the author of the pow2() function are safely ignoring (or erasing) the type information that is passed to and returned from the function from a conceptual point of view. The compiler, however, is very much aware of the types that are being provided and must safely handle each type as needed.

This form of type erasure performs the erasure at the specification of the API, and in C++, this specification is called a concept. Unlike most APIs that dictate both input and output types (for example, the sleep() function takes an unsigned integer and only an unsigned integer), a concept specifically ignores the type in favor of defining, instead, what properties a given type must provide.

For example, the preceding pow2() function has the following requirements:

  • The provided type must either be an integer type or provide an operator *().
  • The provided type must be either copy-constructible or move-constructible.

As shown in the previous code snippet, the pow2() function doesn't care what type it is given so long as the type provided meets certain minimum requirements. Let's examine a more complicated example to demonstrate how C++ templates can be used as a form of type erasure. Suppose we have two different heroes that are fighting a bad guy, and each hero provides the ability to attack the bad guy, which is shown with the following code:

class spiderman
{
public:
bool attack(int x, int) const
{
return x == 0 ? true : false;
}
};

class captain_america
{
public:
bool attack(int, int y) const
{
return y == 0 ? true : false;
}
};

As shown in the preceding code snippet, each hero provides the ability to attack a bad guy, but neither hero shares anything in common other than the fact that both happen to provide an attack() function with the same function signature. We also do not have the ability to add inheritance to each hero (maybe our design cannot handle the extra vTable overhead that inheritance adds, or maybe the hero definition is provided to us).

Now suppose we have a complicated function that must call the attack() function for each hero. We could write the same logic for each hero (that is, manually duplicate the logic), or we could write a C++ template function to handle this for us, which is shown as follows:

template<typename T>
auto attack(const T &t, int x, int y)
{
if (t.attack(x, y)) {
std::cout << "hero won fight ";
}
else {
std::cout << "hero lost the fight :( ";
}
}

As shown in the preceding code snippet, we can leverage the type erasing properties of C++ templates to encapsulate our attack logic into a single template function. The preceding code doesn't care about what type it is provided so long as the type provides an attack() function that takes two integer types and returns an integer type (preferably bool, but any integer would work). In other words, so long as the type provided adheres to an agreed-upon concept, this template function will work, providing the compiler with a means to handle the type-specific logic for us.

We can call the preceding function as follows:

int main(void)
{
attack(spiderman{}, 0, 42);
attack(captain_america{}, 0, 42);

return 0;
}

This results in the following output:

Although this example shows how C++ templates can be used as a form of type erasure (at least for a specification to create a concept), when type erasure is discussed, there is a specific pattern called the type erasure pattern or just type erasure. In the next recipe, we will explore how we can leverage what we have learned in the first two recipes to erase type information generically while still supporting simple things such as containers.

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

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