HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
observable.hpp
1// Copyright Take Vos 2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "unfair_mutex.hpp"
8#include <memory>
9#include <vector>
10#include <atomic>
11#include <concepts>
12#include <type_traits>
13#include <mutex>
14
15namespace tt {
16template<typename T>
17class observable;
18
19namespace detail {
20
21template<typename T>
22struct observable_impl;
23
24inline unfair_mutex observable_mutex;
25
28
29 uint64_t proxy_count = 0;
31
32 observable_notifier_type() = default;
35 observable_notifier_type &operator=(observable_notifier_type const &) = delete;
37
38 void start_proxy() noexcept
39 {
40 ttlet lock = std::scoped_lock(observable_mutex);
41 ++proxy_count;
42 }
43
44 void finish_proxy() noexcept
45 {
46 observable_mutex.lock();
47
48 tt_axiom(proxy_count != 0);
49 --proxy_count;
50
51 notify();
52 }
53
58 void push(callback_ptr_type callback_ptr) noexcept
59 {
60 tt_axiom(observable_mutex.is_locked());
61 callbacks.push_back(std::move(callback_ptr));
62 }
63
71 void notify() noexcept {
72 tt_axiom(observable_mutex.is_locked());
73
74 if (proxy_count == 0 and not callbacks.empty()) {
75 auto to_call = std::vector<callback_ptr_type>{};
76 std::swap(to_call, callbacks);
77 observable_mutex.unlock();
78
79 for (ttlet &callback_ptr : to_call) {
80 if (auto callback = callback_ptr.lock()) {
81 (*callback)();
82 }
83 }
84 } else {
85 observable_mutex.unlock();
86 }
87 }
88};
89
90inline observable_notifier_type observable_notifier;
91
101template<typename T, bool Constant>
103 using value_type = T;
104 static constexpr bool is_atomic = std::is_scalar_v<T>;
105 static constexpr bool is_constant = Constant;
106 static constexpr bool is_variable = not Constant;
107
108 enum class state_type : uint8_t { read, write, modified };
109
110 observable_impl<value_type> *_actual = nullptr;
111 mutable value_type _original;
112 mutable state_type _state = state_type::none;
113
115 {
116 tt_axiom(_actual);
117
118 if (is_variable and _state == state_type::write and _actual->value != _original) {
119 _state = state_type::modified;
120 }
121
122 if (not is_atomic) {
123 _actual->mutex.unlock();
124 observable_notifier.finish_proxy();
125 }
126
127 if (is_variable and _state == state_type::modified) {
128 _actual->notify_owners();
129 }
130 }
131
132 observable_proxy(observable_impl<T> &actual) : _actual(&actual), _original(), _state(state_type::read)
133 {
134 if (not is_atomic) {
135 observable_notifier.start_proxy();
136 this->_actual->mutex.lock();
137 }
138 tt_axiom(this->_actual);
139 }
140
142 observable_proxy(observable_proxy const &) = delete;
143
144 void prepare(state_type new_state) const noexcept
145 {
146 tt_axiom(new_state != state_type::read);
147 if (_state == state_type::read and new_state == state_type::write) {
148 _original = _actual->value;
149 }
150 _state = new_state;
151 }
152
153 operator value_type() const noexcept requires(is_atomic)
154 {
155 return _actual->value;
156 }
157
158 operator value_type &() const noexcept requires(is_variable and not is_atomic)
159 {
160 prepare(state_type::write);
161 return _actual->value;
162 }
163
164 operator value_type const &() const noexcept requires(is_constant and not is_atomic)
165 {
166 return _actual->value;
167 }
168
172 {
173 tt_return_on_self_assignment(other);
174
175 return *this = other._actual->value;
176 }
177
181 {
182 tt_return_on_self_assignment(other);
183
184 return *this = other._actual->value;
185 }
186
187 // MSVC Compiler bug returning this with auto argument
188 template<typename Arg = value_type>
189 observable_proxy &operator=(Arg &&arg) noexcept
190 requires(is_variable and not is_atomic and not std::is_same_v<std::remove_cvref_t<Arg>, observable_proxy>)
191 {
192 prepare(state_type::write);
193 _actual->value = std::forward<Arg>(arg);
194 return *this;
195 }
196
197 template<typename Arg = value_type>
198 observable_proxy &operator=(Arg &&arg) noexcept
199 requires(is_variable and is_atomic and not std::is_same_v<std::remove_cvref_t<Arg>, observable_proxy>)
200 {
201 // value is not std::forwarded, that means exchange() and operator!=() are called
202 // with a const lvalue reference.
203 if (_actual->value.exchange(arg) != arg) {
204 _state = state_type::modified;
205 }
206 return *this;
207 }
208
209 value_type operator*() const noexcept requires(is_atomic)
210 {
211 return _actual->value.load();
212 }
213
214 value_type const &operator*() const noexcept requires(is_constant and not is_atomic)
215 {
216 return _actual->value;
217 }
218
219 value_type &operator*() const noexcept requires(is_variable and not is_atomic)
220 {
221 prepare(state_type::write);
222 return _actual->value;
223 }
224
225 value_type const *operator->() const noexcept requires(is_constant and not is_atomic)
226 {
227 return &(_actual->value);
228 }
229
230 value_type *operator->() const noexcept requires(is_variable and not is_atomic)
231 {
232 prepare(state_type::write);
233 return &(_actual->value);
234 }
235
236 template<typename... Args>
237 decltype(auto) operator()(Args &&...args) const noexcept
238 {
239 prepare(state_type::write);
240 return _actual->value(std::forward<Args>(args)...);
241 }
242
243 template<typename Arg>
244 decltype(auto) operator[](Arg &&arg) const noexcept requires(is_variable and not is_atomic)
245 {
246 prepare(state_type::write);
247 return _actual->value[std::forward<Arg>(arg)];
248 }
249
250 template<typename Arg>
251 decltype(auto) operator[](Arg &&arg) const noexcept requires(is_constant and not is_atomic)
252 {
253 return const_cast<value_type const &>(_actual->value)[std::forward<Arg>(arg)];
254 }
255
256#define X(op) \
257 [[nodiscard]] friend auto operator op(observable_proxy const &lhs, observable_proxy const &rhs) noexcept \
258 { \
259 return lhs._actual->value op rhs._actual->value; \
260 } \
261\
262 [[nodiscard]] friend auto operator op(observable_proxy const &lhs, auto const &rhs) noexcept \
263 { \
264 return lhs._actual->value op rhs; \
265 } \
266\
267 [[nodiscard]] friend auto operator op(auto const &lhs, observable_proxy const &rhs) noexcept \
268 { \
269 return lhs op rhs._actual->value; \
270 }
271
272 X(==)
273 X(<=>)
274 X(-)
275 X(+)
276 X(*)
277 X(/)
278 X(%)
279 X(&)
280 X(|)
281#undef X
282
283#define X(op) \
284 [[nodiscard]] auto operator op() const noexcept \
285 { \
286 return op _actual->value; \
287 }
288
289 X(-)
290 X(~)
291#undef X
292
293#define X(func) \
294 [[nodiscard]] friend decltype(auto) func(observable_proxy const &rhs) noexcept \
295 { \
296 return func(rhs._actual->value); \
297 }
298
299 X(size)
300 X(ssize)
301 X(begin)
302 X(end)
303#undef X
304
305 // MSVC Internal compiler error
306 template<typename Rhs>
307 value_type operator+=(Rhs const &rhs) noexcept requires(is_variable and std::is_arithmetic_v<Rhs>)
308 {
309 if (rhs != Rhs{}) {
310 prepare(state_type::modified);
311 return _actual->value += rhs;
312 } else {
313 return _actual->value;
314 }
315 }
316
317 template<typename Rhs>
318 value_type operator+=(Rhs const &rhs) noexcept requires(is_variable and not std::is_arithmetic_v<Rhs>)
319 {
320 prepare(state_type::write);
321 return _actual->value += rhs;
322 }
323};
324
327template<typename T>
329 static constexpr bool is_atomic = std::is_scalar_v<T>;
330
331 using value_type = T;
332 using atomic_value_type = std::conditional_t<is_atomic, std::atomic<value_type>, value_type>;
334
335 atomic_value_type value;
337
341
343 {
344 tt_axiom(owners.empty());
345 }
346
347 observable_impl(observable_impl const &) = delete;
348 observable_impl(observable_impl &&) = delete;
349 observable_impl &operator=(observable_impl const &) = delete;
350 observable_impl &operator=(observable_impl &&) = delete;
351
352 observable_impl() noexcept : value() {}
353 observable_impl(value_type const &value) noexcept : value(value) {}
354 observable_impl(value_type &&value) noexcept : value(std::move(value)) {}
355
356 observable_proxy<value_type, false> get() noexcept
357 {
358 return observable_proxy<value_type, false>(*this);
359 }
360
361 observable_proxy<value_type, true> cget() noexcept
362 {
363 return observable_proxy<value_type, true>(*this);
364 }
365
370 void add_owner(owner_type &owner) noexcept
371 {
372 tt_axiom(std::find(owners.cbegin(), owners.cend(), &owner) == owners.cend());
373
374 ttlet lock = std::scoped_lock(observable_mutex);
375 owners.push_back(&owner);
376 }
377
382 void remove_owner(owner_type &owner) noexcept
383 {
384 ttlet lock = std::scoped_lock(observable_mutex);
385 ttlet nr_erased = std::erase(owners, &owner);
386 tt_axiom(nr_erased == 1);
387 }
388
389 void notify_owners() const noexcept
390 {
391 observable_mutex.lock();
392 for (auto owner : owners) {
393 owner->notify();
394 }
395 observable_notifier.notify();
396 }
397
398 void reseat_owners(std::shared_ptr<observable_impl> const &new_impl) noexcept
399 {
400 observable_mutex.lock();
401
402 tt_axiom(not owners.empty());
403 auto keep_this_alive = owners.front()->_pimpl;
404
405 for (auto owner : owners) {
406 owner->_pimpl = new_impl;
407 new_impl->owners.push_back(owner);
408 owner->notify();
409 }
410 owners.clear();
411 observable_notifier.notify();
412 }
413};
414
415} // namespace detail
416
463template<typename T>
465public:
466 using value_type = T;
471 static constexpr bool is_atomic = impl_type::is_atomic;
472
474 {
475 _pimpl->remove_owner(*this);
476 }
477
482 observable() noexcept : _pimpl(std::make_shared<impl_type>())
483 {
484 _pimpl->add_owner(*this);
485 }
486
493 observable(observable const &other) noexcept : _pimpl(other._pimpl)
494 {
495 _pimpl->add_owner(*this);
496 }
497
508 observable &operator=(observable const &other) noexcept
509 {
510 tt_return_on_self_assignment(other);
511
512 _pimpl->reseat_owners(other._pimpl);
513 return *this;
514 }
515
524 const_reference cget() const noexcept
525 {
526 return _pimpl->cget();
527 }
528
537 const_reference get() const noexcept
538 {
539 return _pimpl->cget();
540 }
541
552 reference get() noexcept
553 {
554 return _pimpl->get();
555 }
556
561 observable(std::convertible_to<value_type> auto &&value) noexcept :
562 _pimpl(std::make_shared<impl_type>(std::forward<decltype(value)>(value)))
563 {
564 _pimpl->add_owner(*this);
565 }
566
572 template<typename Value = value_type>
573 observable &operator=(Value &&value) noexcept requires(not std::is_same_v<std::remove_cvref_t<Value>, observable>)
574 {
575 tt_axiom(_pimpl);
576 get() = std::forward<Value>(value);
577 return *this;
578 }
579
584 value_type operator*() const noexcept requires(is_atomic)
585 {
586 return *(cget());
587 }
588
590 {
591 return cget();
592 }
593
594 detail::observable_proxy<value_type, false> operator*() noexcept requires(not is_atomic)
595 {
596 return get();
597 }
598
599 detail::observable_proxy<value_type, true> operator->() const noexcept
600 {
601 return cget();
602 }
603
604 detail::observable_proxy<value_type, false> operator->() noexcept
605 {
606 return get();
607 }
608
609 explicit operator bool() const noexcept
610 {
611 return static_cast<bool>(cget());
612 }
613
614#define X(op) \
615 [[nodiscard]] friend auto operator op(observable const &lhs, observable const &rhs) noexcept \
616 { \
617 return lhs.cget() op rhs.cget(); \
618 } \
619\
620 [[nodiscard]] friend auto operator op(observable const &lhs, auto const &rhs) noexcept \
621 { \
622 return lhs.cget() op rhs; \
623 } \
624\
625 [[nodiscard]] friend auto operator op(auto const &lhs, observable const &rhs) noexcept \
626 { \
627 return lhs op rhs.cget(); \
628 }
629
630 X(==)
631 X(-)
632#undef X
633
634#define X(op) \
635 [[nodiscard]] auto operator op() const noexcept \
636 { \
637 return op cget(); \
638 }
639
640 X(-)
641
642#undef X
643
644 // MSVC Internal compiler error
645 template<typename Rhs>
646 value_type operator+=(Rhs const &rhs) noexcept
647 {
648 return get() += rhs;
649 }
650
651 callback_ptr_type subscribe(callback_ptr_type const &callback_ptr) noexcept
652 {
653 ttlet lock = std::scoped_lock(detail::observable_mutex);
654#if TT_BUILD_TYPE == TT_BT_DEBUG
655 auto it = std::find_if(_callbacks.cbegin(), _callbacks.cend(), [&callback_ptr](ttlet &item) {
656 return item.lock() == callback_ptr;
657 });
658 tt_axiom(it == _callbacks.cend());
659#endif
660 _callbacks.push_back(callback_ptr);
661 return callback_ptr;
662 }
663
664 template<typename Callback>
665 [[nodiscard]] callback_ptr_type subscribe(Callback &&callback) noexcept requires(std::is_invocable_v<Callback>)
666 {
667 auto callback_ptr = std::make_shared<std::function<void()>>(std::forward<Callback>(callback));
668 subscribe(callback_ptr);
669
670 detail::observable_mutex.lock();
671 detail::observable_notifier.push(callback_ptr);
672 detail::observable_notifier.notify();
673 return callback_ptr;
674 }
675
676 void unsubscribe(callback_ptr_type const &callback_ptr) noexcept
677 {
678 ttlet lock = std::scoped_lock(detail::observable_mutex);
679 ttlet erase_count = std::erase_if(_callbacks, [&callback_ptr](ttlet &item) {
680 return item.expired() or item.lock() == callback_ptr;
681 });
682 tt_axiom(erase_count == 1);
683 }
684
685private:
687 mutable std::vector<std::weak_ptr<std::function<void()>>> _callbacks;
688
689 void notify() const noexcept
690 {
691 auto has_expired_callbacks = false;
692 for (ttlet &callback_ptr : _callbacks) {
693 if (callback_ptr.expired()) {
694 has_expired_callbacks = true;
695 } else {
696 detail::observable_notifier.push(callback_ptr);
697 }
698 }
699
700 if (has_expired_callbacks) {
701 ttlet erase_count = std::erase_if(_callbacks, [](ttlet &callback_ptr) {
702 return callback_ptr.expired();
703 });
704 tt_axiom(erase_count > 0);
705 }
706 }
707
708 friend impl_type;
709};
710
715template<typename T>
717 using type = T;
718};
719
720template<typename T>
722 using type = typename observable<T>::value_type;
723};
724
725template<typename T>
726using observable_argument_t = typename observable_argument<T>::type;
727
728} // namespace tt
STL namespace.
observable() noexcept
Construct an observable.
Definition observable.hpp:482
observable & operator=(Value &&value) noexcept
Assign a new value.
Definition observable.hpp:573
const_reference cget() const noexcept
Get a constant reference to the shared value.
Definition observable.hpp:524
observable(std::convertible_to< value_type > auto &&value) noexcept
Construct an observable with its value set.
Definition observable.hpp:561
const_reference get() const noexcept
Get a constant reference to the shared value.
Definition observable.hpp:537
observable(observable const &other) noexcept
Construct an observable and chain it to another.
Definition observable.hpp:493
observable & operator=(observable const &other) noexcept
Chain with another observable.
Definition observable.hpp:508
value_type operator*() const noexcept
Get copy of the shared-value.
Definition observable.hpp:584
reference get() noexcept
Get a writable reference to the shared-value.
Definition observable.hpp:552
The shared value, shared between observers.
Definition observable.hpp:328
unfair_mutex mutex
The mutex is used to serialize state to the owners list and non-atomic value.
Definition observable.hpp:340
void remove_owner(owner_type &owner) noexcept
Remove an observer as one of the owners of the shared-value.
Definition observable.hpp:382
void add_owner(owner_type &owner) noexcept
Add an observer as one of the owners of the shared-value.
Definition observable.hpp:370
Definition observable.hpp:26
void push(callback_ptr_type callback_ptr) noexcept
Add a callback to the list.
Definition observable.hpp:58
void notify() noexcept
Notify all callbacks that have been pushed.
Definition observable.hpp:71
A proxy to the shared-value inside an observable.
Definition observable.hpp:102
observable_proxy & operator=(observable_proxy const &other) noexcept
Copy the value from a proxy.
Definition observable.hpp:180
observable_proxy & operator=(observable_proxy &&other) noexcept
Copy the value from a temporary proxy.
Definition observable.hpp:171
The value_type of an observable from the constructor argument type.
Definition observable.hpp:716
Definition type_traits.hpp:315
Definition concepts.hpp:37
T cbegin(T... args)
T clear(T... args)
T empty(T... args)
T cend(T... args)
T find(T... args)
T forward(T... args)
T front(T... args)
T lock(T... args)
T make_shared(T... args)
T move(T... args)
T push_back(T... args)
T swap(T... args)