Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Sharp Tools: A .NET like Event in C++

4.81/5 (18 votes)
10 Jan 2016CPOL4 min read 23.6K   799  
This tip presents a .NET like Event implemented in C++, with which developers can fire events in C++ the same way they do in C#.

Introduction

The Event class presented here is extremely useful for decoupling your classes from each other. Once you start using this class, all your code will naturally become event-driven. Event-driven programming has the advantage of making code highly expandable, readable and maintainable with no effort or extra lines of code. On the contrary, it allows you to achieve the same results with less code.

I might implement an event class for Java too. However, the Java version would be a little bit different as without operator overloading support, I would have to define functions like addObserver/removeObserver.

Background

The class design is based on the observer design pattern.

The Design

I will take a top-down approach in explaining how an event class can be designed. First, the interface we'd expect from a .NET like event is a class that can add/remove handlers and raise an event.

C++
class Event
{
public:
  void operator()();  // to be used to raise the event
  Event& operator += (EventHandler* pHandlerToAdd);  // to be used to add event handlers
  Event& operator -= (EventHandler* pHandlerTorRemove);  // to be used to remove event handlers
};

Simple enough, but this design asks the user to create a middle object of type EventHandler. It's better to provide the user with an automatic way with which he ONLY needs to supply his handler function. This is why we'll be adding a class with static methods that create handlers from the user functions.

C++
class EventHandler
{
public:
  static EventHandler* Bind(void(*nonMemberFunctionToCall)());
};

Using these static methods, now the user can add his functions as handlers without realizing an intermediate object is being created.

C++
myEvent += EventHandler::Bind(myFunc);  // sample usage of the new interface

Memory-wise, we can opt to use smart pointers or make the event class own the memory of assigned handlers.

Now what's missing is that the user would want to have a member function as a handler. So, we'll add an overload of Bind() for that purpose.

C++
static EventHandler* Bind(void(ClassType::*memberFunctionToCall)(), ClassType* instance);

In C++, an instance of the class is needed in order to call a member function, but this is not the biggest of our problems. The big problem is that we can't create a version of Bind function for each class we intend to use events with, or can we?

Actually we can, and that's by making the new Bind() function a template function.

C++
template<typename T> static EventHandler* Bind(void(T::*memberFunctionToCall)(), T* instance);

This would seem perfect, but as always, the devil is in the details; The EventHandler created by Bind() would need to save the passed pointer, which is of an unknown (template) type.

One solution would be to make EventHandler a template class.

C++
// showing a wrong way to solve the issue
template<typename T> class EventHandler
{
public:
  ...
private:
  T* instance;
  void(T::*memberFunctiontoCall)();
};

But then operator += of Event class won't know what to expect as its argument.

C++
//Event& operator += (EventHandler< ??? >* pHandlerToAdd);

Yes, it is as you've guessed. The trivial solution of using inheritence. So the event class would keep expecting a pointer to the base EventHandler class while the Bind function would create an inherited version.

C++
// An inherited version of EventHandler
template<typename T> class EventHandlerImpl: public EventHandler
{
public:
  EventHandlerImpl(void(T::*memberFunctionToCall)(), T* instance);  // constructor
  ...
};
C++
// showing how the Bind function would create an inherited version depending on passed arguments type
class EventHandler
{
public:
  template<typename T> static EventHandler* Bind(void(T::*memberFunctionToCall)(), T* instance)
  {
    return new EventHandlerImpl(memberFunctionToCall, instance);
  }
  ...

Finally, to raise the event, some method must be called on that base EventHandler class. This call has to be virtual as Bind() would always create us an EventHandler object of an inherited class.

C++
class EventHandler
{
public:
  ...
  virtual void RaiseEvent();

and then operator() of class Event would call that virtual function through the pointer it keeps to each handler added.

The final version of the code was tuned a bit and is provided in the attached Event.h file.

Using the Code

When you design your app, you would first decide on your application-level components that perform the business logic. Each of these would have its own separate responsibilities and it would communicate with other components through events.

Shared functionality would be shared through making utility-level classes and injecting them to the application-level components (dependency injection). But this topic is out of the scope of this tip.

C++
#include "Event.h"  // This lib consists of only one file, just copy it and include it in your code.

// an example of an application-level component which perform part of the business logic
class Cashier
{
public:
  Sharp::Event<void> ShiftStarted;  // an event that pass no argument when raised
  Sharp::Event<int> MoneyPaid;  // will be raised whenever the cashier receives money

  void Start();  // called on startup, perform business logic and eventually calls ProcessPayment()

private:
  // somewhere along the logic of this class
  void ProcessPayment(int amount)
  {
    // after some processing
    MoneyPaid(amount);  // this how we raise the event
  }
};

// Another application-level component
class Accountant
{
public:
  void OnMoneyPaid(int& amount);
};

// The main class that decide on the events flow (who handles which events)
// it is also the owner of the application-level components
class Program
{
  // the utility level components(if any)
  //(commented)DataStore databaseConnection;

  // the application-level components
  Cashier cashier1;
  Accountant accountant1;
  //(commented)AttendanceManager attMan(&databaseConnection) // an example of injecting a utility object

public:
  Program()
  {
    // connect the events of the application-level components to their handlers
    cashier1.MoneyPaid += Sharp::EventHandler::Bind( &Accountant::OnMoneyPaid, &accountant1);
  }
  
  ~Program()
  {
    // it is recommended to always connect the event handlers in the constructor 
    // and disconnect in the destructor
    cashier1.MoneyPaid -= Sharp::EventHandler::Bind( &Accountant::OnMoneyPaid, &accountant1);
  }

  void Start()
  {
    // start business logic, maybe tell all cashiers to start their shift
    cashier1.Start();
  }
};

void main()
{
  Program theProgram;
  theProgram.Start();

  // as you can see the Cashier and the Accountant know nothing about each other
  // You can even remove the Accountant class without affecting the system
  // You can add new components (ex: AttendanceManager) without affecting the system
  // all you need to change is the part where you connect/disconnect the events
}

Build Notes

  • This lib consists of only one file: "Event.h". Just copy it and include it in your code.
  • You can define SHARP_EVENT_NO_BOOST in order to have no dependencies and manage thread-safety at the application level.

Points of Interest

  • Event.h is well-documented in the hope that it would help someone out there learn more about templates, virtual functions and threading in general, as it provides a good example of mixing these technologies.
  • A note about the design: In C#, you would need to define a special version of EventArgs class in order to pass event data. While I understand why the language designers opted to do so, I'm not following the same strategy here and I'm relying on the C++ developer to be wise enough to limit the event class usage to passing actual events only.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)