I have created an observer and an observable object class. The idea being that you can create an observer that does some action whenever object being observed changes value.
Design goals:
- Be able to choose to notify all observers every time a value is assigned to the object being observed (even if that value is the same as its current value) or only if the value actually is different.
- If the object being observed is to notify its observers every time it is updated, the underlying object should not be required to define
operator==
. - If the object being observed goes out of scope, it should automatically disassociate itself with all of its observers.
- If an observer goes out of scope, it should automatically disassociate itself with the object it's observing.
- The observer and wrapped object being observed should be thread-safe.
Here's what I came up with:
//observable.h
#ifndef OBSERVABLE_T_H
#define OBSERVABLE_T_H
#include <unordered_map>
#include <functional>
#include <mutex>
#include <utility>
#include <memory>
#include <type_traits>
// make things slightly cleaner for pre C++14 compilers
template< bool B>
using enable_ = typename std::enable_if<B, void>::type;
template<typename T, bool NEW_ONLY> class Observer_t;
template<typename T, bool NEW_ONLY = false>
class Observable_t
{
public:
Observable_t(): object_{} {}
Observable_t(const T& obj): object_(obj) {}
~Observable_t()
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto& obs : observers_)
{
obs.second.get().remote_detach(object_);
}
}
unsigned attach(Observer_t<T, NEW_ONLY> &obs)
{
std::lock_guard<std::mutex> lock(mutex_);
unsigned observer_ID = observer_index_++;
observers_.emplace(std::make_pair(observer_ID, std::ref(obs)));
return observer_ID;
}
bool detach(unsigned observer_ID)
{
std::lock_guard<std::mutex> lock(mutex_);
if (observers_.find(observer_ID) == observers_.end())
{
return false;
}
observers_.erase(observer_ID);
return true;
}
const T& operator=(const T& new_data)
{
std::lock_guard<std::mutex> lock(mutex_);
update_observers(new_data);
return new_data;
}
template<typename... Args>
void update(Args... args)
{
std::lock_guard<std::mutex> lock(mutex_);
update_observers(T(std::forward<Args>(args)...));
}
T operator()() const
{
std::lock_guard<std::mutex> lock(mutex_);
return object_;
}
private:
unsigned observer_index_ = 1;
std::unordered_map<unsigned,
std::reference_wrapper<Observer_t<T, NEW_ONLY>>> observers_;
T object_;
mutable std::mutex mutex_;
template<bool trigger = NEW_ONLY>
enable_<trigger>
update_observers(const T& new_data)
{
if (new_data != object_)
{
for (auto& obs : observers_)
{
obs.second.get() = new_data;
}
object_ = new_data;
}
}
template<bool trigger = NEW_ONLY>
enable_<not trigger>
update_observers(const T& new_data)
{
for (auto& obs : observers_)
{
obs.second.get() = new_data;
}
object_ = new_data;
}
};
template<typename T, bool NEW_ONLY>
class Observer_t
{
public:
Observer_t(){}
Observer_t(Observable_t<T, NEW_ONLY> *object,
std::function<void(const T&)> callback): observable_ptr_(object),
callback_(callback)
{
attach();
}
~Observer_t(){detach();}
void attach()
{
std::lock_guard<std::mutex> lock(mutex_);
if(observer_ID_ != 0)
{
observable_ptr_->detach(observer_ID_);
}
observer_ID_ = observable_ptr_->attach(*this);
}
void detach()
{
std::lock_guard<std::mutex> lock(mutex_);
if(observer_ID_ != 0)
{
cached_object_ = (*observable_ptr_)();
observable_ptr_->detach(observer_ID_);
observer_ID_ = 0;
}
}
void remote_detach(T &obj)
{
std::lock_guard<std::mutex> lock(mutex_);
cached_object_ = obj;
observer_ID_ = 0;
}
T operator()() const
{
std::lock_guard<std::mutex> lock(mutex_);
if (observer_ID_ == 0) //i.e. we're detached
return cached_object_;
else
return (*observable_ptr_)();
}
private:
friend class Observable_t<T, NEW_ONLY>;
const T& operator=(const T& new_object)
{
std::lock_guard<std::mutex> lock(mutex_);
callback_(new_object);
return new_object;
}
Observable_t<T, NEW_ONLY>* observable_ptr_;
std::function<void(const T&)> callback_;
unsigned observer_ID_ = 0;
T cached_object_;
mutable std::mutex mutex_;
};
template<typename T>
using Observable_Object_t = Observable_t<T, false>;
template <typename T>
using Object_Observer_t = Observer_t<T, false>;
template<typename T>
using Observable_State_t = Observable_t<T, true>;
template<typename T>
using State_Observer_t = Observer_t<T, true>;
#endif // OBSERVABLE_T_H
A brief test driver:
#include <iostream>
#include <sstream>
#include "observable_t.h"
struct Attitude_t
{
Attitude_t(double yaw = 0, double pitch = 0, double roll = 0):
yaw_(yaw), pitch_(pitch), roll_(roll) {}
std::string str() const
{
std::ostringstream oss;
oss << "Attitude: Yaw: " << yaw_ << " pitch: " << pitch_ << " roll: "
<< roll_;
return oss.str();
}
Attitude_t& operator=(const Attitude_t& other) = default;
private:
double yaw_;
double pitch_;
double roll_;
};
int main()
{
Observable_State_t<int> oi;
oi = 3;
std::cout <<"oi is currently: " << oi() << std::endl;
State_Observer_t<int> so_i(&oi,[&](const int &new_int)
{
std::cout << "so_i received new int: " << new_int <<std::endl;
});
oi = 5;
oi = 5;
oi = 7;
oi.update(123442);
so_i.detach();
oi = 12;
std::cout <<"after detaching so_i is " << so_i() << std::endl;
Observable_Object_t<Attitude_t> attitude;
attitude = Attitude_t(1,2,3);
std::cout << attitude().str() << std::endl;
Object_Observer_t<Attitude_t> att_obs(&attitude, [&](const Attitude_t& new_att)
{
std::cout << "att_obs received: " << new_att.str() << std::endl;
});
attitude = Attitude_t(4,5,6);
attitude = Attitude_t(5,6,7);
attitude.update(12, 34, 52);
att_obs.detach();
attitude = Attitude_t(7,8,9);
std::cout << "Attitude is now: " << attitude().str() << std::endl;
std::cout << att_obs().str() << std::endl;
return 0;
}
See it run at coliru.
Thoughts?