How it works...

If you have ever read a book on C++, you have likely seen the apples and oranges example, which demonstrates how object-oriented programming works. The idea goes as follows:

  • An apple is a fruit.
  • An orange is a fruit.
  • An apple is not an orange but both are fruit.

This example is meant to teach how to organize your code into logical objects using inheritance. A logic that is shared by both an apple and an orange is written into an object called fruit while logic that is specific to an apple or an orange is written into the apple or orange objects that inherit from the base fruit object.

This example is also, however, showing how to extend the functionality of a fruit. By subclassing a fruit, I can create an apple that is capable of doing more than the fruit base class. This idea of extending the functionality of a class is common in C++, and oftentimes, we think of using inheritance to implement it. In this recipe, we will explore how to do this without the need for the apple or the orange to leverage inheritance with something called a delegate.

Suppose you are creating a game, and you wish to implement a battlefield where heroes and bad guys are fighting. At some point in your code, each hero in the battle will need to attack the bad guys. The problem is heroes come and go within the fight as they need time to recover, and so you really need to maintain a list of heroes that are capable of attacking the bad guys, and you simply need to loop through this dynamically changing list of heroes to see whether their attacks succeed or not.

Each hero could store a list of heroes that subclass a common base class and then run an attack() function that each hero overrides, but this would require the use of inheritance, which might not be desired. We could also use the type erasure pattern to wrap each hero and then store pointers to our wrapper's base class, but this would be specific to our attack() function, and we believe there will be other instances where these types of extensions will be needed.

Enter the delegate pattern, which is an extension to the type erasure pattern. With the delegate pattern, we can write code like the following:

int main(void)
{
spiderman s;
captain_america c;

std::array<delegate<bool(int, int)>, 3> heros {
delegate(attack),
delegate(&s, &spiderman::attack),
delegate(&c, &captain_america::attack)
};

for (auto &h : heros) {
std::cout << h(0, 42) << ' ';
}

return 0;
}

As shown in the preceding code snippet, we have defined an instance of two different classes that are not alike, and then we have created an array that stores three delegates. The delegate's template parameter takes a function signature of bool(int, int), while the delegate itself appears to be created from a function pointer as well as two member function pointers from the class instances we created earlier. We are then able to loop through each of the delegates and call them, effectively calling the function pointer and each member function pointer independently.

The delegate pattern provides the ability to encapsulate different callable objects into a single object with a common type that is capable of calling the callable objects so long as they share the same function signature. More importantly, delegates can encapsulate both function pointers and member function pointers, providing the user of the API with the ability to store a private state if needed.

To explain how this works, we will start simple and then build upon our example until we reach the final implementation. Let's start with a base class as follows:

template<
typename RET,
typename... ARGS
>
class base
{
public:
virtual ~base() = default;
virtual RET func(ARGS... args) = 0;
};

As shown in the preceding code snippet, we have created a template of a pure virtual base class. The template arguments are RET (which defines a return value) and ARGS... (which define a variadic list of arguments). We then create a function called func(), which takes our list of arguments and returns the template return type.

Next, let's define a wrapper that inherits from the base class using the type erasure pattern (if you have not read the previous recipe, please do so now):

template<
typename T,
typename RET,
typename... ARGS
>
class wrapper :
public base<RET, ARGS...>
{
T m_t{};
RET (T::*m_func)(ARGS...);

public:

wrapper(RET (T::*func)(ARGS...)) :
m_func{func}
{ }

RET func(ARGS... args) override
{
return std::invoke(m_func, &m_t, args...);
}
};

Just like the type eraser pattern, we have a wrapper class that stores an instance of our type and then provides a function that the wrapper can call. The difference is the function that can be called is not statically defined and instead is defined by the template arguments that are provided. Furthermore, we also store a function pointer with the same function signature, which is initialized by the wrapper's constructor and called in the func() function using std::invoke.

This additional logic, compared to the typical type erasure example, provides the ability to define any function signature that we wish to call from the object we are storing in the wrapper instead of defining it ahead of time (meaning the function we wish to call can be determined at runtime and not compile time).

We can then create our delegate class as follows:

template<
typename RET,
typename... ARGS
>
class delegate
{
std::unique_ptr<base<RET, ARGS...>> m_wrapper;

public:

template<typename T>
delegate(RET (T::*func)(ARGS...)) :
m_wrapper{
std::make_unique<wrapper<T, RET, ARGS...>>(func)
}
{ }

RET operator()(ARGS... args)
{
return m_wrapper->func(args...);
}
};

As with the type erasure pattern, we store a pointer to the wrapper, which is created from the constructor of the delegate. The important detail to recognize here is the T type is not defined in the delegate itself. Instead, the T type is only known during the construction of the delegate which is used to create an instantiation of the wrapper. This means that each instance of a delegate is the same, even if the delegate is storing a wrapper that wraps different types. This allows us to use the delegate as follows.

Suppose we have two heroes that do not share a common base, but do provide an attack() function with the same signature:

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

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

We can leverage our delegate class to store an instance of our hero classes and call their attack functions as follows:

int main(void)
{
std::array<delegate<bool, int, int>, 2> heros {
delegate(&spiderman::attack),
delegate(&captain_america::attack)
};

for (auto &h : heros) {
std::cout << h(0, 42) << ' ';
}

return 0;
}

This results in the following output:

Although we have already made significant progress in creating our delegate (it at least works), there are a few issues with this early implementation:

  • The delegate's signature is bool, int, int, which is misleading as we really want a function signature such as bool(int, int) so that the code is self-documenting (the delegate's type is a single function signature, not three different types).
  • This delegate cannot handle functions marked const.
  • We have to store an instance of the delegated object inside our wrapper, which prevents us from creating delegates to more than one function for the same object.
  • We do not have support for non-member functions.

Let's address each of these.

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

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