MCRO
C++23 utilities for Unreal Engine.
Loading...
Searching...
No Matches
EventDelegate.h
Go to the documentation of this file.
1/** @noop License Comment
2 * @file
3 * @copyright
4 * This Source Code is subject to the terms of the Mozilla Public License, v2.0.
5 * If a copy of the MPL was not distributed with this file You can obtain one at
6 * https://mozilla.org/MPL/2.0/
7 *
8 * @author David Mórász
9 * @date 2025
10 */
11
12#pragma once
13
14#include "CoreMinimal.h"
15#include "Mcro/FunctionTraits.h"
16#include "Mcro/Construct.h"
19
20namespace Mcro::Delegates
21{
22 using namespace Mcro::FunctionTraits;
23 using namespace Mcro::Construct;
24
25 /** Settings for the TEventDelegate class, which defines optional behavior when adding a binding to it */
27 {
28 /** The event delegate will act the same as a TMulticastDelegate */
30
31 /** The binding will be automatically removed after the next broadcast */
32 InvokeOnce = 1 << 0,
33
34 /** The binding will be executed immediately if the delegate has already been broadcasted */
35 BelatedInvoke = 1 << 1,
36
37 /**
38 * Attempt to copy arguments when storing them for belated invokes, instead of perfect
39 * forwarding them. This is only considered from the template argument
40 */
41 CopyArguments = 1 << 2,
42
43 /** Enable mutex locks around adding/broadcasting delegates. Only considered in DefaultInvokeMode */
44 ThreadSafeEvent = 1 << 3
45 };
46
47 template <typename Function, int32 DefaultInvokeMode = DefaultInvocation>
49
50 /**
51 * "Extension" of a common TMulticastDelegate. It allows to define optional "settings" when
52 * ad1ding a binding, in order to:
53 * - Remove the binding after the next broadcast
54 * - Execute the associated event immediately if the delegate has already been broadcast
55 * - Allow comfortable chaining
56 *
57 * The delegate can be defined with settings that will be applied by default to all bindings
58 * (but the behavior can still be changed per-binding if needed).
59 *
60 * Example usage:
61 *
62 * @code
63 * // The delegate will use default settings (i.e. the behavior will be the same as a TMulticastDelegate by default)
64 * using FMyNativeDelegate = TEventDelegate<void(int32 someParam)>;
65 *
66 * // Fire a new binding immediately if the delegate has been already broadcasted
67 * using FMyNativeDelegate = TEventDelegate<void(int32 someParam), EInvokeMode::BelatedInvoke>;
68 *
69 * // Fire a new binding immediately if the delegate has been already broadcasted,
70 * // AND the binding will be removed after the next broadcast
71 * using FMyNativeDelegate = TEventDelegate<void(int32 someParam), EInvokeMode::BelatedInvoke | EInvokeMode::InvokeOnce>;
72 * @endcode
73 */
74 template <typename... Args, int32 DefaultInvokeMode>
75 class TEventDelegate<void(Args...), DefaultInvokeMode>
76 {
77 public:
78 using MutexLock = std::conditional_t<static_cast<bool>(DefaultInvokeMode & ThreadSafeEvent), FScopeLock, FVoid>;
79
80 using FunctionSignature = void(Args...);
81 using FDelegate = TDelegate<FunctionSignature, FDefaultDelegateUserPolicy>;
82
83 using ArgumentsCache = std::conditional_t<
84 DefaultInvokeMode & CopyArguments,
85 TTuple<std::decay_t<Args>...>,
86 TTuple<Args...>
87 >;
88
89 void Broadcast(Args&&... args)
90 {
91 MutexLock lock(&Mutex.Get());
92 bHasBroadcasted = true;
93 if constexpr (DefaultInvokeMode & CopyArguments)
94 Cache = ArgumentsCache(args...);
95 else
96 Cache = ArgumentsCache(Forward<Args>(args)...);
97
98 MulticastDelegate.Broadcast(Forward<Args>(args)...);
99
100 for (const FDelegateHandle& handle : OnlyNextDelegates)
101 MulticastDelegate.Remove(handle);
102
103 OnlyNextDelegates.Empty();
104 }
105
106 /**
107 * Create a delegate object which is broadcasting this event. This is useful for chaining
108 * events together like so:
109 * ```
110 * MyEvent.Add(MyOtherEvent.Delegation(this));
111 * ```
112 *
113 * @param object Optionally bind an object to the event delegation
114 */
115 template <typename... OptionalObject> requires (sizeof...(OptionalObject) <= 1)
116 FDelegate Delegation(OptionalObject&&... object)
117 {
118 return From(Forward<OptionalObject>(object)..., &TEventDelegate::Broadcast);
119 };
120
121 /**
122 * Adds a new delegate to the event delegate.
123 *
124 * @param delegate The delegate to bind
125 *
126 * @param invokeMode
127 * The (optional) settings to use for this binding. Not passing anything means that it will
128 * use the default settigns for this event delegate
129 *
130 * @return Handle to the delegate
131 */
132 FDelegateHandle Add(FDelegate delegate, const EInvokeMode& invokeMode = DefaultInvocation)
133 {
134 MutexLock lock(&Mutex.Get());
135 return AddInternal(delegate, invokeMode);
136 }
137
138 /**
139 * Bind multiple delegates at once, useful for initial mandatory bindings
140 * @return This event
141 */
142 template <CSameAs<FDelegate>... Delegates>
143 TEventDelegate& With(Delegates&&... delegates)
144 {
145 MutexLock lock(&Mutex.Get());
146 (AddInternal(delegates, DefaultInvocation), ...);
147 return *this;
148 }
149
151
152 /** Bind multiple delegates at once, useful for initial mandatory bindings */
153 template <CSameAs<FDelegate>... Delegates>
154 TEventDelegate(Delegates... delegates)
155 {
156 WithHelper(Add(delegates)...);
157 }
158
159 /**
160 * Adds a new dynamic delegate to this event delegate.
161 *
162 * @param dynamicDelegate The dynamic delegate to bind
163 *
164 * @param invokeMode
165 * The (optional) settings to use for this binding. Not passing anything means that it will
166 * use the default settigns for this event delegate
167 *
168 * @return Handle to the delegate
169 */
170 template <CDynamicDelegate DynamicDelegateType>
171 FDelegateHandle Add(const DynamicDelegateType& dynamicDelegate, const EInvokeMode& invokeMode = DefaultInvocation)
172 {
173 MutexLock lock(&Mutex.Get());
174 return AddInternal(AsNative(dynamicDelegate), invokeMode, FDelegateHandle(), dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName());
175 }
176
177 /**
178 * Adds the given dynamic delegate only if it's not already bound to this event delegate.
179 *
180 * @param dynamicDelegate The dynamic delegate to bind
181 *
182 * @param invokeMode
183 * The (optional) settings to use for this binding. Not passing anything means that it will
184 * use the default settigns for this event delegate
185 *
186 * @return
187 * Handle to the delegate. If the binding already existed, the handle to the existing
188 * binding is returned
189 */
190 template <CDynamicDelegate DynamicDelegateType>
191 FDelegateHandle AddUnique(const DynamicDelegateType& dynamicDelegate, EInvokeMode invokeMode = DefaultInvocation)
192 {
193 MutexLock lock(&Mutex.Get());
194 return AddUniqueInternal(AsNative(dynamicDelegate), invokeMode, dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName());
195 }
196
197 private:
198 bool RemoveInternal(const FDelegateHandle& delegateHandle)
199 {
200 const bool result = MulticastDelegate.Remove(delegateHandle);
201
202 if (const FBoundUFunction* key = BoundUFunctionsMap.FindKey(delegateHandle))
203 BoundUFunctionsMap.Remove(*key);
204
205 OnlyNextDelegates.Remove(delegateHandle);
206
207 return result;
208 }
209
210 public:
211 /**
212 * Remove the binding associated to the given handle
213 *
214 * @param delegateHandle Handle of the binding to remove
215 *
216 * @return True if a binding was removed; false otherwise
217 */
218 bool Remove(const FDelegateHandle& delegateHandle)
219 {
220 MutexLock lock(&Mutex.Get());
221 return RemoveInternal(delegateHandle);
222 }
223
224 /**
225 * Remove the binding associated to the dynamic delegate.
226 *
227 * @param dynamicDelegate The dynamic delegate to remove
228 *
229 * @return True if a binding was removed; false otherwise
230 */
231 template <class DynamicDelegateType>
232 bool Remove(const DynamicDelegateType& dynamicDelegate)
233 {
234 MutexLock lock(&Mutex.Get());
235 FDelegateHandle delegateHandle;
236 if (BoundUFunctionsMap.RemoveAndCopyValue(FBoundUFunction(dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName()), delegateHandle))
237 return RemoveInternal(delegateHandle);
238
239 return false;
240 }
241
242 /**
243 * Removes all binding associated to the given object
244 *
245 * @param inUserObject The object to remove the bindings for
246 *
247 * @return The total number of bindings that were removed
248 */
249 int32 RemoveAll(const void* inUserObject)
250 {
251 MutexLock lock(&Mutex.Get());
252 for (auto it = BoundUFunctionsMap.CreateIterator(); it; ++it)
253 if (!it.Key().Key.IsValid() || it.Key().Key.Get() == inUserObject)
254 it.RemoveCurrent();
255
256 return MulticastDelegate.RemoveAll(inUserObject);
257 }
258
259 /** Resets all states of this event delegate to their default. */
260 void Reset()
261 {
262 MutexLock lock(&Mutex.Get());
263 MulticastDelegate.Clear();
264 OnlyNextDelegates.Reset();
265 BoundUFunctionsMap.Reset();
266 bHasBroadcasted = false;
267 Cache.Reset();
268 }
269
270 /** @returns true if this event delegate was ever broadcasted. */
271 bool IsBroadcasted() const
272 {
273 return bHasBroadcasted;
274 }
275
276 private:
277
278 FDelegateHandle AddUniqueInternal(
279 FDelegate delegate,
280 const EInvokeMode& invokeMode,
281 const UObject* boundObject,
282 const FName& boundFunctionName
283 ) {
284 FDelegateHandle uniqueHandle;
285
286 if (const FDelegateHandle* delegateHandle = BoundUFunctionsMap.Find(FBoundUFunction(boundObject, boundFunctionName)))
287 uniqueHandle = *delegateHandle;
288
289 return AddInternal(delegate, invokeMode, uniqueHandle, boundObject, boundFunctionName);
290 }
291
292 FDelegateHandle AddInternal(
293 FDelegate delegate,
294 EInvokeMode invokeMode,
295 FDelegateHandle const& uniqueHandle = FDelegateHandle(),
296 const UObject* boundObject = nullptr,
297 FName const& boundFunctionName = NAME_None
298 ) {
299 const int32 actualInvokeMode = invokeMode == DefaultInvocation ? DefaultInvokeMode : invokeMode;
300
301 if (bHasBroadcasted && actualInvokeMode & (BelatedInvoke | InvokeOnce))
302 {
303 CallBelated(delegate);
304 return FDelegateHandle();
305 }
306
307 FDelegateHandle outputHandle = uniqueHandle;
308 if (!outputHandle.IsValid())
309 {
310 outputHandle = MulticastDelegate.Add(delegate);
311
312 if (boundObject && boundFunctionName != NAME_None)
313 BoundUFunctionsMap.Add(FBoundUFunction(boundObject, boundFunctionName), outputHandle);
314
315 if (actualInvokeMode & InvokeOnce)
316 OnlyNextDelegates.Add(outputHandle);
317 }
318
319 if (bHasBroadcasted && actualInvokeMode & BelatedInvoke)
320 CallBelated(delegate);
321
322 return outputHandle;
323 }
324
325 void CallBelated(FDelegate& delegate)
326 {
327 InvokeWithTuple(&delegate, &FDelegate::Execute, Cache.GetValue());
328 }
329
330 using FBoundUFunction = TPair<TWeakObjectPtr<const UObject>, FName>;
331
332 bool bHasBroadcasted = false;
334 TSet<FDelegateHandle> OnlyNextDelegates;
335 TMap<FBoundUFunction, FDelegateHandle> BoundUFunctionsMap;
336 TOptional<ArgumentsCache> Cache;
337 TMulticastDelegate<void(Args...), FDefaultDelegateUserPolicy> MulticastDelegate;
338 };
339
340 template <typename Signature, int32 Flags = 0>
342
343 template <typename Signature, int32 Flags = 0>
345
346 template <typename Signature, int32 Flags = 0>
348
349 template <typename Signature, int32 Flags = 0>
351
352 template <typename Signature, int32 Flags = 0>
354
355 template <typename Signature, int32 Flags = 0>
357
358 template <typename Signature, int32 Flags = 0>
361 >;
362
363 /** Map the input dynamic multicast delegate to a conceptually compatible native event delegate type */
364 template <CDynamicMulticastDelegate Dynamic, int32 DefaultSettings = DefaultInvocation>
366 {
368 };
369
370 /** Map the input dynamic multicast delegate to a conceptually compatible native event delegate type */
371 template <typename Dynamic, int32 DefaultSettings = DefaultInvocation>
373}
FDelegateHandle Add(FDelegate delegate, const EInvokeMode &invokeMode=DefaultInvocation)
std::conditional_t< DefaultInvokeMode &CopyArguments, TTuple< std::decay_t< Args >... >, TTuple< Args... > > ArgumentsCache
TDelegate< FunctionSignature, FDefaultDelegateUserPolicy > FDelegate
FDelegateHandle AddUnique(const DynamicDelegateType &dynamicDelegate, EInvokeMode invokeMode=DefaultInvocation)
bool Remove(const DynamicDelegateType &dynamicDelegate)
FDelegateHandle Add(const DynamicDelegateType &dynamicDelegate, const EInvokeMode &invokeMode=DefaultInvocation)
std::conditional_t< static_cast< bool >(DefaultInvokeMode &ThreadSafeEvent), FScopeLock, FVoid > MutexLock
typename TNativeEvent_Struct< Dynamic, DefaultSettings >::Type TNativeEvent
NativeDelegateType AsNative(Dynamic &&dynamicDelegate)
Definition AsNative.h:60
TInferredDelegate< Function, Captures... > From(Function func, const Captures &... captures)
TFunction_Return< Function > InvokeWithTuple(Function &&function, TFunction_Arguments< Function > const &arguments)
Definition Void.h:25