├── .gitattributes ├── .gitignore ├── BenchEventVsCall.h ├── CMakeLists.txt ├── CMakeSettings.json ├── LICENSE ├── README.md ├── Test.h ├── event.hpp ├── event_vector.hpp └── main.cpp /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # Benchmark Results 46 | BenchmarkDotNet.Artifacts/ 47 | 48 | # .NET Core 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | **/Properties/launchSettings.json 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # TFS 2012 Local Workspace 100 | $tf/ 101 | 102 | # Guidance Automation Toolkit 103 | *.gpState 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper*/ 107 | *.[Rr]e[Ss]harper 108 | *.DotSettings.user 109 | 110 | # JustCode is a .NET coding add-in 111 | .JustCode 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # AxoCover is a Code Coverage Tool 120 | .axoCover/* 121 | !.axoCover/settings.json 122 | 123 | # Visual Studio code coverage results 124 | *.coverage 125 | *.coveragexml 126 | 127 | # NCrunch 128 | _NCrunch_* 129 | .*crunch*.local.xml 130 | nCrunchTemp_* 131 | 132 | # MightyMoose 133 | *.mm.* 134 | AutoTest.Net/ 135 | 136 | # Web workbench (sass) 137 | .sass-cache/ 138 | 139 | # Installshield output folder 140 | [Ee]xpress/ 141 | 142 | # DocProject is a documentation generator add-in 143 | DocProject/buildhelp/ 144 | DocProject/Help/*.HxT 145 | DocProject/Help/*.HxC 146 | DocProject/Help/*.hhc 147 | DocProject/Help/*.hhk 148 | DocProject/Help/*.hhp 149 | DocProject/Help/Html2 150 | DocProject/Help/html 151 | 152 | # Click-Once directory 153 | publish/ 154 | 155 | # Publish Web Output 156 | *.[Pp]ublish.xml 157 | *.azurePubxml 158 | # Note: Comment the next line if you want to checkin your web deploy settings, 159 | # but database connection strings (with potential passwords) will be unencrypted 160 | *.pubxml 161 | *.publishproj 162 | 163 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 164 | # checkin your Azure Web App publish settings, but sensitive information contained 165 | # in these scripts will be unencrypted 166 | PublishScripts/ 167 | 168 | # NuGet Packages 169 | *.nupkg 170 | # The packages folder can be ignored because of Package Restore 171 | **/packages/* 172 | # except build/, which is used as an MSBuild target. 173 | !**/packages/build/ 174 | # Uncomment if necessary however generally it will be regenerated when needed 175 | #!**/packages/repositories.config 176 | # NuGet v3's project.json files produces more ignorable files 177 | *.nuget.props 178 | *.nuget.targets 179 | 180 | # Microsoft Azure Build Output 181 | csx/ 182 | *.build.csdef 183 | 184 | # Microsoft Azure Emulator 185 | ecf/ 186 | rcf/ 187 | 188 | # Windows Store app package directories and files 189 | AppPackages/ 190 | BundleArtifacts/ 191 | Package.StoreAssociation.xml 192 | _pkginfo.txt 193 | *.appx 194 | 195 | # Visual Studio cache files 196 | # files ending in .cache can be ignored 197 | *.[Cc]ache 198 | # but keep track of directories ending in .cache 199 | !*.[Cc]ache/ 200 | 201 | # Others 202 | ClientBin/ 203 | ~$* 204 | *~ 205 | *.dbmdl 206 | *.dbproj.schemaview 207 | *.jfm 208 | *.pfx 209 | *.publishsettings 210 | orleans.codegen.cs 211 | 212 | # Since there are multiple workflows, uncomment next line to ignore bower_components 213 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 214 | #bower_components/ 215 | 216 | # RIA/Silverlight projects 217 | Generated_Code/ 218 | 219 | # Backup & report files from converting an old project file 220 | # to a newer Visual Studio version. Backup files are not needed, 221 | # because we have git ;-) 222 | _UpgradeReport_Files/ 223 | Backup*/ 224 | UpgradeLog*.XML 225 | UpgradeLog*.htm 226 | 227 | # SQL Server files 228 | *.mdf 229 | *.ldf 230 | *.ndf 231 | 232 | # Business Intelligence projects 233 | *.rdl.data 234 | *.bim.layout 235 | *.bim_*.settings 236 | 237 | # Microsoft Fakes 238 | FakesAssemblies/ 239 | 240 | # GhostDoc plugin setting file 241 | *.GhostDoc.xml 242 | 243 | # Node.js Tools for Visual Studio 244 | .ntvs_analysis.dat 245 | node_modules/ 246 | 247 | # Typescript v1 declaration files 248 | typings/ 249 | 250 | # Visual Studio 6 build log 251 | *.plg 252 | 253 | # Visual Studio 6 workspace options file 254 | *.opt 255 | 256 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 257 | *.vbw 258 | 259 | # Visual Studio LightSwitch build output 260 | **/*.HTMLClient/GeneratedArtifacts 261 | **/*.DesktopClient/GeneratedArtifacts 262 | **/*.DesktopClient/ModelManifest.xml 263 | **/*.Server/GeneratedArtifacts 264 | **/*.Server/ModelManifest.xml 265 | _Pvt_Extensions 266 | 267 | # Paket dependency manager 268 | .paket/paket.exe 269 | paket-files/ 270 | 271 | # FAKE - F# Make 272 | .fake/ 273 | 274 | # JetBrains Rider 275 | .idea/ 276 | *.sln.iml 277 | 278 | # CodeRush 279 | .cr/ 280 | 281 | # Python Tools for Visual Studio (PTVS) 282 | __pycache__/ 283 | *.pyc 284 | 285 | # Cake - Uncomment if you are using it 286 | # tools/** 287 | # !tools/packages.config 288 | 289 | # Tabs Studio 290 | *.tss 291 | 292 | # Telerik's JustMock configuration file 293 | *.jmconfig 294 | 295 | # BizTalk build output 296 | *.btp.cs 297 | *.btm.cs 298 | *.odx.cs 299 | *.xsd.cs 300 | -------------------------------------------------------------------------------- /BenchEventVsCall.h: -------------------------------------------------------------------------------- 1 | //NOTE: intended to be included multiple times 2 | 3 | struct Foo 4 | { 5 | Event evt; 6 | }; 7 | 8 | #define CONN(X, Y) X##Y 9 | #define GEN_STUFF(X) virtual void CONN(stuff, X)() {} 10 | #define STUFF() GEN_STUFF(__COUNTER__) 11 | 12 | #define STUFF_10() STUFF() STUFF() STUFF() STUFF() STUFF() STUFF() STUFF() STUFF() STUFF() STUFF() 13 | #define STUFF_100() STUFF_10() STUFF_10() STUFF_10() STUFF_10() STUFF_10() STUFF_10() STUFF_10() STUFF_10() STUFF_10() 14 | #define STUFF_1000() STUFF_100() STUFF_100() STUFF_100() STUFF_100() STUFF_100() STUFF_100() STUFF_100() STUFF_100() STUFF_100() 15 | 16 | struct BarBase 17 | { 18 | virtual ~BarBase() {} 19 | STUFF_1000() 20 | virtual void vf(float delta) = 0; 21 | STUFF_1000() 22 | }; 23 | 24 | template 25 | struct Bar : BarBase 26 | { 27 | void onEvt(float delta) 28 | { 29 | total += delta * ((rand() % 2) ? 1 : -1); 30 | } 31 | 32 | virtual void vf(float delta) override 33 | { 34 | onEvt(delta); 35 | } 36 | 37 | Listener<&Foo::evt, &Bar::onEvt> listener; 38 | 39 | void connect(Foo* foo) 40 | { 41 | listener.connect(foo, this); 42 | } 43 | 44 | volatile float total = 0.f; 45 | }; 46 | 47 | template 48 | std::function getFac() 49 | { 50 | return [](Foo* foo) -> BarBase* 51 | { 52 | auto b = new Bar; 53 | b->connect(foo); 54 | return b; 55 | }; 56 | } 57 | 58 | template 59 | auto getFactoriesImpl(std::index_sequence) 60 | { 61 | return std::array, sizeof...(Is)> { getFac() ... }; 62 | } 63 | 64 | auto getFactories() 65 | { 66 | return getFactoriesImpl(std::make_index_sequence<100>()); 67 | } 68 | 69 | static auto fac = getFactories(); 70 | 71 | void test() 72 | { 73 | std::cout << "Event vs call (same class):\n"; 74 | 75 | { 76 | std::vector> bars; 77 | Foo foo; 78 | bars.resize(100'000); 79 | for (auto& bar : bars) 80 | bar.listener.connect(&foo, &bar); 81 | 82 | double event_duration, call_duration; 83 | 84 | { 85 | auto start = std::chrono::high_resolution_clock::now(); 86 | foo.evt(0.001f); 87 | auto end = std::chrono::high_resolution_clock::now(); 88 | std::chrono::duration diff = end - start; 89 | std::cout << "Event: " << diff.count() << " s\n"; 90 | event_duration = diff.count(); 91 | } 92 | 93 | { 94 | auto start = std::chrono::high_resolution_clock::now(); 95 | for (auto& bar : bars) 96 | bar.onEvt(0.001f); 97 | auto end = std::chrono::high_resolution_clock::now(); 98 | std::chrono::duration diff = end - start; 99 | std::cout << "Call: " << diff.count() << " s\n"; 100 | call_duration = diff.count(); 101 | } 102 | 103 | { 104 | auto start = std::chrono::high_resolution_clock::now(); 105 | for (auto& bar : bars) 106 | bar.vf(0.001f); 107 | auto end = std::chrono::high_resolution_clock::now(); 108 | std::chrono::duration diff = end - start; 109 | std::cout << "Virtual Call: " << diff.count() << " s\n"; 110 | call_duration = diff.count(); 111 | } 112 | 113 | std::cout << "Event takes " << (int)(100 * event_duration / call_duration) << "% time of direct function calls \n"; 114 | } 115 | 116 | std::cout << "Event vs virtual (random from 100 classes):\n"; 117 | 118 | { 119 | std::vector> bars; 120 | Foo foo; 121 | for (int i = 0; i < 100'000; i++) 122 | bars.emplace_back(fac[rand() % 100](&foo)); 123 | 124 | double event_duration, call_duration; 125 | 126 | { 127 | auto start = std::chrono::high_resolution_clock::now(); 128 | foo.evt(0.001f); 129 | auto end = std::chrono::high_resolution_clock::now(); 130 | std::chrono::duration diff = end - start; 131 | std::cout << "Event: " << diff.count() << " s\n"; 132 | event_duration = diff.count(); 133 | } 134 | 135 | { 136 | auto start = std::chrono::high_resolution_clock::now(); 137 | for (auto& bar : bars) 138 | { 139 | bar->vf(0.001f); 140 | } 141 | auto end = std::chrono::high_resolution_clock::now(); 142 | std::chrono::duration diff = end - start; 143 | std::cout << "Virtual call: " << diff.count() << " s\n"; 144 | call_duration = diff.count(); 145 | } 146 | 147 | std::cout << "Event takes " << (int)(100 * event_duration / call_duration) << "% time of virtual function calls \n"; 148 | } 149 | } -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.9) 2 | project (ImpossiblyFastEvent) 3 | 4 | add_executable(ImpossiblyFastEvent main.cpp) 5 | set_target_properties(ImpossiblyFastEvent PROPERTIES 6 | CXX_STANDARD 17 7 | CXX_STANDARD_REQUIRED ON 8 | CXX_EXTENSIONS OFF 9 | ) -------------------------------------------------------------------------------- /CMakeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com//fwlink//?linkid=834763 for more information about this file. 3 | "configurations": [ 4 | { 5 | "name": "x86-Debug", 6 | "generator": "Ninja", 7 | "configurationType": "Debug", 8 | "inheritEnvironments": [ "msvc_x86" ], 9 | "buildRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 10 | "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", 11 | "cmakeCommandArgs": "", 12 | "buildCommandArgs": "-v", 13 | "ctestCommandArgs": "" 14 | }, 15 | { 16 | "name": "x86-Release", 17 | "generator": "Ninja", 18 | "configurationType": "RelWithDebInfo", 19 | "inheritEnvironments": [ "msvc_x86" ], 20 | "buildRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 21 | "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", 22 | "cmakeCommandArgs": "", 23 | "buildCommandArgs": "-v", 24 | "ctestCommandArgs": "" 25 | }, 26 | { 27 | "name": "x64-Debug", 28 | "generator": "Ninja", 29 | "configurationType": "Debug", 30 | "inheritEnvironments": [ "msvc_x64_x64" ], 31 | "buildRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 32 | "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", 33 | "cmakeCommandArgs": "", 34 | "buildCommandArgs": "-v", 35 | "ctestCommandArgs": "" 36 | }, 37 | { 38 | "name": "x64-Release", 39 | "generator": "Ninja", 40 | "configurationType": "RelWithDebInfo", 41 | "inheritEnvironments": [ "msvc_x64_x64" ], 42 | "buildRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 43 | "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", 44 | "cmakeCommandArgs": "", 45 | "buildCommandArgs": "-v", 46 | "ctestCommandArgs": "" 47 | }, 48 | { 49 | "name": "Linux-Debug", 50 | "generator": "Unix Makefiles", 51 | "remoteMachineName": "${defaultRemoteMachineName}", 52 | "configurationType": "Debug", 53 | "remoteCMakeListsRoot": "/var/tmp/src/${workspaceHash}/${name}", 54 | "cmakeExecutable": "/usr/local/bin/cmake", 55 | "buildRoot": "${env.LOCALAPPDATA}\\CMakeBuilds\\${workspaceHash}\\build\\${name}", 56 | "remoteBuildRoot": "/var/tmp/build/${workspaceHash}/build/${name}", 57 | "remoteCopySources": true, 58 | "remoteCopySourcesOutputVerbosity": "Normal", 59 | "remoteCopySourcesConcurrentCopies": "10", 60 | "cmakeCommandArgs": "", 61 | "buildCommandArgs": "", 62 | "ctestCommandArgs": "", 63 | "inheritEnvironments": [ "linux-x64" ] 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 TheWisp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Talk slide 2 | https://docs.google.com/presentation/d/1Y5JktAemPYNDmVJ5-3f_KQH1LTDmey-YJFAnly0Cxpo/edit?usp=sharing 3 | 4 | # A boost::signals2 Compatible Implementation 5 | See https://github.com/TheWisp/signals 6 | 7 | # Introduction 8 | C++ did not natively provide an equivalent to events or delegates in other languages until C++11, and even though the added `std::function` is nice and easy to use, it has some non-negligible performance overhead making it hard to replace the existing event notification systems, which are usually either hard-coded, or based on observer-pattern interfaces. Improvements or replacements to `std::function` have been hot topics every once a while, e.g. `function_view`. At any time, there are probably more than two dozens of active libraries about signals / events / delegates on Github. Each comes with a slightly different flavor, for instance, some want to be more generic and flexible, while some others want to be thread-safe. For the applications I've been working on, games and game engines, it is extremely important to keep everything fastest possible. I want to find a multicast-delegate that beats interface-based observers on performance. I want to have an alternative, equivalent in speed solution to hard-coded function calls. When it is fast enough, we can afford to use it everywhere to prevent tight couplings or design-pattern hell situations. 9 | 10 | Long before C++11, this legendary article https://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible pointed out that the usage of member function pointers can save overhead compared to virtual-based type-erasures. The topic eventually got refined in https://www.codeproject.com/articles/11015/the-impossibly-fast-c-delegates and further in https://www.codeproject.com/Articles/1170503/The-Impossibly-Fast-Cplusplus-Delegates-Fixed. The performance is close to perfect, but the syntax isn't. `auto dInstance = decltype(d)::create(&sample);` is far too mouthful, and contains a fundamental design flaw: If the delegate (the instance here) knows its target function upfront, there is no meaning to use a delegate at all - just use the bound function instead. In order to be useful, the delegate (= the sender) needs to be decoupled from the target (= receiver), which means the target needs to know the type of the sender, but not the other way around. Lastly, C++17 provides a new mechanism, `template`, which can eliminate the need of mentioning the type of value in template arguments, such as ``. 11 | 12 | In this project, I will demonstrate how to design a clean, foolproof event (aka multi-cast delegate) system that satisfies: 13 | * Max performance - one pointer indirection 14 | * RAII for connection and disconnection 15 | * No inheritance in user code 16 | 17 | Ideally, the user-code would look like this: 18 | ```cpp 19 | struct Foo 20 | { 21 | //defines the container for listeners 22 | Event update; 23 | }; 24 | 25 | struct Bar 26 | { 27 | //callback method 28 | void onFooUpdate(const Foo& foo); 29 | 30 | //connection object, can be default constructed (disconnected) 31 | 32 | Listener<&Foo::update, &Bar::onFooUpdate> m_listenerFooUpdate; 33 | 34 | //or connected on construction 35 | //Listener<&Foo::update, &Bar::onFooUpdate> m_listenerFooUpdate { some_obj, this }; 36 | 37 | //connect 38 | void someInitFunc(Foo* foo) 39 | { 40 | m_listenerFooUpdate.connect(foo, this); 41 | } 42 | }; 43 | ``` 44 | 45 | 46 | -------------------------------------------------------------------------------- /Test.h: -------------------------------------------------------------------------------- 1 | //NOTE: intended to be included multiple times 2 | 3 | struct Foo 4 | { 5 | Event evt; 6 | }; 7 | 8 | struct Bar 9 | { 10 | void onEvt(float delta) 11 | { 12 | total += delta; 13 | } 14 | 15 | Listener<&Foo::evt, &Bar::onEvt> listener; 16 | 17 | Bar(Foo& foo) 18 | : listener(&foo, this) 19 | { 20 | } 21 | 22 | volatile float total = 0.f; 23 | }; 24 | 25 | Foo g_foo; 26 | std::function g_func; 27 | 28 | 29 | namespace ReentranceTest 30 | { 31 | struct Foo 32 | { 33 | Event evt; 34 | }; 35 | 36 | struct Bar 37 | { 38 | void onEvt(int count); 39 | 40 | Listener<&Foo::evt, &Bar::onEvt> listener; 41 | Foo* m_foo; 42 | int x = 0; 43 | bool flag; 44 | 45 | Bar(Foo& foo, bool flag = true) 46 | : listener(&foo, this) 47 | , flag(flag) 48 | { 49 | m_foo = &foo; 50 | } 51 | }; 52 | 53 | std::vector> dyn_listeners; 54 | 55 | void test() 56 | { 57 | dyn_listeners.clear(); 58 | 59 | Foo foo; 60 | for (int i = 0, n = rand() % 10; i < n; i++) 61 | dyn_listeners.emplace_back(new Bar(foo, false)); 62 | 63 | Bar bar(foo); 64 | 65 | foo.evt(3); 66 | assert(bar.x == 3); 67 | } 68 | 69 | inline void Bar::onEvt(int count) 70 | { 71 | std::cout << "Calling " << this << std::endl; 72 | if (flag) { 73 | if (count > 0) { 74 | this->x++; 75 | 76 | //purposely add or remove listeners 77 | if (rand() & 1) 78 | dyn_listeners.emplace_back(new Bar(*m_foo, false)); 79 | else if (!dyn_listeners.empty()) 80 | dyn_listeners.erase(dyn_listeners.begin() + rand() % dyn_listeners.size()); 81 | m_foo->evt(count - 1); 82 | } 83 | } 84 | } 85 | } 86 | 87 | namespace MoveTest 88 | { 89 | void test() 90 | { 91 | Foo foo; 92 | Bar bar(foo); 93 | Bar bar2 = std::move(bar); 94 | foo.evt(1); 95 | assert(bar2.total == 1); 96 | } 97 | } 98 | 99 | void test() 100 | { 101 | //basic usage 102 | { 103 | Foo foo; 104 | Bar bar(foo); 105 | foo.evt(3.14f); 106 | assert(bar.total == 3.14f); 107 | foo.evt(3.14f); 108 | assert(bar.total == 6.28f); 109 | } 110 | 111 | //default construct 112 | { 113 | Listener<&Foo::evt, &Bar::onEvt> listener; 114 | (void)listener; 115 | } 116 | 117 | //scope safety (listener) 118 | { 119 | Foo foo; 120 | { 121 | Bar bar1(foo), * bar3; 122 | { 123 | Bar bar2(foo); 124 | bar3 = new Bar(foo); 125 | foo.evt(1.f); 126 | assert(bar1.total == 1); 127 | assert(bar2.total == 1); 128 | assert(bar3->total == 1); 129 | } 130 | foo.evt(1.f); 131 | assert(bar1.total == 2); 132 | assert(bar3->total == 2); 133 | } 134 | foo.evt(1.f); 135 | } 136 | 137 | //reentrance and modification safety 138 | ReentranceTest::test(); 139 | 140 | //move 141 | MoveTest::test(); 142 | } 143 | -------------------------------------------------------------------------------- /event.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | template struct Event; 8 | template struct ListenerBase; 9 | template struct Listener; 10 | 11 | template 12 | struct ListenerBase 13 | { 14 | using CallbackFuncType = void(void*, A...); 15 | void* object = nullptr; 16 | CallbackFuncType* func = nullptr; 17 | ListenerBase* prev = nullptr; 18 | ListenerBase* next = nullptr; 19 | Event* event = nullptr; 20 | }; 21 | 22 | template 23 | struct Event 24 | { 25 | using CallbackFuncType = void(void*, A...); 26 | using ListenerType = ListenerBase; 27 | 28 | ~Event() 29 | { 30 | for (auto listener = head; listener != nullptr; ) 31 | { 32 | auto next = listener->next; 33 | listener->event = nullptr; 34 | listener->prev = nullptr; 35 | listener->next = nullptr; 36 | listener = next; 37 | } 38 | } 39 | 40 | template 41 | void operator()(ActualArgsT&&... args) 42 | { 43 | if (first_func) { 44 | first_func(first_object, std::forward(args)...); 45 | } 46 | else if (head) // at least two 47 | { 48 | auto listener = head, next = head; 49 | for (auto pre_tail = tail->prev; listener != pre_tail; listener = next) 50 | { 51 | next = listener->next; 52 | //this prefetches the next next pointer 53 | _mm_prefetch((const char*)next->next, _MM_HINT_T0); 54 | 55 | //this prefetches next function and object 56 | _mm_prefetch((const char*)next->func, _MM_HINT_T0); 57 | _mm_prefetch((const char*)next->object, _MM_HINT_T0); 58 | listener->func(listener->object, std::forward(args)...); 59 | } 60 | 61 | //now listener is the pre-tail 62 | { 63 | _mm_prefetch((const char*)tail->func, _MM_HINT_T0); 64 | _mm_prefetch((const char*)tail->object, _MM_HINT_T0); 65 | listener->func(listener->object, std::forward(args)...); 66 | } 67 | 68 | tail->func(tail->object, std::forward(args)...); 69 | } 70 | } 71 | 72 | //Caches the callback of head as a small-object optimization for single callback 73 | CallbackFuncType* first_func = nullptr; 74 | void* first_object = nullptr; 75 | 76 | ListenerType* head = nullptr; 77 | ListenerType* tail = nullptr; 78 | std::unordered_map mapping_by_func; 79 | 80 | void add(ListenerType* listener) 81 | { 82 | if (mapping_by_func.find(listener->func) == mapping_by_func.end()) 83 | { 84 | if (!head) 85 | { 86 | head = tail = listener; 87 | listener->prev = listener->next = nullptr; 88 | first_func = listener->func; 89 | first_object = listener->object; 90 | } 91 | else 92 | { 93 | tail->next = listener; 94 | listener->prev = tail; 95 | listener->next = nullptr; 96 | tail = listener; 97 | first_func = nullptr; //no longer single function 98 | first_object = nullptr; 99 | } 100 | mapping_by_func[listener->func] = listener; 101 | } 102 | else //found the listener group of the same callback 103 | { 104 | ListenerType*& groupedHead = mapping_by_func[listener->func]; 105 | listener->next = groupedHead->next; 106 | listener->prev = groupedHead; 107 | if (groupedHead->next) 108 | groupedHead->next->prev = listener; 109 | groupedHead->next = listener; 110 | if (groupedHead == tail) 111 | tail = listener; 112 | groupedHead = listener; 113 | first_func = nullptr; //no longer single function 114 | first_object = nullptr; 115 | } 116 | } 117 | 118 | void remove(ListenerType* listener) 119 | { 120 | if (tail == listener) 121 | tail = listener->prev; 122 | if (head == listener) { 123 | head = listener->next; 124 | first_func = head && head == tail ? head->func : nullptr; 125 | first_object = head && head == tail ? head->object : nullptr; 126 | } 127 | if (mapping_by_func[listener->func] == listener) 128 | { 129 | if (listener->prev && listener->prev->func == listener->func) 130 | mapping_by_func[listener->func] = listener->prev; 131 | else mapping_by_func.erase(listener->func); 132 | } 133 | 134 | if (listener->prev) 135 | listener->prev->next = listener->next; 136 | if (listener->next) 137 | listener->next->prev = listener->prev; 138 | } 139 | 140 | void replace(ListenerType* from, ListenerType* to) 141 | { 142 | if (mapping_by_func[from->func] == from) 143 | mapping_by_func[from->func] = to; 144 | if (tail == from) 145 | tail = to; 146 | if (head == from) { 147 | head = to; 148 | first_func = head && head == tail ? head->func : nullptr; 149 | first_object = head && head == tail ? head->object : nullptr; 150 | } 151 | if (from->prev) 152 | from->prev->next = to; 153 | } 154 | }; 155 | 156 | template< 157 | class EventClass, typename... EventFuncArgsT, Event EventClass:: * EventPtr, 158 | class CallbackClass, typename CallbackFuncType, CallbackFuncType CallbackClass:: * CallbackPtr 159 | > 160 | struct Listener : public ListenerBase 161 | { 162 | using Base = ListenerBase; 163 | 164 | Listener() = default; 165 | 166 | Listener(EventClass* eventSender, CallbackClass* eventReceiver) 167 | { 168 | connect(eventSender, eventReceiver); 169 | } 170 | 171 | Listener(const Listener&) = delete; 172 | Listener(Listener&& other) : Base(other) 173 | { 174 | //fix the object 175 | if (other.object) 176 | this->object = (void*)((size_t)this - ((size_t)&other - (size_t)other.object)); 177 | 178 | //fix the links 179 | if (this->event) 180 | this->event->replace(&other, this); 181 | other.object = nullptr; 182 | other.func = nullptr; 183 | other.prev = nullptr; 184 | other.next = nullptr; 185 | other.event = nullptr; 186 | } 187 | 188 | void connect(EventClass* eventSender, CallbackClass* eventReceiver) 189 | { 190 | //potential optimization: don't store event, and disregard the ordering within the same callback group 191 | //potential optimization: use another indirection instead of this->event, so that when event goes out of the scope, we wouldn't need to reset every listener 192 | this->object = eventReceiver; 193 | this->func = call; 194 | this->event = &(eventSender->*EventPtr); 195 | this->event->add(this); 196 | } 197 | 198 | void disconnect() 199 | { 200 | if (this->event) this->event->remove(this); 201 | } 202 | 203 | ~Listener() 204 | { 205 | disconnect(); 206 | } 207 | 208 | static void call(void* obj, EventFuncArgsT ... args) 209 | { 210 | (static_cast(obj)->*CallbackPtr)(args...); 211 | } 212 | }; -------------------------------------------------------------------------------- /event_vector.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ifevec 4 | { 5 | template struct Event; 6 | 7 | struct EventBase; 8 | struct ListenerBase 9 | { 10 | EventBase* m_Event = nullptr; 11 | size_t m_idx; 12 | }; 13 | 14 | struct EventBase 15 | { 16 | struct Callback 17 | { 18 | void* object; 19 | void* func; 20 | }; 21 | 22 | union 23 | { 24 | struct 25 | { 26 | Callback* m_Callbacks; 27 | ListenerBase** m_Listeners; 28 | size_t m_Size : 8 * sizeof size_t - 2; 29 | size_t m_Calling : 1;//represents if we are in the middle of calling back, no matter recursive or not. 30 | size_t m_Dirty : 1; 31 | }; 32 | 33 | struct 34 | { 35 | Callback m_SingleCb; 36 | ListenerBase* m_SingleListener; 37 | }; 38 | }; 39 | 40 | size_t m_Capacity : 8 * sizeof size_t - 1; 41 | size_t m_Multi : 1; //flag, 0 => holding single listener 42 | 43 | EventBase() : m_SingleCb(), m_SingleListener(), m_Capacity(), m_Multi() 44 | { 45 | } 46 | 47 | ~EventBase() 48 | { 49 | if (m_Multi) 50 | { 51 | for (ListenerBase** l = m_Listeners, **end = m_Listeners + m_Size; l < end; ++l) 52 | if (*l) (*l)->m_Event = nullptr; 53 | free(m_Listeners); 54 | free(m_Callbacks); 55 | } 56 | else 57 | { 58 | if (m_SingleListener) 59 | m_SingleListener->m_Event = nullptr; 60 | } 61 | } 62 | 63 | void add(void* object, void* func, ListenerBase* pListener) 64 | { 65 | if (!m_Multi) 66 | { 67 | if (!m_SingleListener) 68 | { 69 | m_SingleCb.object = object; 70 | m_SingleCb.func = func; 71 | m_SingleListener = pListener; 72 | pListener->m_idx = 0; 73 | } 74 | else 75 | { 76 | m_Multi = 1; 77 | m_Capacity = 2;//initially 2 slots 78 | Callback* callbacks = (Callback*)malloc(sizeof Callback * m_Capacity); 79 | callbacks[0] = m_SingleCb; 80 | callbacks[1].object = object; 81 | callbacks[1].func = func; 82 | ListenerBase** listeners = (ListenerBase * *)malloc(sizeof(void*) * m_Capacity); 83 | listeners[0] = m_SingleListener; 84 | listeners[1] = pListener; 85 | m_Callbacks = callbacks; 86 | m_Listeners = listeners; 87 | m_Size = 2; 88 | m_Calling = 0; 89 | m_Dirty = 0; 90 | pListener->m_idx = 1; 91 | } 92 | } 93 | else 94 | { 95 | if (m_Size == m_Capacity) 96 | { 97 | //grow and copy 98 | m_Capacity = (m_Capacity * 3) / 2; //grow by 1.5 2->3->4->6->9... 99 | m_Callbacks = (Callback*)realloc(m_Callbacks, sizeof Callback * m_Capacity); 100 | m_Listeners = (ListenerBase * *)realloc(m_Listeners, sizeof(void*) * m_Capacity); 101 | } 102 | m_Callbacks[m_Size].object = object; 103 | m_Callbacks[m_Size].func = func; 104 | m_Listeners[m_Size] = pListener; 105 | pListener->m_idx = m_Size++; 106 | } 107 | } 108 | 109 | void remove(size_t idx) 110 | { 111 | if (m_Multi) 112 | { 113 | m_Callbacks[idx].object = m_Callbacks[idx].func = nullptr; 114 | m_Listeners[idx] = nullptr; 115 | m_Dirty = 1; 116 | } 117 | else 118 | { 119 | m_SingleCb.object = nullptr; 120 | m_SingleCb.func = nullptr; 121 | m_SingleListener = nullptr; 122 | } 123 | } 124 | 125 | void replace(size_t idx, void* object, ListenerBase* listener) 126 | { 127 | if (m_Multi) 128 | { 129 | m_Callbacks[idx].object = object; 130 | m_Listeners[idx] = listener; 131 | } 132 | else 133 | { 134 | m_SingleCb.object = object; 135 | m_SingleListener = listener; 136 | } 137 | } 138 | 139 | }; 140 | 141 | template 142 | struct Event : EventBase 143 | { 144 | template friend struct Listener; 145 | using CallbackFuncType = R(void*, A...); 146 | 147 | template 148 | void operator()(ActualArgsT&& ... args) 149 | { 150 | if (m_Multi) 151 | { 152 | bool isRecursion = m_Calling; 153 | if (!m_Calling) m_Calling = 1; 154 | 155 | //TODO: using index is necessary because of potential reallocation during callback. Can we make it faster? (yet another flag?) 156 | //TODO: alternatively, simply use another buffer for newly added ones during callback? 157 | for (size_t i = 0; i < m_Size; ++i) 158 | { 159 | auto& cb = m_Callbacks[i]; 160 | if (cb.func) 161 | static_cast(cb.func)(cb.object, std::forward(args)...); 162 | } 163 | 164 | if (!isRecursion) 165 | { 166 | m_Calling = 0; 167 | 168 | if (m_Dirty) 169 | { 170 | m_Dirty = 0; 171 | //remove all empty slots while patching the stored index in the listener 172 | size_t sz = 0; 173 | for (size_t i = 0; i < m_Size; ++i) 174 | { 175 | if (m_Listeners[i]) { 176 | m_Listeners[sz] = m_Listeners[i]; 177 | m_Callbacks[sz] = m_Callbacks[i]; 178 | m_Listeners[sz]->m_idx = sz; 179 | ++sz; 180 | } 181 | } 182 | m_Size = sz; 183 | } 184 | } 185 | } 186 | else 187 | { 188 | if (m_SingleCb.func) 189 | static_cast(m_SingleCb.func)(m_SingleCb.object, std::forward(args)...); 190 | } 191 | } 192 | }; 193 | 194 | static_assert(sizeof Event == 4 * sizeof size_t); 195 | 196 | template struct Listener; 197 | 198 | template< 199 | class EventClass, typename... EventFuncArgsT, Event EventClass::* EventPtr, 200 | class CallbackClass, typename CallbackFuncType, CallbackFuncType CallbackClass::* CallbackPtr > 201 | struct Listener : public ListenerBase 202 | { 203 | using EventType = Event; 204 | 205 | CallbackClass* m_object = nullptr; 206 | 207 | Listener() = default; 208 | 209 | Listener(EventClass* eventSender, CallbackClass* eventReceiver) 210 | { 211 | connect(eventSender, eventReceiver); 212 | } 213 | 214 | Listener(const Listener&) = delete; 215 | Listener(Listener&& other) : ListenerBase(other) 216 | { 217 | //fix the object 218 | if (other.m_object) 219 | m_object = (CallbackClass*)((size_t)this - ((size_t)& other - (size_t)other.m_object)); 220 | 221 | //fix the links 222 | if (m_Event) 223 | m_Event->replace(m_idx, m_object, this); 224 | 225 | other.m_object = nullptr; 226 | other.m_Event = nullptr; 227 | } 228 | 229 | void connect(EventClass* eventSender, CallbackClass* eventReceiver) 230 | { 231 | m_object = eventReceiver; 232 | m_Event = &(eventSender->*EventPtr); 233 | m_Event->add(eventReceiver, +[](void* obj, EventFuncArgsT ... args) {(static_cast(obj)->*CallbackPtr)(args...); }, this); 234 | } 235 | 236 | void disconnect() 237 | { 238 | if (m_Event) m_Event->remove(m_idx); 239 | } 240 | 241 | ~Listener() 242 | { 243 | disconnect(); 244 | } 245 | }; 246 | } 247 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "event.hpp" 4 | #include "event_vector.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace TestEventList 14 | { 15 | #include "Test.h" 16 | 17 | //this part of the test inspects specific layouts in the memory, thus only applies to the linked-list implementation 18 | namespace GroupingTest 19 | { 20 | template 21 | struct Bar 22 | { 23 | void onEvt(float f) { 24 | str += std::to_string(x); 25 | } 26 | 27 | Listener<&Foo::evt, &Bar::onEvt> listener; 28 | 29 | Bar(Foo& foo, std::string& str) 30 | : listener(&foo, this) 31 | , str(str) 32 | { 33 | 34 | } 35 | 36 | std::string& str; 37 | }; 38 | 39 | void test() 40 | { 41 | Foo foo; 42 | std::string str; 43 | 44 | Bar<1> bar1(foo, str); 45 | { 46 | Bar<2> bar2(foo, str); 47 | { 48 | Bar<1> bar3(foo, str); 49 | Bar<2> bar4(foo, str); 50 | //bar2 and bar4 should be grouped together 51 | foo.evt(1); 52 | assert(str == "1122" || str == "2211"); 53 | } 54 | str.clear(); 55 | foo.evt(1); 56 | assert(str == "12" || str == "21"); 57 | } 58 | str.clear(); 59 | Bar<1> bar2(foo, str); 60 | foo.evt(1); 61 | assert(str == "11"); 62 | } 63 | } 64 | } 65 | 66 | namespace TestEventVector 67 | { 68 | using ifevec::Event; 69 | using ifevec::Listener; 70 | #include "Test.h" 71 | } 72 | 73 | namespace BenchEventList 74 | { 75 | #include "BenchEventVsCall.h" 76 | } 77 | 78 | namespace BenchEventVector 79 | { 80 | using ifevec::Event; 81 | using ifevec::Listener; 82 | #include "BenchEventVsCall.h" 83 | } 84 | 85 | int main() 86 | { 87 | srand((unsigned)time(NULL)); 88 | //TestEventList::test(); 89 | //TestEventList::GroupingTest::test(); 90 | TestEventVector::test(); 91 | 92 | std::cout << "\n"; 93 | std::cout << "Benchmarking implementation using linked list:\n"; 94 | BenchEventList::test(); 95 | std::cout << "\n"; 96 | std::cout << "Benchmarking implementation using vector:\n"; 97 | BenchEventVector::test(); 98 | } 99 | 100 | 101 | --------------------------------------------------------------------------------