#pragma once #include #include #include #include #include #include namespace util { template concept SomeAtomic = std::same_as, std::atomic>>; /** * @brief Concept defining types that can be observed for changes. * * A type is Observable if it satisfies all requirements for being stored * and monitored in an ObservableValue container: * * - Must be equality comparable to detect changes * - Must be copy constructible for capturing old values in guards * - Must be move constructible for efficient value updates * * @note Copy assignment is intentionally not required since we use move semantics * for value updates and only need copy construction for change detection. */ template concept Observable = std::equality_comparable && std::copy_constructible && std::move_constructible; namespace impl { /** * @brief Base class containing common ObservableValue functionality. * * This class contains all the observer management and notification logic * that is shared between regular and atomic ObservableValue specializations. * * @tparam T The value type (for atomic specializations, this is the underlying type, not * std::atomic) */ template class ObservableValueBase { protected: boost::signals2::signal onUpdate_; public: virtual ~ObservableValueBase() = default; /** * @brief Registers an observer callback for value changes. * @param fn Callback function/lambda that accepts T const& * @return Connection object for managing the subscription */ boost::signals2::connection observe(std::invocable auto&& fn) { return onUpdate_.connect(std::forward(fn)); } /** * @brief Checks if there are any active observers. * @return true if there are observers, false otherwise */ [[nodiscard]] bool hasObservers() const { return not onUpdate_.empty(); } /** * @brief Forces notification of all observers with the current value. * * This method will notify all observers with the current value regardless * of whether the value has changed since the last notification. */ virtual void forceNotify() = 0; protected: /** * @brief Notifies all observers with the given value. * @param value The value to send to observers */ void notifyObservers(T const& value) { onUpdate_(value); } }; } // namespace impl // Forward declaration template class ObservableValue; /** * @brief An observable value container that notifies observers when the value changes. * * ObservableValue wraps a value of type T and provides a mechanism to observe changes to that * value. When the value is modified (and actually changes), all registered observers are notified. * * @tparam T The type of value to observe. Must satisfy the Observable concept. * * @par Thread Safety * - Observer subscription/unsubscription (observe() and connection.disconnect()) are thread-safe * - Value modification operations (set(), operator=) are NOT thread-safe and require external * synchronization * - Observer callbacks are invoked synchronously on the same thread that triggered the value change * - If observers need to perform work on different threads, they must handle dispatch themselves * (e.g., using an async execution context or message queue) * * @par Exception Handling * - If an observer callback throws an exception, the exception will propagate to the caller * - The value will still be updated even if observers throw exceptions * - No guarantee is made about whether other observers will be called if one throws * - It is the caller's responsibility to handle exceptions from observer callbacks */ template requires(not SomeAtomic) class ObservableValue : public impl::ObservableValueBase { T value_; /** * @brief RAII guard for deferred notification of value changes. * * ObservableGuard captures the current value when created and compares it * with the final value when destroyed. If the values differ, observers * are notified. This allows for multiple modifications to the value with * only a single notification at the end. * * @note This class is returned by operator->() and should not be used directly. */ struct ObservableGuard { T const oldValue; ///< Value captured at construction time ObservableValue& ref; ///< Reference to the observable value /** * @brief Constructs guard and captures current value. * @param observable The ObservableValue to guard */ ObservableGuard(ObservableValue& observable) : oldValue(observable), ref(observable) { } /** * @brief Destructor that triggers notification if value changed. * * Compares the captured value with the current value. If they differ, * notifies all observers with the current value. */ ~ObservableGuard() { if (oldValue != ref.value_) ref.notifyObservers(ref.value_); } /** * @brief Provides mutable access to the underlying value. * @return Mutable reference to the wrapped value */ [[nodiscard]] operator T&() { return ref.value_; } }; public: /** * @brief Constructs ObservableValue with initial value. * @param value Initial value (must be convertible to T) */ ObservableValue(std::convertible_to auto&& value) : value_{std::forward(value)} { } /** * @brief Constructs ObservableValue with default initial value. */ ObservableValue() requires std::default_initializable : value_{} { } ObservableValue(ObservableValue const&) = delete; ObservableValue(ObservableValue&&) = default; ObservableValue& operator=(ObservableValue const&) = delete; ObservableValue& operator=(ObservableValue&&) = default; /** * @brief Assignment operator that updates value and notifies observers. * * Updates the stored value and notifies observers if the new value * differs from the current value (using operator!=). * * @param val New value (must be convertible to T) * @return Reference to this object for chaining * * @throws Any exception thrown by observer callbacks will propagate */ ObservableValue& operator=(std::convertible_to auto&& val) { set(val); return *this; } /** * @brief Provides deferred notification access to the value. * * Returns an ObservableGuard that allows modification of the value * with notification deferred until the guard is destroyed. * * @return ObservableGuard for deferred notification */ [[nodiscard]] ObservableGuard operator->() { return {*this}; } /** * @brief Implicit conversion to const reference of the value. * @return Const reference to the stored value */ [[nodiscard]] operator T const&() const { return value_; } /** * @brief Explicitly gets the current value. * @return Const reference to the stored value */ [[nodiscard]] T const& get() const { return value_; } /** * @brief Sets a new value and notifies observers if changed. * * Updates the stored value and notifies all observers if the new value * differs from the current value (using operator!=). If the values are * equal, no notification occurs. * * @param val New value (must be convertible to T) * * @throws Any exception thrown by observer callbacks will propagate * * @par Thread Safety * - This method is NOT thread-safe and requires external synchronization for concurrent access * - Observer callbacks are invoked synchronously on the calling thread */ void set(std::convertible_to auto&& val) { if (value_ != val) { value_ = std::forward(val); this->notifyObservers(value_); } } /** * @brief Forces notification of all observers with the current value. * * This method will notify all observers with the current value regardless * of whether the value has changed since the last notification. */ void forceNotify() override { this->notifyObservers(value_); } }; /** * @brief Partial specialization of ObservableValue for atomic types. * * This specialization provides thread-safe observation of atomic values while * maintaining atomic semantics. It avoids the issues of copying atomic values * and handles race conditions properly. * * @tparam T The underlying type stored in the atomic * * @par Thread Safety * - All operations are thread-safe * - Observer notifications are atomic with respect to value changes * - Multiple threads can safely modify and observe the atomic value * * @par Performance Considerations * - Uses atomic compare-and-swap operations for updates * - Minimizes atomic reads during guard operations * - Observer notifications happen outside of atomic operations when possible */ template class ObservableValue> : public impl::ObservableValueBase { std::atomic value_; public: /** * @brief Constructs ObservableValue with initial atomic value. * @param value Initial value (will be stored in the atomic) */ ObservableValue(std::convertible_to auto&& value) : value_{std::forward(value)} { } /** * @brief Constructs ObservableValue with default initial value. */ ObservableValue() requires std::default_initializable : value_{} { } ObservableValue(ObservableValue const&) = delete; ObservableValue(ObservableValue&&) = default; ObservableValue& operator=(ObservableValue const&) = delete; ObservableValue& operator=(ObservableValue&&) = default; /** * @brief Assignment operator that updates atomic value and notifies observers. * * Uses atomic compare-and-swap to update the value and notifies observers * only if the value actually changed. * * @param val New value * @return Reference to this object for chaining */ ObservableValue& operator=(std::convertible_to auto&& val) { set(std::forward(val)); return *this; } /** * @brief Gets the current atomic value. * @return Current value stored in the atomic */ [[nodiscard]] T get() const { return value_.load(); } /** * @brief Implicit conversion to the current atomic value. * @return Current value stored in the atomic */ [[nodiscard]] operator T() const { return get(); } /** * @brief Sets a new atomic value and notifies observers if changed. * * Uses atomic compare-and-swap to update the value. Notifies all observers * if the value actually changed. * * @param val New value */ void set(std::convertible_to auto&& val) { T newValue = std::forward(val); T oldValue = value_.load(); // Use compare-and-swap to atomically update while (!value_.compare_exchange_weak(oldValue, newValue)) { // compare_exchange_weak updates oldValue with current value on failure // Continue until we succeed } // Notify observers if we actually changed the value // Note: oldValue now contains the actual previous value that was replaced if (oldValue != newValue) { this->notifyObservers(newValue); } } /** * @brief Forces notification of all observers with the current value. * * This method will notify all observers with the current atomic value * regardless of whether the value has changed since the last notification. */ void forceNotify() override { this->notifyObservers(value_.load()); } }; } // namespace util