Last active
June 8, 2025 23:08
-
-
Save dk949/2508ff2b4b5ddf152e7b6c027e8f13c4 to your computer and use it in GitHub Desktop.
A C++ wrapper class that invokes callbacks when it's held value changes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef UT_CHANGE_OBSERVER | |
#define UT_CHANGE_OBSERVER | |
#if __cplusplus < 202'002L | |
# error this file has to be compiled with at least C++20 | |
#endif | |
#include <functional> | |
#include <type_traits> | |
#include <vector> | |
namespace ut { | |
// NOTE: There's 2 versions of several member functions because if the move | |
// ctor/assgn op is deleted, the move ctor/assgn op will not be called | |
// when using std::move. | |
/// | |
enum struct ChangeObserverKeepOldCopy : bool { Yes = true, No = false }; | |
template<typename T, // | |
ChangeObserverKeepOldCopy KeepOldCopy = ChangeObserverKeepOldCopy::Yes, // | |
typename Comp = std::conditional_t<bool(KeepOldCopy), std::equal_to<>, void>> | |
struct ChangeObserver { | |
private: | |
static constexpr auto keepOldCopy = bool(KeepOldCopy); | |
public: | |
using function_type = | |
std::conditional_t<keepOldCopy, std::function<void(T const &, T const &)>, std::function<void(T const &)>>; | |
private: | |
std::vector<function_type> m_callbacks; | |
T m_data; | |
void runCallbacks(T const &old_val, T const &new_val) { | |
for (auto const &cb : m_callbacks) { | |
if constexpr (keepOldCopy) | |
cb(old_val, new_val); | |
else | |
cb(new_val); | |
} | |
} | |
void runCallbacks(T const &new_val) { | |
for (auto const &cb : m_callbacks) | |
cb(new_val); | |
} | |
bool compare(T const &a, T const &b) requires(!std::is_same_v<Comp, void>) { | |
return Comp {}(a, b); | |
} | |
bool compare(T const &, T const &) requires(std::is_same_v<Comp, void>) { | |
return false; | |
} | |
template<bool isConst> | |
struct Proxy { | |
private: | |
struct Empty { }; | |
using Parent = std::conditional_t<isConst, ChangeObserver const *, ChangeObserver *>; | |
using Child = std::conditional_t<isConst, T const *, T *>; | |
using Old = std::conditional_t<isConst || !keepOldCopy, Empty, T>; | |
Parent m_ptr; | |
#ifdef _MSC_VER | |
[[msvc::no_unique_address]] | |
#else | |
[[no_unique_address]] | |
#endif | |
Old m_old; | |
bool m_updated = false; | |
friend struct ChangeObserver; | |
Proxy(Parent ptr, Old old) requires(std::is_constructible_v<Old, Old &&>) | |
: m_ptr(ptr) | |
, m_old(std::move(old)) { } | |
Proxy(Parent ptr, Old const &old) requires(!std::is_constructible_v<Old, Old &&>) | |
: m_ptr(ptr) | |
, m_old(old) { } | |
public: | |
Child operator->() { | |
m_updated = !isConst; | |
return &m_ptr->m_data; | |
} | |
Child operator->() const { | |
return &m_ptr->m_data; | |
} | |
Proxy(Proxy const &) = delete; | |
Proxy(Proxy &&) = delete; | |
Proxy &operator=(Proxy const &) = delete; | |
Proxy &operator=(Proxy &&) = delete; | |
Proxy &operator=(T value) requires(std::is_assignable_v<T, T &&>) { | |
m_updated = true; | |
m_ptr->m_data = std::move(value); | |
return *this; | |
} | |
Proxy &operator=(T const &value) requires(!std::is_assignable_v<T, T &&>) { | |
m_updated = true; | |
m_ptr->m_data = value; | |
return *this; | |
} | |
~Proxy() { | |
if (!m_updated) return; | |
if constexpr (!isConst) { | |
if constexpr (keepOldCopy) { | |
if (!m_ptr->compare(m_ptr->m_data, m_old)) m_ptr->runCallbacks(m_old, m_ptr->m_data); | |
} else { | |
m_ptr->runCallbacks(m_ptr->m_data); | |
} | |
} | |
} | |
}; | |
public: | |
ChangeObserver() requires(std::is_default_constructible_v<T>) | |
: m_data() { } | |
/// | |
explicit ChangeObserver(T data) requires(std::is_constructible_v<T, T &&>) | |
: m_data(std::move(data)) { } | |
explicit ChangeObserver(T const &data) requires(!std::is_constructible_v<T, T &&>) | |
: m_data(data) { } | |
ChangeObserver &operator=(ChangeObserver other) requires(std::is_assignable_v<T, T &&>) { | |
if (&other != this && !compare(m_data, other.m_data)) runCallbacks(m_data, other.m_data); | |
m_data = std::move(other.m_data); | |
return *this; | |
} | |
ChangeObserver &operator=(ChangeObserver const &other) requires(!std::is_assignable_v<T, T &&>) { | |
if (&other != this && !compare(m_data, other.m_data)) runCallbacks(m_data, other.m_data); | |
m_data = other.m_data; | |
return *this; | |
} | |
ChangeObserver(ChangeObserver const &) = default; | |
ChangeObserver(ChangeObserver &&) = default; | |
~ChangeObserver() = default; | |
void update(T other) requires(std::is_assignable_v<T, T &&>) { | |
if (!compare(m_data, other)) { | |
runCallbacks(m_data, other); | |
} | |
m_data = std::move(other); | |
} | |
void update(T const &other) requires(!std::is_assignable_v<T, T &&>) { | |
if (!compare(m_data, other)) { | |
runCallbacks(m_data, other); | |
} | |
m_data = other; | |
} | |
Proxy<false> getRef() requires(keepOldCopy) { | |
return {this, m_data}; | |
} | |
Proxy<false> getRef() requires(!keepOldCopy) { | |
return {this, {}}; | |
} | |
Proxy<true> getRef() const { | |
return {this, {}}; | |
} | |
/** | |
* Drop-in-replacement for `getRef` that is always `const`. | |
* Identical to calling `getRef` on a `const` observer. | |
* | |
* Prefer `get` if possible. | |
*/ | |
Proxy<true> getCRef() const { | |
return {this, {}}; | |
} | |
T const &get() const { | |
return m_data; | |
} | |
template<typename Fn> | |
function_type const &onChange(Fn &&fn) { | |
return m_callbacks.emplace_back(std::forward<Fn>(fn)); | |
} | |
}; | |
} // namespace ut | |
#endif // UT_CHANGE_OBSERVER |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
wrapper class that invokes callbacks when it's held value changes |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment