From 6fb4615769515d1b1be6162dfb9363e1ece6de8d Mon Sep 17 00:00:00 2001 From: wqking Date: Mon, 21 Oct 2024 16:00:35 +0800 Subject: [PATCH] Added sample code and document for Tip - use C++ data type as event identifier --- doc/tip_use_type_as_id.md | 178 ++++++++++++++++++++++++++ readme.md | 36 +++--- tests/tutorial/CMakeLists.txt | 2 +- tests/tutorial/tip_use_type_as_id.cpp | 159 +++++++++++++++++++++++ 4 files changed, 357 insertions(+), 18 deletions(-) create mode 100644 doc/tip_use_type_as_id.md create mode 100644 tests/tutorial/tip_use_type_as_id.cpp diff --git a/doc/tip_use_type_as_id.md b/doc/tip_use_type_as_id.md new file mode 100644 index 0000000..0df7275 --- /dev/null +++ b/doc/tip_use_type_as_id.md @@ -0,0 +1,178 @@ +# Tip - use C++ data type as event identifier + +## Overview + +`eventpp` is based on the event identifier. Event identifiers are used to distinguish each event. The identifier can be an integer, +an enumerator, a string, etc. For example, pseudo code, `eventQueue.appendListener(5, someCallback)`, here the value `5` is the identifier. + +Some developers may want to use C++ data type to distinguish each event, and an event is just a data type. +For example, pseudo code, `eventQueue.appendListener(someCallback)`, here the type `KeyEvent` represents the event, no identifier involves. +There is [an issue demanding such feature](https://github.com/wqking/eventpp/issues/60). + +## Use C++ data type as event identifier + +With the help of utility classes `AndId` and `AndData`, now we can simulate using C++ data type as event identifier perfectly. +Some notes on the sample code. + +1, The code uses `EventQueue`. The same code can also be used with `EventDispatcher`. +2, The code is so generic that not only C++ class, but also enumerator, or even primary data types can be used. +3, Be careful that `AndData` is not type safe, you may want to examine how the safer function `safeAppendListener` works. +4, The code gives two version of `TypeIndexDigester`, you may only need to choose the one that's appropriate for your C++ compiler. +5, If you don't want to use `std::type_info`, you may use following pseudo code to simulate the RTTI. `template void fakeRtti() {}`, then `&fakeRtti` can be used to distinguish the data type. + +## Code + +The full runnable code is in file `eventpp/tests/tutorial/tip_use_type_as_id.cpp`. + +```c++ +// Include the headers +#include "eventpp/eventqueue.h" +#include "eventpp/utilities/anyid.h" +#include "eventpp/utilities/anydata.h" + +#include +#include +#include + +// Follow headers are only for tutorial purpose +#include "tutorial.h" +#include + +// We need a digester class for AnyId. +/* +// This is the C++17 version with cleaner code. +template +struct TypeIndexDigesterCpp17 +{ + std::type_index operator() (const T & typeInfo) const + { + if constexpr (std::is_same::value) { + return std::type_index(typeInfo); + } + else { + return std::type_index(typeid(T)); + } + } + +}; +*/ + +// This is the C++11 version with SFINAE. +// Basically we need to support two overloaded operator(). +// The first one is `std::type_index operator() (const T &) const`, +// it's to get std::type_index from typeid(T) where T is a general type. +// The second one is `std::type_index operator() (const std::type_info &) const`, +// we don't need to apply typeid on the type_info. +template +struct TypeIndexDigesterCpp11 +{ + template + auto operator() (const U &) const + -> typename std::enable_if::value, std::type_index>::type + { + return std::type_index(typeid(T)); + } + + template + auto operator() (const U & typeInfo) const + -> typename std::enable_if::value, std::type_index>::type + { + return std::type_index(typeInfo); + } +}; + +// Define the maxSize parameter used in AnyData. 128 is an arbitrary hard coded size. +// You may want to calculate the max size of your event struct, such as, +// constexpr std::size_t eventMaxSize = eventpp::maxSizeOf(); +// See document for AnyData for more information. +constexpr std::size_t eventMaxSize = 128; +// Now let's define the event queue +using TypeBasedEventQueue = eventpp::EventQueue< + eventpp::AnyId, + void(const eventpp::AnyData &) +>; + +// Note AnyData is not type safe, that means +// queue.appendListener(typeid(MouseEvent), [](const KeyEvent & event) {}); +// will compile but the listener will receive wrong data and crash. +// To ensure type safety, we may introduce an auxiliary function `safeAppendListener`. +template +void safeAppendListener(Queue & queue, const Callback & callback) +{ + // In C++17, we can use std::is_invocable to check if we can invoke callback(Event()). + // Here to be compatible with C++11, we use lambda to let the compiler perform the check. + queue.appendListener(typeid(Event), [callback](const Event & event) { + callback(event); + }); +} + +// We can use any C++ type as the event, not only class, but also enum. +struct KeyEvent { int key; }; +struct MouseEvent { int x; int y; }; +struct DrawText { std::string text; }; +enum class Animal { dog, cat }; + +TEST_CASE("Tip: Use C++ type as event identifier") +{ + std::cout << std::endl << "Tip: Use C++ type as event identifier" << std::endl; + + TypeBasedEventQueue queue; + + // Append a listener, here we use an object of KeyEvent as the event identifier. + // This calls the overload `std::type_index operator() (const T &) const` in TypeIndexDigester. + queue.appendListener(KeyEvent{}, [](const KeyEvent & event) { + std::cout << "Received KeyEvent, key=" << event.key << std::endl; + }); + + // You may not want to create an object only for an identifier. So here we use typeid. + // This calls the overload `std::type_index operator() (const std::type_info &) const` in TypeIndexDigester. + queue.appendListener(typeid(MouseEvent), [](const MouseEvent & event) { + std::cout << "Received MouseEvent, x=" << event.x << " y=" << event.y << std::endl; + }); + queue.appendListener(typeid(DrawText), [](const DrawText & event) { + std::cout << "Received DrawText, text=" << event.text << std::endl; + }); + // In above code, wrong event type may compile fine but crash your program, for example, + // queue.appendListener(typeid(DrawText), [](const KeyEvent & event) {}); + + // safeAppendListener is a better way to append listener. Following won't compile, + // safeAppendListener(queue, [](const Animal & event) {}); + safeAppendListener(queue, [](const Animal & event) { + std::cout << "Received Animal, the animal is " << (event == Animal::dog ? "dog" : "cat") << std::endl; + }); + // We can even use primary data type as event. + safeAppendListener(queue, [](const int event) { + std::cout << "Received int, the value is " << event << std::endl; + }); + safeAppendListener(queue, [](const long event) { + std::cout << "Received long, the value is " << event << std::endl; + }); + + // We have introduced three methods to append a listener, it's for demonstration. + // In the production code, we should only use one method and be consistent. + + queue.enqueue(KeyEvent{ 9 }); + queue.enqueue(KeyEvent{ 32 }); + queue.enqueue(MouseEvent{ 1024, 768 }); + queue.enqueue(DrawText{ "Hello" }); + queue.enqueue(Animal::dog); + queue.enqueue(Animal::cat); + queue.enqueue(3); + queue.enqueue(5L); + // This won't trigger any listener since there is no listener for long long. + queue.enqueue(8LL); + + queue.process(); +} +``` + +**Output** + +> Received KeyEvent, key=9 +> Received KeyEvent, key=32 +> Received MouseEvent, x=1024 y=768 +> Received DrawText, text=Hello +> Received Animal, the animal is dog +> Received Animal, the animal is cat +> Received int, the value is 3 +> Received long, the value is 5 diff --git a/readme.md b/readme.md index aaa5f27..087d898 100644 --- a/readme.md +++ b/readme.md @@ -1,23 +1,23 @@ # eventpp -- C++ library for event dispatcher and callback list - [eventpp -- C++ library for event dispatcher and callback list](#eventpp----c-library-for-event-dispatcher-and-callback-list) - - [Facts and features](#facts-and-features) - - [License](#license) - - [Version 0.1.3](#version-013) - - [Source code](#source-code) - - [Supported compilers](#supported-compilers) - - [C++ standard requirements](#c-standard-requirements) - - [Quick start](#quick-start) - - [Namespace](#namespace) - - [Use eventpp in your project](#use-eventpp-in-your-project) - - [Using CallbackList](#using-callbacklist) - - [Using EventDispatcher](#using-eventdispatcher) - - [Using EventQueue](#using-eventqueue) - - [Documentations](#documentations) - - [Build the test code](#build-the-test-code) - - [Motivations](#motivations) - - [Change log](#change-log) - - [Contributors](#contributors) + - [Facts and features](#facts-and-features) + - [License](#license) + - [Version 0.1.3](#version-013) + - [Source code](#source-code) + - [Supported compilers](#supported-compilers) + - [C++ standard requirements](#c-standard-requirements) + - [Quick start](#quick-start) + - [Namespace](#namespace) + - [Use eventpp in your project](#use-eventpp-in-your-project) + - [Using CallbackList](#using-callbacklist) + - [Using EventDispatcher](#using-eventdispatcher) + - [Using EventQueue](#using-eventqueue) + - [Documentations](#documentations) + - [Build the test code](#build-the-test-code) + - [Motivations](#motivations) + - [Change log](#change-log) + - [Contributors](#contributors) eventpp is a C++ event library for callbacks, event dispatcher, and event queue. With eventpp you can easily implement signal and slot mechanism, publisher and subscriber pattern, or observer pattern. @@ -173,6 +173,8 @@ queue.process(); * Miscellaneous * [Performance benchmarks](doc/benchmark.md) * [FAQs, tricks, and tips](doc/faq.md) +* Tips and tricks + * [Use C++ data type as event identifier](doc/tip_use_type_as_id.md) * Heterogeneous classes and functions, for proof of concept, usually you don't need them * [Overview of heterogeneous classes](doc/heterogeneous.md) * [Class HeterCallbackList](doc/hetercallbacklist.md) diff --git a/tests/tutorial/CMakeLists.txt b/tests/tutorial/CMakeLists.txt index f63da78..0e4d4cb 100644 --- a/tests/tutorial/CMakeLists.txt +++ b/tests/tutorial/CMakeLists.txt @@ -9,6 +9,7 @@ set(SRC_TUTORIAL tutorial_hetereventdispatcher.cpp tutorial_argumentadapter.cpp tutorial_anydata.cpp + tip_use_type_as_id.cpp ) add_executable( @@ -20,4 +21,3 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(${TARGET_TUTORIAL} Threads::Threads) - diff --git a/tests/tutorial/tip_use_type_as_id.cpp b/tests/tutorial/tip_use_type_as_id.cpp new file mode 100644 index 0000000..db7f7ad --- /dev/null +++ b/tests/tutorial/tip_use_type_as_id.cpp @@ -0,0 +1,159 @@ +// eventpp library +// Copyright (C) 2018 Wang Qi (wqking) +// Github: https://github.com/wqking/eventpp +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This tutorial code demonstrates how to use C++ data type as event identifier. +// The tutorial document is in https://github.com/wqking/eventpp/blob/master/doc/tip_use_type_as_id.md + +// Include the headers +#include "eventpp/eventqueue.h" +#include "eventpp/utilities/anyid.h" +#include "eventpp/utilities/anydata.h" + +#include +#include +#include + +// Follow headers are only for tutorial purpose +#include "tutorial.h" +#include + +namespace { + +// We need a digester class for AnyId. +/* +// This is the C++17 version with cleaner code. +template +struct TypeIndexDigesterCpp17 +{ + std::type_index operator() (const T & typeInfo) const + { + if constexpr (std::is_same::value) { + return std::type_index(typeInfo); + } + else { + return std::type_index(typeid(T)); + } + } + +}; +*/ + +// This is the C++11 version with SFINAE. +// Basically we need to support two overloaded operator(). +// The first one is `std::type_index operator() (const T &) const`, +// it's to get std::type_index from typeid(T) where T is a general type. +// The second one is `std::type_index operator() (const std::type_info &) const`, +// we don't need to apply typeid on the type_info. +template +struct TypeIndexDigesterCpp11 +{ + template + auto operator() (const U &) const + -> typename std::enable_if::value, std::type_index>::type + { + return std::type_index(typeid(T)); + } + + template + auto operator() (const U & typeInfo) const + -> typename std::enable_if::value, std::type_index>::type + { + return std::type_index(typeInfo); + } +}; + +// Define the maxSize parameter used in AnyData. 128 is an arbitrary hard coded size. +// You may want to calculate the max size of your event struct, such as, +// constexpr std::size_t eventMaxSize = eventpp::maxSizeOf(); +// See document for AnyData for more information. +constexpr std::size_t eventMaxSize = 128; +// Now let's define the event queue +using TypeBasedEventQueue = eventpp::EventQueue< + eventpp::AnyId, + void(const eventpp::AnyData &) +>; + +// Note AnyData is not type safe, that means +// queue.appendListener(typeid(MouseEvent), [](const KeyEvent & event) {}); +// will compile but the listener will receive wrong data and crash. +// To ensure type safety, we may introduce an auxiliary function `safeAppendListener`. +template +void safeAppendListener(Queue & queue, const Callback & callback) +{ + // In C++17, we can use std::is_invocable to check if we can invoke callback(Event()). + // Here to be compatible with C++11, we use lambda to let the compiler perform the check. + queue.appendListener(typeid(Event), [callback](const Event & event) { + callback(event); + }); +} + +// We can use any C++ type as the event, not only class, but also enum. +struct KeyEvent { int key; }; +struct MouseEvent { int x; int y; }; +struct DrawText { std::string text; }; +enum class Animal { dog, cat }; + +TEST_CASE("Tip: Use C++ type as event identifier") +{ + std::cout << std::endl << "Tip: Use C++ type as event identifier" << std::endl; + + TypeBasedEventQueue queue; + + // Append a listener, here we use an object of KeyEvent as the event identifier. + // This calls the overload `std::type_index operator() (const T &) const` in TypeIndexDigester. + queue.appendListener(KeyEvent{}, [](const KeyEvent & event) { + std::cout << "Received KeyEvent, key=" << event.key << std::endl; + }); + + // You may not want to create an object only for an identifier. So here we use typeid. + // This calls the overload `std::type_index operator() (const std::type_info &) const` in TypeIndexDigester. + queue.appendListener(typeid(MouseEvent), [](const MouseEvent & event) { + std::cout << "Received MouseEvent, x=" << event.x << " y=" << event.y << std::endl; + }); + queue.appendListener(typeid(DrawText), [](const DrawText & event) { + std::cout << "Received DrawText, text=" << event.text << std::endl; + }); + // In above code, wrong event type may compile fine but crash your program, for example, + // queue.appendListener(typeid(DrawText), [](const KeyEvent & event) {}); + + // safeAppendListener is a better way to append listener. Following won't compile, + // safeAppendListener(queue, [](const Animal & event) {}); + safeAppendListener(queue, [](const Animal & event) { + std::cout << "Received Animal, the animal is " << (event == Animal::dog ? "dog" : "cat") << std::endl; + }); + // We can even use primary data type as event. + safeAppendListener(queue, [](const int event) { + std::cout << "Received int, the value is " << event << std::endl; + }); + safeAppendListener(queue, [](const long event) { + std::cout << "Received long, the value is " << event << std::endl; + }); + + // We have introduced three methods to append a listener, it's for demonstration. + // In the production code, we should only use one method and be consistent. + + queue.enqueue(KeyEvent{ 9 }); + queue.enqueue(KeyEvent{ 32 }); + queue.enqueue(MouseEvent{ 1024, 768 }); + queue.enqueue(DrawText{ "Hello" }); + queue.enqueue(Animal::dog); + queue.enqueue(Animal::cat); + queue.enqueue(3); + queue.enqueue(5L); + // This won't trigger any listener since there is no listener for long long. + queue.enqueue(8LL); + + queue.process(); +} + +}