MCRO
C++23 utilities for Unreal Engine.
Loading...
Searching...
No Matches
Observable.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#include "CoreMinimal.h"
14#include "Mcro/AssertMacros.h"
16#include "Mcro/Construct.h"
17#include "Mcro/Observable.Fwd.h"
18
19namespace Mcro::Observable
20{
21 using namespace Mcro::Delegates;
22 using namespace Mcro::Construct;
23
24 /**
25 * This struct holds the circumstances of the data change. It cannot be moved or copied and its lifespan is
26 * managed entirely by `TState`
27 */
28 template <typename T>
30 {
31 template <CDefaultInitializable = T>
33
34 template <CMoveConstructible = T>
35 TChangeData(T&& value) : Next(Forward<T>(value)) {}
36
37 template <CCopyConstructible = T>
38 TChangeData(const TChangeData& from) : Next(from.Next), Previous(from.Previous) {}
39
40 template <CMoveConstructible = T>
41 TChangeData(TChangeData&& from) : Next(MoveTemp(from.Next)), Previous(MoveTemp(from.Previous)) {}
42
43 template <typename Arg>
44 requires (!CSameAs<Arg, TChangeData> && !CSameAs<Arg, T>)
45 TChangeData(Arg&& arg) : Next(Forward<Arg>(arg)) {}
46
47 template <typename... Args>
48 requires (sizeof...(Args) > 1)
49 TChangeData(Args&&... args) : Next(Forward<Args>(args)...) {}
50
52 TOptional<T> Previous;
53 };
54
55 /** Public API and base class for `TState` which shouldn't concern with policy flags or thread safety */
56 template <typename T>
58 {
59 using Type = T;
60 using ReadLockVariant = TVariant<FReadScopeLock, FVoid>;
61 using WriteLockVariant = TVariant<FWriteScopeLock, FVoid>;
62
63 virtual ~IState() = default;
64
65 /**
66 * Get the wrapped value if for some reason the conversion operator is not enough or deleted.
67 * Thread safety is not considered in this function, use `ReadLock` before `Get`, or use `GetOnAnyThread`
68 * which provides a read lock, if thread safety is a concern.
69 */
70 virtual T const& Get() const = 0;
71
72 /**
73 * Set the wrapped value if for some reason the assignment operator is not enough or deleted. When thread
74 * safety is enabled Set will automatically lock this state for writing.
75 *
76 * @warning
77 * Setting this state from within its change listeners is prohibited and will trigger a check()
78 */
79 virtual void Set(T const& value) = 0;
80
81 /**
82 * Modify this state via an l-value ref in a functor
83 *
84 * @param modifier The functor which modifies this value
85 *
86 * @param alwaysNotify
87 * Notify observers about the change even when the previous state is not different after the modification.
88 * This is only applicable when T is copyable, comparable, StorePrevious flag is set and AlwaysNotify flag is
89 * not set via policy.
90 */
91 virtual void Modify(TUniqueFunction<void(T&)>&& modifier, bool alwaysNotify = true) = 0;
92
93 protected:
94 template <CChangeListener<T> Function>
95 static auto DelegateValueArgument(Function const& onChange, EInvokeMode invokeMode = DefaultInvocation)
96 {
97 return [onChange](TChangeData<T> const& change)
98 {
100 onChange(change.Next);
102 onChange(change.Next, change.Previous);
103 };
104 }
105 public:
106
107 /** Add a delegate which gets a `TChangeData<T> const&` if this state has been set. */
108 virtual FDelegateHandle OnChange(TDelegate<void(TChangeData<T> const&)> onChange, EInvokeMode invokeMode = DefaultInvocation) = 0;
109
110 /**
111 * Add a function without object binding which either has one or two arguments with the following signature:
112 *
113 * @code
114 * [](T const& next, [TOptional<T> const& previous])
115 * @endcode
116 *
117 * Where the argument `previous` is optional (to have, not its type). The argument `previous` when it is
118 * present is TOptional because it may only have a value when StorePrevious policy is active and T is copyable.
119 */
120 template <CChangeListener<T> Function>
121 FDelegateHandle OnChange(Function const& onChange, EInvokeMode invokeMode = DefaultInvocation)
122 {
123 return OnChange(From(DelegateValueArgument(onChange)), invokeMode);
124 }
125
126 /**
127 * Add a function with an object binding which either has one or two arguments with the following signature:
128 *
129 * @code
130 * [](T const& next, [TOptional<T> const& previous])
131 * @endcode
132 *
133 * Where the argument `previous` is optional (to have, not its type). The argument `previous` when it is
134 * present is TOptional because it may only have a value when StorePrevious policy is active and T is copyable.
135 */
136 template <typename Object, CChangeListener<T> Function>
137 FDelegateHandle OnChange(Object&& object, Function const& onChange, EInvokeMode invokeMode = DefaultInvocation)
138 {
139 return OnChange(From(Forward<Object>(object), DelegateValueArgument(onChange)), invokeMode);
140 }
141
142 /**
143 * Given value will be stored in the state only if T is equality comparable and it differs from the current
144 * state value. If T is not equality comparable this function is equivalent to Set and always returns true.
145 *
146 * @return
147 * True if the given value was different from the previous state value. Always returns true when T is is not
148 * equality comparable.
149 */
150 virtual bool HasChangedFrom(const T& nextValue) = 0;
151
152 /** Returns true if this state has ever been changed from its initial value given at construction. */
153 virtual bool HasEverChanged() const = 0;
154
155 /** Equivalent to `TMulticastDelegate::Remove` */
156 virtual bool Remove(FDelegateHandle const& handle) = 0;
157
158 /** Equivalent to `TMulticastDelegate::RemoveAll` */
159 virtual int32 RemoveAll(const void* object) = 0;
160
161 /**
162 * If thread safety is enabled in DefaultPolicy, get the value with a bundled read-scope-lock. Otherwise the
163 * tuple returns an empty dummy struct as its second argument.
164 * Use C++17 structured binding for convenience:
165 *
166 * @code
167 * auto [value, lock] = MyState.GetOnAnyThread();
168 * @endcode
169 *
170 * Unlike the placeholder `auto` keyword, the structured binding `auto` keyword preserves reference qualifiers.
171 * See https://godbolt.org/z/jn918fKfd
172 *
173 * @return
174 * The lock is returned as TUniquePtr it's slightly more expensive because of ref-counting but it makes the API
175 * so much easier to use as TState can decide to return a real lock or just a dummy.
176 */
177 virtual TTuple<T const&, TUniquePtr<ReadLockVariant>> GetOnAnyThread() const = 0;
178
179 /**
180 * Lock this state for reading for the current scope
181 *
182 * @return
183 * The lock is returned as TUniquePtr it's slightly more expensive because of ref-counting but it makes the API
184 * so much easier to use as TState can decide to return a real lock or just a dummy.
185 */
186 virtual TUniquePtr<ReadLockVariant> ReadLock() const = 0;
187
188 /**
189 * Lock this state for writing for the current scope
190 *
191 * @return
192 * The lock is returned as TUniquePtr it's slightly more expensive because of ref-counting but it makes the API
193 * so much easier to use as TState can decide to return a real lock or just a dummy.
194 */
195 virtual TUniquePtr<WriteLockVariant> WriteLock() = 0;
196
197 template <typename Self>
198 operator const T& (this Self&& self)
199 {
200 return self.Get();
201 }
202
203 template <typename Self>
204 const T* operator -> (this Self&& self)
205 {
206 return &self.Get();
207 }
208
209 template <typename Self, CConvertibleTo<T> Other>
210 requires (!CState<Other>)
211 Self& operator = (this Self&& self, Other&& value)
212 {
213 if constexpr (CCopyable<Other>)
214 self.Set(value);
215 else if constexpr (CMovable<Other>)
216 self.Set(MoveTemp(value));
217 return self;
218 }
219 };
220
221 /**
222 * Storage wrapper for any value which state needs to be tracked or their change needs to be observed.
223 * By default `TState` is not thread-safe unless ThreadSafeState policy is active in `DefaultPolicy`
224 */
225 template <typename T, int32 DefaultPolicy>
226 struct TState : IState<T>
227 {
228 template <typename ThreadSafeType, typename NaiveType>
229 using ThreadSafeSwitch = std::conditional_t<static_cast<bool>(DefaultPolicy & ThreadSafeState), ThreadSafeType, NaiveType>;
230
232
233 using typename StateBase::ReadLockVariant;
234 using typename StateBase::WriteLockVariant;
235
238
239 static constexpr int32 DefaultPolicyFlags = DefaultPolicy;
240
241 /** Enable default constructor only when T is default initializable */
242 template <CDefaultInitializable = T>
243 TState() : Value() {}
244
245 /** Enable copy constructor for T only when T is copy constructable */
246 template <CCopyConstructible = T>
247 TState(T const& value) : Value(value) {}
248
249 /** Enable move constructor for T only when T is move constructable */
250 template <CMoveConstructible = T>
251 TState(T&& value) : Value(MoveTemp(value)) {}
252
253 /** Enable copy constructor for the state only when T is copy constructable */
254 template <CCopyConstructible = T>
255 TState(TState const& other) : Value(other.Value.Next) {}
256
257 /** Enable move constructor for the state only when T is move constructable */
258 template <CMoveConstructible = T>
259 TState(TState&& other) : Value(MoveTemp(other.Value.Next)) {}
260
261 /** Construct value in-place with non-semantic single argument constructor */
262 template <typename Arg>
263 requires (!CConvertibleTo<Arg, TState> && !CSameAs<Arg, T>)
264 TState(Arg&& arg) : Value(Forward<Arg>(arg)) {}
265
266 /** Construct value in-place with multiple argument constructor */
267 template <typename... Args>
268 requires (sizeof...(Args) > 1)
269 TState(Args&&... args) : Value(Forward<Args>(args)...) {}
270
271 virtual T const& Get() const override { return Value.Next; }
272
273 virtual TTuple<T const&, TUniquePtr<ReadLockVariant>> GetOnAnyThread() const override
274 {
275 return { Value.Next, ReadLock() };
276 }
277
278 virtual void Set(T const& value) override
279 {
280 ASSERT_QUIT(!Modifying, ,
281 ->WithMessage(TEXT("Attempting to set this state while this state is already being set from somewhere else."))
282 );
283 TGuardValue modifyingGuard(Modifying, true);
284 auto lock = WriteLock();
285 bool broadcast = true;
286
287 if constexpr (CCoreEqualityComparable<T>)
288 broadcast = PolicyFlags & AlwaysNotify || Value.Next != value;
289
290 if constexpr (CCopyable<T>)
292 Value.Previous = Value.Next;
293
294 Value.Next = value;
295
296 if (broadcast)
297 OnChangeEvent.Broadcast(Value);
298 }
299
300 virtual void Modify(TUniqueFunction<void(T&)>&& modifier, bool alwaysNotify = true) override
301 {
302 ASSERT_QUIT(!Modifying, ,
303 ->WithMessage(TEXT("Attempting to set this state while this state is already being set from somewhere else."))
304 );
305 TGuardValue modifyingGuard(Modifying, true);
306 auto lock = WriteLock();
307 bool broadcast = true;
308
309 if constexpr (CCopyable<T>)
310 {
312 {
313 Value.Previous = Value.Next;
314 }
315 }
316
317 modifier(Value.Next);
318
319 if constexpr (CCopyable<T> && CCoreEqualityComparable<T>)
320 broadcast = alwaysNotify
323 || !Value.Previous.IsSet()
324 || Value.Previous.GetValue() != Value.Next;
325
326 if (broadcast)
327 OnChangeEvent.Broadcast(Value);
328 }
329
330 virtual FDelegateHandle OnChange(TDelegate<void(TChangeData<T> const&)> onChange, EInvokeMode invokeMode = DefaultInvocation) override
331 {
332 auto lock = WriteLock();
333 return OnChangeEvent.Add(onChange, invokeMode);
334 }
335
336 virtual bool Remove(FDelegateHandle const& handle) override
337 {
338 auto lock = WriteLock();
339 return OnChangeEvent.Remove(handle);
340 }
341
342 virtual int32 RemoveAll(const void* object) override
343 {
344 auto lock = WriteLock();
345 return OnChangeEvent.RemoveAll(object);
346 }
347
348 virtual bool HasChangedFrom(const T& nextValue) override
349 {
350 if constexpr (CCoreEqualityComparable<T>)
351 {
352 bool hasChanged = Value.Next != nextValue;
353 Set(nextValue);
354 return hasChanged;
355 }
356 else
357 {
358 Set(nextValue);
359 return true;
360 }
361 }
362
363 virtual bool HasEverChanged() const override
364 {
365 return OnChangeEvent.IsBroadcasted();
366 }
367
368 virtual TUniquePtr<ReadLockVariant> ReadLock() const override
369 {
370 return MakeUnique<ReadLockVariant>(TInPlaceType<ReadLockType>(), Mutex.Get());
371 }
372
373 virtual TUniquePtr<WriteLockVariant> WriteLock() override
374 {
375 return MakeUnique<WriteLockVariant>(TInPlaceType<WriteLockType>(), Mutex.Get());
376 }
377
378 int32 PolicyFlags = DefaultPolicy;
379
380 private:
381 TEventDelegate<void(TChangeData<T> const&)> OnChangeEvent;
382 TChangeData<T> Value;
383 bool Modifying = false;
384 mutable TInitializeOnCopy<FRWLock> Mutex;
385 };
386
387 template <typename LeftValue, CWeaklyEqualityComparableWith<LeftValue> RightValue>
388 bool operator == (IState<LeftValue> const& left, IState<RightValue> const& right)
389 {
390 return left.Get() == right.Get();
391 }
392
393 template <typename LeftValue, CPartiallyOrderedWith<LeftValue> RightValue>
394 bool operator <=> (IState<LeftValue> const& left, IState<RightValue> const& right)
395 {
396 return left.Get() <=> right.Get();
397 }
398}
#define ASSERT_QUIT(condition, returnOnFailure,...)
TInferredDelegate< Function, Captures... > From(Function func, const Captures &... captures)
bool operator==(IState< LeftValue > const &left, IState< RightValue > const &right)
Definition Observable.h:388
bool operator<=>(IState< LeftValue > const &left, IState< RightValue > const &right)
Definition Observable.h:394
virtual FDelegateHandle OnChange(TDelegate< void(TChangeData< T > const &)> onChange, EInvokeMode invokeMode=DefaultInvocation)=0
virtual int32 RemoveAll(const void *object)=0
virtual void Set(T const &value)=0
virtual bool HasEverChanged() const =0
virtual void Modify(TUniqueFunction< void(T &)> &&modifier, bool alwaysNotify=true)=0
TVariant< FWriteScopeLock, FVoid > WriteLockVariant
Definition Observable.h:61
virtual bool Remove(FDelegateHandle const &handle)=0
virtual bool HasChangedFrom(const T &nextValue)=0
virtual T const & Get() const =0
FDelegateHandle OnChange(Function const &onChange, EInvokeMode invokeMode=DefaultInvocation)
Definition Observable.h:121
const T * operator->(this Self &&self)
Definition Observable.h:204
static auto DelegateValueArgument(Function const &onChange, EInvokeMode invokeMode=DefaultInvocation)
Definition Observable.h:95
FDelegateHandle OnChange(Object &&object, Function const &onChange, EInvokeMode invokeMode=DefaultInvocation)
Definition Observable.h:137
virtual TUniquePtr< ReadLockVariant > ReadLock() const =0
TVariant< FReadScopeLock, FVoid > ReadLockVariant
Definition Observable.h:60
virtual TTuple< T const &, TUniquePtr< ReadLockVariant > > GetOnAnyThread() const =0
virtual TUniquePtr< WriteLockVariant > WriteLock()=0
virtual ~IState()=default
TChangeData(Args &&... args)
Definition Observable.h:49
TChangeData(const TChangeData &from)
Definition Observable.h:38
TChangeData(TChangeData &&from)
Definition Observable.h:41
virtual TUniquePtr< WriteLockVariant > WriteLock() override
Definition Observable.h:373
static constexpr int32 DefaultPolicyFlags
Definition Observable.h:239
virtual int32 RemoveAll(const void *object) override
Definition Observable.h:342
virtual void Set(T const &value) override
Definition Observable.h:278
virtual bool Remove(FDelegateHandle const &handle) override
Definition Observable.h:336
TState(TState const &other)
Definition Observable.h:255
virtual void Modify(TUniqueFunction< void(T &)> &&modifier, bool alwaysNotify=true) override
Definition Observable.h:300
virtual TUniquePtr< ReadLockVariant > ReadLock() const override
Definition Observable.h:368
std::conditional_t< static_cast< bool >(DefaultPolicy &ThreadSafeState), ThreadSafeType, NaiveType > ThreadSafeSwitch
Definition Observable.h:229
virtual TTuple< T const &, TUniquePtr< ReadLockVariant > > GetOnAnyThread() const override
Definition Observable.h:273
virtual bool HasChangedFrom(const T &nextValue) override
Definition Observable.h:348
ThreadSafeSwitch< FReadScopeLock, FVoid > ReadLockType
Definition Observable.h:236
ThreadSafeSwitch< FWriteScopeLock, FVoid > WriteLockType
Definition Observable.h:237
TState(Args &&... args)
Definition Observable.h:269
TState(TState &&other)
Definition Observable.h:259
virtual FDelegateHandle OnChange(TDelegate< void(TChangeData< T > const &)> onChange, EInvokeMode invokeMode=DefaultInvocation) override
Definition Observable.h:330
virtual T const & Get() const override
Definition Observable.h:271
TState(T const &value)
Definition Observable.h:247
virtual bool HasEverChanged() const override
Definition Observable.h:363