Skip to content

Instantly share code, notes, and snippets.

@dk949
Last active June 8, 2025 23:08
Show Gist options
  • Save dk949/2508ff2b4b5ddf152e7b6c027e8f13c4 to your computer and use it in GitHub Desktop.
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
#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
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