Table of Contents
Introduction
When developing our applications, we sometimes need a way for performing some tasks when something occurs. For that purpose, we usually want to register the needed tasks to some place and, invoke them when we got the notification about the relevant thing. Some frameworks already have built-in solutions for that need (like: .NET events, Qt signals). But, in my case, I had to implement that behavior using standard C++ only. Since I saw that I repeat the same pattern each time I want to implement it, I thought that it can be good if we'll have a generic solution for this common need.
This article shows how we can implement a thread-safe solution, that can be used in a similar manner of the .NET events, using the standard C++ library.
Background
Since the usage of our solution is similar to the usage of the .NET events, we use the same terminology:
- event handler: A holder for an actual function that should be called when we raise the notification.
- event: A holder for a number of handlers. An event can be called for raising a notification (and call its handlers).
This article assumes a basic familiarity with the C++ language and the standard C++ library.
How It Works
Holding a Handler Function
In our solution, we have to provide a way for defining event-handlers for differnet events. Since any event handler function can be with a different signature, we need a way for defining the different arguments for any event type. In order to achieve that goal, we create a variadic class template. A variadic template is a template that its arguments can be varied between the template's instances. In order to make it possible, C++11 brings us the Parameter pack. Using the ellipsis operator, we can declare and expand our variadic template's arguments.
For our template, we use the parameter pack to define our handler function's arguments types:
template <typename... Args> class event_handler
{
};
For holding the function of the event-handler, we use a std::function object. The std::function
definition is composed from an undefined class template that takes a single template argument and, a partial template specialization that takes one template argument for the function's return type and a parameter-pack for the function's arguments types. The single template argument is defined as a function type, using the function's return type and the function's arguments types.
In our case, the function's return type is always void
. Using void
as the function's return type and the template's parameter-pack as the function's arguments types, we can define our handler's function holder:
typedef std::function<void(Args...)> handler_func_type;
handler_func_type m_handlerFunc;
For calling our function, we add an appropriate function call operator:
void operator()(Args... params) const
{
if (m_handlerFunc)
{
m_handlerFunc(params...);
}
}
Since an event can hold some event-handlres, we need a way for identifying each event-handler. For that purpose, we add a data-member for holding the handler's identification number. In order to make it thread-safe, we use an atomic type:
template <typename... Args> class event_handler
{
public:
typedef unsigned int handler_id_type;
explicit event_handler(const handler_func_type& handlerFunc)
: m_handlerFunc(handlerFunc)
{
m_handlerId = ++m_handlerIdCounter;
}
bool operator==(const event_handler& other) const
{
return m_handlerId == other.m_handlerId;
}
handler_id_type id() const
{
return m_handlerId;
}
private:
handler_id_type m_handlerId;
static std::atomic_uint m_handlerIdCounter;
};
template <typename... Args> std::atomic_uint event_handler<Args...>::m_handlerIdCounter(0);
Holding Some Event-handlers Together
Typically, when using events, we want to publish a notification about something that happened and, let some subscribers to subscribe with their needed implementations. For that purpose, we need a way to join some event-handlers together and, call all of them when a thing (an event) has happened. We can do that by adding another variadic class template:
template <typename... Args> class event
{
public:
typedef event_handler<Args...> handler_type;
protected:
typedef std::list<handler_type> handler_collection_type;
private:
handler_collection_type m_handlers;
};
In the event
class, we hold a collection of event-handler objects. Since we want our event to be thread-safe, we use mutex protection for the collection's operations:
typename handler_type::handler_id_type add(const handler_type& handler)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
m_handlers.push_back(handler);
return handler.id();
}
inline typename handler_type::handler_id_type add
(const typename handler_type::handler_func_type& handler)
{
return add(handler_type(handler));
}
bool remove(const handler_type& handler)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
auto it = std::find(m_handlers.begin(), m_handlers.end(), handler);
if (it != m_handlers.end())
{
m_handlers.erase(it);
return true;
}
return false;
}
bool remove_id(const typename handler_type::handler_id_type& handlerId)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
auto it = std::find_if(m_handlers.begin(), m_handlers.end(),
[handlerId](const handler_type& handler) { return handler.id() == handlerId; });
if (it != m_handlers.end())
{
m_handlers.erase(it);
return true;
}
return false;
}
mutable std::mutex m_handlersLocker;
Calling the Event Handlers
After we have our event class, we can add a function for calling its event-handlers. Since our event can be used in some threads simultaneously and, we don't want to block other threads from using the event (add and remove handlers, call the event) until all the event-handlers have finished their implementations, we lock the mutex only for getting a copy of the event-handlers. Then, we go over the copied handlers and call them without locking. That is done as follows:
void call(Args... params) const
{
handler_collection_type handlersCopy = get_handlers_copy();
call_impl(handlersCopy, params...);
}
void call_impl(const handler_collection_type& handlers, Args... params) const
{
for (const auto& handler : handlers)
{
handler(params...);
}
}
handler_collection_type get_handlers_copy() const
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
return m_handlers;
}
In order to make our event more convenient (and more like a C# event), we wrap the add
, remove
and, call
functions with appropriate operators:
inline void operator()(Args... params) const
{
call(params...);
}
inline typename handler_type::handler_id_type operator+=(const handler_type& handler)
{
return add(handler);
}
inline typename handler_type::handler_id_type
operator+=(const typename handler_type::handler_func_type& handler)
{
return add(handler);
}
inline bool operator-=(const handler_type& handler)
{
return remove(handler);
}
Sometimes, we don't want to wait to all the event-handlers to be completed. We just want to raise an event and move on. For that purpose, we add another function for calling our event-handlers asynchronously:
std::future<void> call_async(Args... params) const
{
return std::async(std::launch::async, [this](Args... asyncParams)
{ call(asyncParams...); }, params...);
}
Finally, in order to make our event copyable and movable, we add appropriate copy constructor, copy assignment operator, move constructor and, move assignment operator:
template <typename... Args> class event_handler
{
event_handler(const event_handler& src)
: m_handlerFunc(src.m_handlerFunc), m_handlerId(src.m_handlerId)
{
}
event_handler(event_handler&& src)
: m_handlerFunc(std::move(src.m_handlerFunc)), m_handlerId(src.m_handlerId)
{
}
event_handler& operator=(const event_handler& src)
{
m_handlerFunc = src.m_handlerFunc;
m_handlerId = src.m_handlerId;
return *this;
}
event_handler& operator=(event_handler&& src)
{
std::swap(m_handlerFunc, src.m_handlerFunc);
m_handlerId = src.m_handlerId;
return *this;
}
};
template <typename... Args> class event
{
event(const event& src)
{
std::lock_guard<std::mutex> lock(src.m_handlersLocker);
m_handlers = src.m_handlers;
}
event(event&& src)
{
std::lock_guard<std::mutex> lock(src.m_handlersLocker);
m_handlers = std::move(src.m_handlers);
}
event& operator=(const event& src)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
std::lock_guard<std::mutex> lock2(src.m_handlersLocker);
m_handlers = src.m_handlers;
return *this;
}
event& operator=(event&& src)
{
std::lock_guard<std::mutex> lock(m_handlersLocker);
std::lock_guard<std::mutex> lock2(src.m_handlersLocker);
std::swap(m_handlers, src.m_handlers);
return *this;
}
};
How to Use It
Demo Helpers and Environment Settings
Support C++11 and Threads in Eclipse Project
For developing our examples (under Linux), we use the eclipse environment. Since we use C++11 in our code, we have to support it in our project. We can do that by adding -std=c++11
to the compiler settings in the project's properties:
For making eclipse index to recognize c++11, in the GNU C++ symbols (in the project's properties), we can:
- Add the
__GXX_EXPERIMENTAL_CXX0X__
symbol (with an empty value):
- Change the value of the
__cplusplus
symbol to 201103L
(or a greater value):
Since we use threads in our project, we add pthread
to the projectcs linked libraries:
A Helper Class for Colored Console Prints
Since we have some colored console prints in our examples, we add a helper class for simplifying this operation:
template <typename ItemT, typename CharT> class colored
{
};
The colored
class takes two template arguments. The first one is for the printed item type and, the second one is for the output-stream's characters type.
In spite of the fact that in our examples we use this helper class only for printing strings and numbers, it's implemented to be more efficient for other complex types too. So, since the use of our colored
class is only for temporary console prints, we can restrict the use of it to be only for temporary objects:
template <typename ItemT, typename CharT> class colored
{
public:
colored(const CharT* colorStr, const ItemT& item)
: m_colorStr(colorStr), m_item(item)
{
}
colored(const colored& other) = delete;
colored& operator=(const colored& other) = delete;
colored(colored&& other)
: m_colorStr(other.m_colorStr), m_item(other.m_item)
{
}
friend std::basic_ostream<CharT>& operator<<(std::basic_ostream<CharT>& os, colored&& item)
{
static const CharT strPrefix[3]{ '\x1b', '[', '\0' };
static const CharT strSuffix[5]{ '\x1b', '[', '0', 'm', '\0' };
os << strPrefix << item.m_colorStr << CharT('m') << item.m_item << strSuffix;
return os;
}
private:
const CharT* m_colorStr;
const ItemT& m_item;
};
In the colored
class constructor, we store a reference for the given item (instead of copying it), we don't allow a copy operation on this class (only move) and, we implement the <<
operator to take only r-value references.
Since class template argument deduction is supported only since C++17, we provide a helper function for the earlier C++ versions:
template <typename ItemT, typename CharT> colored<ItemT, CharT>
with_color(const CharT* colorStr, const ItemT& item)
{
return colored<ItemT, CharT>(colorStr, item);
}
Using this function, we can use our colored
class without specifying the template's arguments (they are deduced using the function template argument deduction).
Support Console Virtual Terminal Sequences in Windows 10 console
We can support Console Virtual Terminal Sequences also in our Windows 10 example as follows:
#include <windows.h>
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif
DWORD InitializeWindowsEscSequnces()
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE)
{
return GetLastError();
}
DWORD dwMode = 0;
if (!GetConsoleMode(hOut, &dwMode))
{
return GetLastError();
}
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
if (!SetConsoleMode(hOut, dwMode))
{
return GetLastError();
}
return 0;
}
For more information about this topic, you can go here.
The Demo Application
For demonstrating our event
class, we create a publisher class for publishing some events:
class EventsPublisher
{
};
In this class, we add:
- An event that we'll publish by a manual request:
sz::event<const std::string&, int> SomethingHappened;
- An event that will be published by a timer:
sz::event<unsigned int> TimerTick;
For implementing our timer, we add a new class:
class MyTimer
{
public:
MyTimer();
~MyTimer();
sz::event<> Tick;
bool Start(unsigned int millisecondsInterval);
bool Stop();
private:
void TimerFunc();
bool m_isRunning;
unsigned int m_millisecondsInterval;
std::thread m_timerThread;
};
#define DEFAULT_TIMER_INTERVAL 1000
MyTimer::MyTimer()
: m_isRunning(false), m_millisecondsInterval(DEFAULT_TIMER_INTERVAL)
{
}
MyTimer::~MyTimer()
{
}
bool MyTimer::Start(unsigned int millisecondsInterval)
{
if (m_isRunning)
{
return false;
}
m_isRunning = true;
m_millisecondsInterval = millisecondsInterval > 0 ? millisecondsInterval : DEFAULT_TIMER_INTERVAL;
m_timerThread = std::thread([this]() { TimerFunc(); });
return true;
}
bool MyTimer::Stop()
{
if (!m_isRunning)
{
return false;
}
m_isRunning = false;
if (m_timerThread.joinable())
{
m_timerThread.join();
}
return true;
}
void MyTimer::TimerFunc()
{
while (m_isRunning)
{
std::this_thread::sleep_for(std::chrono::milliseconds(m_millisecondsInterval));
if (m_isRunning)
{
Tick();
}
}
}
In the MyTimer
class, we implement a timer that runs a thread that publishes a Tick
event every constant interval.
In the EventsPublisher
class, we register to the Tick
event and, publish the TimerTick
event with the current tick number as the parameter:
class EventsPublisher
{
MyTimer m_timer;
unsigned int m_counter;
};
EventsPublisher::EventsPublisher()
: m_counter(0)
{
m_timer.Tick += [this]() {
m_counter++;
TimerTick(m_counter);
};
}
After we have the EventsPublisher
class, we can register some event-handlers to it:
EventsPublisher ep;
std::mutex printLocker;
sz::event_handler<unsigned int> timerHandler1([&ep, &printLocker](unsigned int counter) {
if ((counter % 5) == 0)
{
ep.SomethingHappened.call_async("Something happened from timer handler 1", 1);
}
std::lock_guard<std::mutex> lock(printLocker);
std::cout << sz::with_color("31", "Timer handler1: Timer tick ")
<< sz::with_color("41;97", counter) << std::endl;
});
sz::event_handler<unsigned int> timerHandler2([&ep, &printLocker](unsigned int counter) {
if ((counter % 7) == 0)
{
ep.SomethingHappened.call_async("Something happened from timer handler 2", 2);
}
std::lock_guard<std::mutex> lock(printLocker);
std::cout << sz::with_color("32", "Timer handler2: Timer tick ")
<< sz::with_color("42;97", counter) << std::endl;
});
auto somethingHappenedHandlerId = ep.SomethingHappened.add(
[&printLocker](const std::string& message, int publisherId) {
std::lock_guard<std::mutex> lock(printLocker);
std::cout << "Something happened. Message: "
<< sz::with_color(publisherId == 1 ? "91" : "92", message.c_str())
<< std::endl;
});
ep.TimerTick += timerHandler1;
ep.TimerTick += timerHandler2;
In the event-handlers for the TimerTick
event, we print a message for indicating the event handling and, publish a SomethingHappened
event asynchronously for every several ticks.
In the event-handler for the SomethingHappened
event, we print the gotten message.
After registering the needed event-handlers, we start the EventsPublisher
and wait for the user to stop the demo:
std::cout << sz::with_color("93", "Press <Enter> to stop.") << std::endl;
ep.Start();
getchar();
ep.SomethingHappened.remove_id(somethingHappenedHandlerId);
ep.TimerTick -= timerHandler1;
ep.TimerTick -= timerHandler2;
ep.Stop();
The result is: