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"
19
20namespace Mcro::Delegates
21{
22 using namespace Mcro::FunctionTraits;
23 using namespace Mcro::InitializeOnCopy;
24
25 /** @brief Settings for the TEventDelegate class, which defines optional behavior when adding a binding to it */
27 {
28 /** @brief The binding will be automatically removed after the next broadcast */
29 bool Once = false;
30
31 /** @brief The binding will be executed immediately if the delegate has already been broadcasted */
32 bool Belated = false;
33
34 /**
35 * @brief
36 * Attempt to copy arguments when storing them for belated invokes, instead of perfect
37 * forwarding them. This is only considered from the template argument
38 */
39 bool CacheViaCopy = false;
40
41 /** @brief Enable mutex locks around adding/broadcasting delegates. Only considered in DefaultPolicy */
42 bool ThreadSafe = false;
43
44 /** @brief Merge two policy flags */
45 FORCEINLINE constexpr FEventPolicy With(FEventPolicy const& other) const
46 {
47 return {
48 Once || other.Once,
49 Belated || other.Belated,
51 ThreadSafe || other.ThreadSafe
52 };
53 }
54
55 FORCEINLINE friend constexpr bool operator == (FEventPolicy const& lhs, FEventPolicy const& rhs)
56 {
57 return lhs.Once == rhs.Once
58 && lhs.Belated == rhs.Belated
59 && lhs.CacheViaCopy == rhs.CacheViaCopy
60 && lhs.ThreadSafe == rhs.ThreadSafe
61 ;
62 }
63
64 FORCEINLINE friend constexpr bool operator != (FEventPolicy const& lhs, FEventPolicy const& rhs)
65 {
66 return !(lhs == rhs);
67 }
68
69 /** @brief Is this instance equivalent to a default constructed one */
70 FORCEINLINE constexpr bool IsDefault() const
71 {
72 return *this == FEventPolicy();
73 }
74 };
75
76 /**
77 * @brief
78 * "Extension" of a common TMulticastDelegate. It allows to define optional "flags" when adding a binding,
79 * in order to:
80 * - Remove the binding after the next broadcast
81 * - Execute the associated event immediately if the delegate has already been broadcast
82 * - Allow comfortable chaining
83 *
84 * The delegate can be defined with default settings that will be applied by default to all bindings (but the
85 * behavior can still be changed per-binding if needed (except thread safety and argument retention mode).
86 *
87 * Example usage:
88 *
89 * @code
90 * // The delegate will use default settings (i.e. the behavior will be the same as a TMulticastDelegate by default)
91 * using FMyEventDelegate = TEventDelegate<void(int32 someParam)>;
92 *
93 * // Fire a new binding immediately if the delegate has been already broadcasted
94 * using FMyEventDelegate = TEventDelegate<void(int32 someParam), {.Belated = true}>;
95 *
96 * // Fire a new binding immediately if the delegate has been already broadcasted,
97 * // AND the binding will be removed after the next broadcast
98 * using FMyEventDelegate = TEventDelegate<void(int32 someParam), {.Belated = true, .Once = true}>;
99 * @endcode
100 */
101 template <typename Function, FEventPolicy DefaultPolicy = {}>
103
104 /** @copydoc TEventDelegate */
105 template <typename... Args, FEventPolicy DefaultPolicy>
106 class TEventDelegate<void(Args...), DefaultPolicy>
107 {
108 public:
109 using MutexLock = std::conditional_t<DefaultPolicy.ThreadSafe, FScopeLock, FVoid>;
110
111 using FunctionSignature = void(Args...);
112 using FDelegate = TDelegate<FunctionSignature, FDefaultDelegateUserPolicy>;
113
114 using ArgumentsCache = std::conditional_t<
115 DefaultPolicy.CacheViaCopy,
116 TTuple<std::decay_t<Args>...>,
117 TTuple<Args...>
118 >;
119
120 template <typename... BroadcastArgs>
121 requires CConvertibleTo<TTuple<BroadcastArgs...>, TTuple<Args...>>
122 void Broadcast(BroadcastArgs&&... args)
123 {
124 MutexLock lock(&Mutex.Get());
125 bHasBroadcasted = true;
126 if constexpr (DefaultPolicy.CacheViaCopy)
127 {
128 // this here actually copies twice, instead of once, but if we don't have this temporary variable
129 // it doesn't copy at all. CopyTemp, or function with pass-by-copy parameters didn't help
130 ArgumentsCache cache{args...};
131 Cache = cache;
132 }
133 else
134 Cache = ArgumentsCache(FWD(args)...);
135
136 MulticastDelegate.Broadcast(FWD(args)...);
137
138 for (const FDelegateHandle& handle : OnlyNextDelegates)
139 MulticastDelegate.Remove(handle);
140
141 OnlyNextDelegates.Empty();
142 }
143
144 /**
145 * @brief
146 * Create a delegate object which is broadcasting this event. This is useful for chaining
147 * events together like so:
148 * @code
149 * MyEvent.Add(MyOtherEvent.Delegation(this));
150 * @endcode
151 *
152 * @param object Optionally bind an object to the event delegation
153 */
154 template <typename... OptionalObject> requires (sizeof...(OptionalObject) <= 1)
155 FDelegate Delegation(OptionalObject&&... object)
156 {
157 return InferDelegate::From(FWD(object)..., [this](Args... args) { Broadcast(args...); });
158 };
159
160 /**
161 * @brief Adds a new delegate to the event delegate.
162 *
163 * @param delegate The delegate to bind
164 *
165 * @param policy
166 * The (optional) settings to use for this binding. Not passing anything means that it will
167 * use the default settigns for this event delegate
168 *
169 * @return Handle to the delegate
170 */
171 FDelegateHandle Add(FDelegate delegate, FEventPolicy const& policy = {})
172 {
173 MutexLock lock(&Mutex.Get());
174 return AddInternal(delegate, policy);
175 }
176
177 /**
178 * @brief Bind multiple delegates at once, useful for initial mandatory bindings
179 * @return This event
180 */
181 template <CSameAs<FDelegate>... Delegates>
182 TEventDelegate& With(Delegates&&... delegates)
183 {
184 MutexLock lock(&Mutex.Get());
185 (AddInternal(delegates, {}), ...);
186 return *this;
187 }
188
190
191 /** @brief Bind multiple delegates at once, useful for initial mandatory bindings */
192 template <CSameAs<FDelegate>... Delegates>
193 TEventDelegate(Delegates... delegates)
194 {
195 (AddInternal(delegates, {}), ...);
196 }
197
198 /**
199 * @brief Adds a new dynamic delegate to this event delegate.
200 *
201 * @param dynamicDelegate The dynamic delegate to bind
202 *
203 * @param policy
204 * The (optional) settings to use for this binding. Not passing anything means that it will
205 * use the default settigns for this event delegate
206 *
207 * @return Handle to the delegate
208 */
209 template <CDynamicDelegate DynamicDelegateType>
210 FDelegateHandle Add(const DynamicDelegateType& dynamicDelegate, FEventPolicy const& policy = {})
211 {
212 MutexLock lock(&Mutex.Get());
213 return AddInternal(AsNative(dynamicDelegate), policy, {}, dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName());
214 }
215
216 /**
217 * @brief Adds the given dynamic delegate only if it's not already bound to this event delegate.
218 *
219 * @param dynamicDelegate The dynamic delegate to bind
220 *
221 * @param policy
222 * The (optional) settings to use for this binding. Not passing anything means that it will
223 * use the default settigns for this event delegate
224 *
225 * @return
226 * Handle to the delegate. If the binding already existed, the handle to the existing
227 * binding is returned
228 */
229 template <CDynamicDelegate DynamicDelegateType>
230 FDelegateHandle AddUnique(const DynamicDelegateType& dynamicDelegate, FEventPolicy const& policy = {})
231 {
232 MutexLock lock(&Mutex.Get());
233 return AddUniqueInternal(AsNative(dynamicDelegate), policy, dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName());
234 }
235
236 private:
237 bool RemoveInternal(const FDelegateHandle& delegateHandle)
238 {
239 const bool result = MulticastDelegate.Remove(delegateHandle);
240
241 if (const FBoundUFunction* key = BoundUFunctionsMap.FindKey(delegateHandle))
242 BoundUFunctionsMap.Remove(*key);
243
244 OnlyNextDelegates.Remove(delegateHandle);
245
246 return result;
247 }
248
249 public:
250 /**
251 * @brief Remove the binding associated to the given handle
252 *
253 * @param delegateHandle Handle of the binding to remove
254 *
255 * @return True if a binding was removed; false otherwise
256 */
257 bool Remove(const FDelegateHandle& delegateHandle)
258 {
259 MutexLock lock(&Mutex.Get());
260 return RemoveInternal(delegateHandle);
261 }
262
263 /**
264 * @brief Remove the binding associated to the dynamic delegate.
265 *
266 * @param dynamicDelegate The dynamic delegate to remove
267 *
268 * @return True if a binding was removed; false otherwise
269 */
270 template <class DynamicDelegateType>
271 bool Remove(const DynamicDelegateType& dynamicDelegate)
272 {
273 MutexLock lock(&Mutex.Get());
274 FDelegateHandle delegateHandle;
275 if (BoundUFunctionsMap.RemoveAndCopyValue(FBoundUFunction(dynamicDelegate.GetUObject(), dynamicDelegate.GetFunctionName()), delegateHandle))
276 return RemoveInternal(delegateHandle);
277
278 return false;
279 }
280
281 /**
282 * @brief Removes all binding associated to the given object
283 *
284 * @param inUserObject The object to remove the bindings for
285 *
286 * @return The total number of bindings that were removed
287 */
288 int32 RemoveAll(const void* inUserObject)
289 {
290 MutexLock lock(&Mutex.Get());
291 for (auto it = BoundUFunctionsMap.CreateIterator(); it; ++it)
292 if (!it.Key().Key.IsValid() || it.Key().Key.Get() == inUserObject)
293 it.RemoveCurrent();
294
295 return MulticastDelegate.RemoveAll(inUserObject);
296 }
297
298 /** @brief Resets all states of this event delegate to their default. */
299 void Reset()
300 {
301 MutexLock lock(&Mutex.Get());
302 MulticastDelegate.Clear();
303 OnlyNextDelegates.Reset();
304 BoundUFunctionsMap.Reset();
305 bHasBroadcasted = false;
306 Cache.Reset();
307 }
308
309 /** @returns true if this event delegate was ever broadcasted. */
310 bool IsBroadcasted() const
311 {
312 return bHasBroadcasted;
313 }
314
315 private:
316
317 FDelegateHandle AddUniqueInternal(
318 FDelegate delegate,
319 FEventPolicy const& policy,
320 const UObject* boundObject,
321 const FName& boundFunctionName
322 ) {
323 FDelegateHandle uniqueHandle;
324
325 if (const FDelegateHandle* delegateHandle = BoundUFunctionsMap.Find(FBoundUFunction(boundObject, boundFunctionName)))
326 uniqueHandle = *delegateHandle;
327
328 return AddInternal(delegate, policy, uniqueHandle, boundObject, boundFunctionName);
329 }
330
331 FDelegateHandle AddInternal(
332 FDelegate delegate,
333 FEventPolicy const& policy,
334 FDelegateHandle const& uniqueHandle = {},
335 const UObject* boundObject = nullptr,
336 FName const& boundFunctionName = NAME_None
337 ) {
338 const FEventPolicy actualPolicy = policy.With(DefaultPolicy);
339
340 if (bHasBroadcasted && actualPolicy.Belated && actualPolicy.Once)
341 {
342 CallBelated(delegate);
343 return FDelegateHandle();
344 }
345
346 FDelegateHandle outputHandle = uniqueHandle;
347 if (!outputHandle.IsValid())
348 {
349 outputHandle = MulticastDelegate.Add(delegate);
350
351 if (boundObject && boundFunctionName != NAME_None)
352 BoundUFunctionsMap.Add(FBoundUFunction(boundObject, boundFunctionName), outputHandle);
353
354 if (actualPolicy.Once)
355 OnlyNextDelegates.Add(outputHandle);
356 }
357
358 if (bHasBroadcasted && actualPolicy.Belated)
359 CallBelated(delegate);
360
361 return outputHandle;
362 }
363
364 void CallBelated(FDelegate& delegate)
365 {
366 InvokeWithTuple(&delegate, &FDelegate::Execute, Cache.GetValue());
367 }
368
369 using FBoundUFunction = TPair<TWeakObjectPtr<const UObject>, FName>;
370
371 bool bHasBroadcasted = false;
372
374 TSet<FDelegateHandle> OnlyNextDelegates;
375 TMap<FBoundUFunction, FDelegateHandle> BoundUFunctionsMap;
376 TOptional<ArgumentsCache> Cache;
377 TMulticastDelegate<void(Args...), FDefaultDelegateUserPolicy> MulticastDelegate;
378 };
379
380 /** @brief Shorthand alias for TEventDelegate which copies arguments to its cache regardless of their qualifiers */
381 template <typename Signature, FEventPolicy DefaultPolicy = {}>
382 using TRetainingEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.CacheViaCopy = true})>;
383
384 /** @brief Shorthand alias for TEventDelegate which broadcasts listeners immediately once they're added */
385 template <typename Signature, FEventPolicy DefaultPolicy = {}>
386 using TBelatedEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.Belated = true})>;
387
388 /** @brief Shorthand alias for combination of TRetainingEventDelegate and TBelatedEventDelegate */
389 template <typename Signature, FEventPolicy DefaultPolicy = {}>
390 using TBelatedRetainingEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.Belated = true, .CacheViaCopy = true})>;
391
392 /** @brief Shorthand alias for TEventDelegate which broadcasts listeners only once and then they're removed */
393 template <typename Signature, FEventPolicy DefaultPolicy = {}>
394 using TOneTimeEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.Once = true})>;
395
396 /** @brief Shorthand alias for combination of TRetainingEventDelegate and TOneTimeEventDelegate */
397 template <typename Signature, FEventPolicy DefaultPolicy = {}>
398 using TOneTimeRetainingEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.Once = true, .CacheViaCopy = true})>;
399
400 /** @brief Shorthand alias for combination of TBelatedEventDelegate and TOneTimeEventDelegate */
401 template <typename Signature, FEventPolicy DefaultPolicy = {}>
402 using TOneTimeBelatedEventDelegate = TEventDelegate<Signature, DefaultPolicy.With({.Once = true, .Belated = true})>;
403
404 /** @brief Collect'em all */
405 template <typename Signature, FEventPolicy DefaultPolicy = {}>
407 DefaultPolicy.With({.Once = true, .Belated = true, .CacheViaCopy = true})
408 >;
409
410 /** @brief Map the input dynamic multicast delegate to a conceptually compatible native event delegate type */
411 template <CDynamicMulticastDelegate Dynamic, FEventPolicy DefaultPolicy = {}>
413 {
415 };
416
417 /** @brief Map the input dynamic multicast delegate to a conceptually compatible native event delegate type */
418 template <typename Dynamic, FEventPolicy DefaultPolicy = {}>
420}
#define FWD(...)
Shorten forwarding expression with this macro so one may not need to specify explicit type.
Definition Macros.h:100
FDelegateHandle Add(FDelegate delegate, FEventPolicy const &policy={})
Adds a new delegate to the event delegate.
int32 RemoveAll(const void *inUserObject)
Removes all binding associated to the given object.
std::conditional_t< DefaultPolicy.ThreadSafe, FScopeLock, FVoid > MutexLock
TEventDelegate & With(Delegates &&... delegates)
Bind multiple delegates at once, useful for initial mandatory bindings
TDelegate< FunctionSignature, FDefaultDelegateUserPolicy > FDelegate
FDelegate Delegation(OptionalObject &&... object)
Create a delegate object which is broadcasting this event. This is useful for chaining events togethe...
FDelegateHandle AddUnique(const DynamicDelegateType &dynamicDelegate, FEventPolicy const &policy={})
Adds the given dynamic delegate only if it's not already bound to this event delegate.
std::conditional_t< DefaultPolicy.CacheViaCopy, TTuple< std::decay_t< Args >... >, TTuple< Args... > > ArgumentsCache
void Reset()
Resets all states of this event delegate to their default.
FDelegateHandle Add(const DynamicDelegateType &dynamicDelegate, FEventPolicy const &policy={})
Adds a new dynamic delegate to this event delegate.
bool Remove(const FDelegateHandle &delegateHandle)
Remove the binding associated to the given handle.
bool Remove(const DynamicDelegateType &dynamicDelegate)
Remove the binding associated to the dynamic delegate.
TEventDelegate(Delegates... delegates)
Bind multiple delegates at once, useful for initial mandatory bindings.
"Extension" of a common TMulticastDelegate. It allows to define optional "flags" when adding a bindin...
TInferredDelegate< Function, Captures... > From(Function func, Captures &&... captures)
Instead of specifying manually a delegate type, infer it from the input function and the extra captur...
typename TNativeEvent_Struct< Dynamic, DefaultPolicy >::Type TNativeEvent
Map the input dynamic multicast delegate to a conceptually compatible native event delegate type.
NativeDelegateType AsNative(Dynamic &&dynamicDelegate)
Creates a native delegate that is bound to the same UFunction as the specified dynamic delegate.
Definition AsNative.h:63
TFunction_Return< Function > InvokeWithTuple(Function &&function, Tuple &&arguments)
A clone of std::apply for Unreal, STL and RangeV3 tuples which also supports function pointers.
This struct may be used for situations where something needs to be returned but it's not meaningful t...
Definition Void.h:26
Settings for the TEventDelegate class, which defines optional behavior when adding a binding to it.
bool ThreadSafe
Enable mutex locks around adding/broadcasting delegates. Only considered in DefaultPolicy.
bool Belated
The binding will be executed immediately if the delegate has already been broadcasted.
FORCEINLINE friend constexpr bool operator!=(FEventPolicy const &lhs, FEventPolicy const &rhs)
FORCEINLINE constexpr bool IsDefault() const
Is this instance equivalent to a default constructed one.
bool Once
The binding will be automatically removed after the next broadcast.
bool CacheViaCopy
Attempt to copy arguments when storing them for belated invokes, instead of perfect forwarding them....
FORCEINLINE friend constexpr bool operator==(FEventPolicy const &lhs, FEventPolicy const &rhs)
FORCEINLINE constexpr FEventPolicy With(FEventPolicy const &other) const
Merge two policy flags.
Map the input dynamic multicast delegate to a conceptually compatible native event delegate type.
A type wrapper around a default initializeable object which may not be copyable but which needs to be...