Click here to Skip to main content
15,868,010 members
Articles / Programming Languages / C++11

CppEvent - How to Implement Events using Standard C++

Rate me:
Please Sign up or sign in to vote.
5.00/5 (26 votes)
13 Aug 2018CPOL7 min read 113K   2.8K   42   11
This article shows how we can implement a thread-safe events (similar to .NET events) mechanism using the standard C++ library.

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:

C++
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:

C++
typedef std::function<void(Args...)> handler_func_type;

handler_func_type m_handlerFunc;

For calling our function, we add an appropriate function call operator:

C++
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:

C++
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:

C++
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:

C++
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:

C++
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);

    // Since the function return value is by copy, 
    // before the function returns (and destruct the lock_guard object),
    // it creates a copy of the m_handlers container.
    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:

C++
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:

C++
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:

C++
template <typename... Args> class event_handler
{
    // ...

    // copy constructor
    event_handler(const event_handler& src)
        : m_handlerFunc(src.m_handlerFunc), m_handlerId(src.m_handlerId)
    {
    }

    // move constructor
    event_handler(event_handler&& src)
        : m_handlerFunc(std::move(src.m_handlerFunc)), m_handlerId(src.m_handlerId)
    {
    }

    // copy assignment operator
    event_handler& operator=(const event_handler& src)
    {
        m_handlerFunc = src.m_handlerFunc;
        m_handlerId = src.m_handlerId;

        return *this;
    }

    // move assignment operator
    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
{
    // ...

    // copy constructor
    event(const event& src)
    {
        std::lock_guard<std::mutex> lock(src.m_handlersLocker);

        m_handlers = src.m_handlers;
    }

    // move constructor
    event(event&& src)
    {
        std::lock_guard<std::mutex> lock(src.m_handlersLocker);

        m_handlers = std::move(src.m_handlers);
    }

    // copy assignment operator
    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;
    }

    // move assignment operator
    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:

C++11 -std=c++11 Eclipse

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):

    C++11 __GXX_EXPERIMENTAL_CXX0X__ Eclipse

  • Change the value of the __cplusplus symbol to 201103L (or a greater value):

    C++11 __cplusplus Eclipse

Since we use threads in our project, we add pthread to the projectcs linked libraries:

pthread Eclipse

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:

C++
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:

C++
template <typename ItemT, typename CharT> class colored
{
public:
    colored(const CharT* colorStr, const ItemT& item)
        : m_colorStr(colorStr), m_item(item)
    {
    }

    // Disable Copy
    colored(const colored& other) = delete;
    colored& operator=(const colored& other) = delete;

    // Enable Move
    colored(colored&& other)
        : m_colorStr(other.m_colorStr), m_item(other.m_item)
    {
    }

    // The parameter is an R-Value reference since we don't want retained copies of this class.
    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:

C++
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:

C++
#include <windows.h>

// Define ENABLE_VIRTUAL_TERMINAL_PROCESSING, if it isn't defined.
#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
#endif

DWORD InitializeWindowsEscSequnces()
{
    // Set output mode to handle virtual terminal sequences
    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:

C++
class EventsPublisher
{
};

In this class, we add:

  1. An event that we'll publish by a manual request:
    C++
    // event of <message, publisher id>.
    sz::event<const std::string&, int> SomethingHappened;
  2. An event that will be published by a timer:
    C++
    sz::event<unsigned int> TimerTick;

For implementing our timer, we add a new class:

C++
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;
};
C++
// Can be implemented to be more thread safe. But, it's only for the example.
// If the interval is too big, the Stop function will be affected too.

#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:

C++
class EventsPublisher
{
    // ...

    MyTimer m_timer;
    unsigned int m_counter;
};
C++
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:

C++
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;
});

// We can create an event_handler also for this handler. 
// But, we want to demonstrate the use without it.
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:

C++
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:

sz::event demo result

License

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


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionDoesn't work with member functions Pin
OneWay 20234-Sep-23 1:27
OneWay 20234-Sep-23 1:27 
QuestionError:Local lambda is not allowed in a member function of managed class Pin
Member 1467043927-Nov-19 18:37
Member 1467043927-Nov-19 18:37 
When I tried to subscribe the event in a CLI class, I am getting the error as

Error:Local lambda is not allowed in a member function of managed class

My code is as below

'''CLI

TestCLI::TestCLI()
{
		
	testClassCpp.LogNotification += [this](string data)
	{
		Write(data); 
	};
}	

void TestCLI::Write(string data)
{
	cout<<data<<endl;
}


'''
		
		
		
		
'''CPP
	
class __declspec(dllexport) TestClassCpp
{
public:
	
	sz::event <string>LogNotification;
	
};
'''

AnswerRe: Error:Local lambda is not allowed in a member function of managed class Pin
Shmuel Zang14-Jan-20 1:26
Shmuel Zang14-Jan-20 1:26 
GeneralRe: Error:Local lambda is not allowed in a member function of managed class Pin
Joe O'Leary28-May-21 8:32
Joe O'Leary28-May-21 8:32 
GeneralMy vote of 5 Pin
koothkeeper14-Aug-18 11:11
professionalkoothkeeper14-Aug-18 11:11 
GeneralRe: My vote of 5 Pin
Shmuel Zang15-Aug-18 7:02
Shmuel Zang15-Aug-18 7:02 
PraiseOutstanding! Pin
koothkeeper14-Aug-18 11:11
professionalkoothkeeper14-Aug-18 11:11 
GeneralMy vote of 5 Pin
Сергій Ярошко14-Aug-18 5:26
professionalСергій Ярошко14-Aug-18 5:26 
GeneralRe: My vote of 5 Pin
Shmuel Zang15-Aug-18 7:00
Shmuel Zang15-Aug-18 7:00 
GeneralMy vote of 5 Pin
Franc Morales13-Aug-18 14:34
Franc Morales13-Aug-18 14:34 
GeneralRe: My vote of 5 Pin
Shmuel Zang15-Aug-18 6:59
Shmuel Zang15-Aug-18 6:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.