2
\$\begingroup\$

C++ Event class

From the desire of having a callback function which includes a void* userdata argument, I've made this generic event class.

It is tested and working, but some desired features are missing

  • Allow to have a parameter less event with no userdata
  • Allow to specify a TResult template argument, for example to have operator() returns bool instead of void.

Also I do not like the fact that

  • TUserData is turned into a pointer for _userdata and into a reference for the callback function argument. It does not seems intuitive enough to use.

The problem with adding a TResult, is that the operator() function will

  • Not compile, either because TResult is void and it tries to return a value, or because TResult is not void and it does try to return something.
  • Have to return a default value when _func is null.

I've looked into template partial specialization but it does not seem to be a solution. Or does it?
Would using concepts be of any help?
Any generic improvements to be made?

event.hpp

#pragma once
namespace shoujin {
template<typename TUserData, typename... TArguments>
class Event {
    using TFunc = void (*)(TArguments..., TUserData&);
    TFunc _func;
    TUserData* _userdata;

public:
    Event() :
        _func{}, _userdata{}
    {}

    Event(TFunc func, TUserData& userdata)
    {
        _func = func;
        _userdata = &userdata;
    }

    Event& operator=(const Event& rhs)
    {
        _func = rhs._func;
        _userdata = rhs._userdata;
        return *this;
    }

    ~Event()
    {
        _func = nullptr;
        _userdata = nullptr;
    }

    void operator()(TArguments... args) const
    {
        if(_func)
            _func(args..., *_userdata);
    }

    operator bool() const
    {
        return _func != nullptr;
    }
};
}

event_test.cpp

#include "CppUnitTest.h"

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <tchar.h>
#include <shoujin/event.hpp>

using namespace shoujin;

TEST_CLASS(EventTest) {
    static void OnEventTwoParam(int x, int y, int& value)
    {
        value = x + y;
    }

public:
    TEST_METHOD(Event_IsCopyConstructible) {
        Assert::IsTrue(std::is_copy_constructible_v<Event<void*>>);
    }

    TEST_METHOD(Event_IsCopyAssignable) {
        Assert::IsTrue(std::is_copy_assignable_v<Event<void*>>);
    }

    TEST_METHOD(Event_IsMoveConstructible) {
        Assert::IsTrue(std::is_move_constructible_v<Event<void*>>);
    }

    TEST_METHOD(Event_IsMoveAssignable) {
        Assert::IsTrue(std::is_move_assignable_v<Event<void*>>);
    }

    TEST_METHOD(Event_WhenTwoParam_EventRaised) {
        //Arrange
        int value;
        Event<int, int, int> event_two_param(OnEventTwoParam, value);
        int x{3}, y{2};

        //Act
        event_two_param(x, y);

        //Assert
        Assert::AreEqual(x + y, value);
    }

    TEST_METHOD(Event_CopyAssignment_EventRaised) {
        //Arrange
        int value;
        Event<int, int, int> event_two_param;
        int x{3}, y{2};

        event_two_param = {OnEventTwoParam, value};

        //Act
        event_two_param(x, y);

        //Assert
        Assert::AreEqual(x + y, value);
    }

    TEST_METHOD(Event_OperatorBool_EventRaised) {
        //Arrange
        int value;
        Event<int, int, int> event_two_param;
        int x{3}, y{2};

        //Act
        bool before = event_two_param;
        event_two_param = {OnEventTwoParam, value};
        bool after = event_two_param;

        //Assert
        Assert::IsFalse(before);
        Assert::IsTrue(after);
    }
};

All criticism is much appreciated.

\$\endgroup\$
2
  • \$\begingroup\$ A custom concept was added to force TUserData to be a pointer, and a UserDataEvent class was made with it. The Event class not no longer have any userdata support. \$\endgroup\$ Commented Oct 30, 2021 at 18:08
  • \$\begingroup\$ Making UserData a template instead of a void* does not seem to be good, because the component who declare and raise the event does not know what kind of user data the listeners will need, and the listeners do not declare anything. This is why using void* is more flexible. In C language, the listener can simply define his OnEvent function to receive the right userdata type, and C will allow it, but C++ won't implicitly convert void* to another pointer type, so the listener function has to receive void* and reinterpret_cast it. \$\endgroup\$ Commented Oct 30, 2021 at 18:12

1 Answer 1

1
\$\begingroup\$

Since C++11, the standard library provides std::function, which basically does what your Event class does: store a function and any associated data you want captured. For example, your Event_CopyAssignment_EventRaised test can be rewritten like so:

#include <functional>
...
TEST_METHOD(Event_CopyAssignment_EventRaised) {
  int value;
  std::function<void(int, int)> event_two_param;
  int x{3}, y{2};

  using namespace std::placeholders;
  bool before = (bool)event_two_param;
  event_two_param = std::bind(OnEventTwoParam, _1, _2, std::ref(value));
  bool after = (bool)event_two_param;

  event_two_param(x, y);
  Assert::AreEqual(x + y, value);
  Assert::IsFalse(before);
  Assert::IsTrue(after);
}

Assigning the function to event_two_param such that it captures the reference to value is a bit awkward that way, but you can also use a lambda to do the same:

event_two_param = [&](int x, int y){ OnEventTwoParam(x, y, value); };

Or just use a lambda expression directly to sum two values:

event_two_param = [&](int x, int y){ value = x + y; };

Note that std::function also has an operator bool(), but it is marked explicit to avoid accidental conversion to bool, so you have to explicitly cast it if you want to use it like in your test. But you can use it without casting in an if-statement:

if (event_two_param) {
    /* event_two_param has a function associated with it */
    ...
}

std::function also solves all the issues you have mentioned: you can also use it for functions that don't take a pointer to userdata, and you can have it store functions that return a value:

std::function<int()> event_no_param;
event_no_param = []{ return 42; };
int value = event_no_param();
Assert::AreEqual(value, 42);
\$\endgroup\$
1
  • \$\begingroup\$ Very informative, and your answer gives me leads into learning more about std::function, and also about the fact that a template parameter can be a function signature such as void(int, int). Thank you for your answer. \$\endgroup\$ Commented Oct 30, 2021 at 21:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.