I'm working on an object that represents normal data values that utilize functional reactive programming that change their values when a dependant value is changed. What i mean is, let's say you have an var3 = var1 + var2; and then when you change the value of var1 the value of var3 is updated automatically. In c++ that's tough, but with an update function being called in another worker thread somewhere, you can give it the appearance of being functionally reactive.
So here was my method for doing so. I create a template object called a Reactive which can have any type in it, and then overload it's operators so that when two of these reactives are, say, added together, not only is the resultant value equal to their sum, but a lambda is made which stores the operation in an std::function which can be called again later to update the value of the resultant at any time you call it's update function.
A couple of issues arise. What if one of the dependant values is destructed. even though the resultant Reactive still has what it thinks is a valid lambda, the arguments the lambda has used no longer exist. To account for this, i turn to boost::signals2 to set up a system of signals and slots to inform resultants of the destruction of one of any of their dependants. When the resultant receives the signal, it's affecter function is nulled out and won't be called on update.
For the Reactives to be able to perform that + operation, a temporary reactive must be made, which has a signal of it's own and then the = operator must be overloaded to move the data in the temporary to the resultant reactive. Signals cannot be copied however. I dodged around this by containing the destruct signal in an std::unique_ptr , using std::move when the operator = received a Reactive &&. Crisis averted.
Now here is where i'm stuck. I realized later that despite move construction being fine, there was still no way to copy construct one of my Reactives because, lets say var3 = var1 + var2; and then you do this: var4 = var3; Then somehow the destruct signals in var1 and var2 need a way to inform var4 that they have been destructed. What i finally came up with was to set it up so that i had a subobject called Proxy, which was a functor, that contained a boost::signals2::signal, and then each object would have one contained in an std::shared_ptr. If a Reactive has a reference to that Proxy, then it connects it's inform destruct method to that proxy. Then the depandent attach there signals to that proxy. When the proxy is invoked, it invokes all it's connections as well. This allow for copies to recieve the destruct signals from the dependants as well.
Problem is, connecting a proxy to the dependant signal requires that a proxy have a copy constructor, or at least, that is the error msvc is giving me. Apparently boost::signals2::signal::connect uses it's copy constructor, which it can't because the proxy itself contains a signal. I give you all this information because i'm still not sure whether this is the best solution. I chose signals and slots because i'm most familiar with them, but if you have a better solution please point it out. Otherwise, please help me avoid this error.
Btw, Slot is just way of making the Unreact() function a functor, unique to each Reactive.
Here is the object:
template<class T>
class Reactive
{
template<class H>
friend class Reactive;
class Slot : public boost::signals2::trackable
{
public:
Slot(std::function<void()> & func) :
m_Func(func)
{}
void operator()()
{m_Func();}
private:
std::function<void()> m_Func;
};
class Proxy : public boost::signals2::trackable
{
Proxy(const Proxy & s);
Proxy & operator=(const Proxy & s);
public:
Proxy(){}
void operator()()
{m_Informer();}
void attach(Slot & m_Unreacter)
{m_Informer.connect(m_Unreacter);}
private:
boost::signals2::signal<void()> m_Informer;
};
public:
~Reactive()
{
(*m_SendDestruct)();
}
Reactive() :
m_SendDestruct(new boost::signals2::signal<void()>),
m_Proxy(new Proxy),
m_ReceiveDestruct(std::function<void()>(std::bind(&Reactive::Unreact, this))),
m_Affecter(nullptr)
{
m_Proxy->attach(m_ReceiveDestruct);
}
template<class H>
Reactive(const H & data) :
m_SendDestruct(new boost::signals2::signal<void()>),
m_Proxy(new Proxy),
m_ReceiveDestruct(std::function<void()>(std::bind(&Reactive::Unreact, this))),
m_Affecter(nullptr),
m_Data(data)
{
m_Proxy->attach(m_ReceiveDestruct);
}
Reactive(const Reactive & reac) :
m_SendDestruct(new boost::signals2::signal<void()>),
m_Proxy(reac.m_Proxy),
m_ReceiveDestruct(std::function<void()>(std::bind(&Reactive::Unreact, this))),
m_Affecter(reac.m_Affecter),
m_Data(reac.m_Data)
{
m_Proxy->attach(m_ReceiveDestruct);
}
Reactive(Reactive && reac) :
m_SendDestruct(std::move(reac.m_SendDestruct)),
m_Proxy(reac.m_Proxy),
m_ReceiveDestruct(std::function<void()>(std::bind(&Reactive::Unreact, this))),
m_Affecter(reac.m_Affecter),
m_Data(reac.m_Data)
{
m_Proxy->attach(m_ReceiveDestruct);
}
Reactive & operator=(const T & data)
{
m_Data = data;
return *this;
}
Reactive & operator=(const Reactive & reac)
{
m_Proxy = reac.m_Proxy;
m_Proxy.attach(m_ReceiveDestruct);
m_Affecter = reac.m_Affecter;
m_Data = reac.m_Data;
}
Reactive & operator=(Reactive && reac)
{
m_SendDestruct = std::move(reac.m_SendDestruct);
m_Proxy = reac.m_Proxy;
m_Affecter(reac.m_Affecter);
m_Data(reac.m_Data);
}
template<class H>
Reactive & operator+(const H & rhs)
{
m_Data += rhs;
return *this;
}
template<class H>
auto operator+(Reactive<H> & rhs) -> Reactive<decltype(m_Data + rhs.m_Data)> &&
{
Reactive<decltype(m_Data + rhs.m_Data)> m_temp;
std::function<decltype(m_Data + rhs.m_Data)()> func;
if (!rhs.m_Affecter)
func = [&](){ return m_Data + rhs.m_Data;};
else
func = [&](){return m_Data + rhs.m_Affecter();};
m_SendDestruct->connect((*m_temp.m_Proxy));
rhs.m_SendDestruct->connect((*m_temp.m_Proxy));
return std::forward<Reactive<decltype(m_Data+rhs.m_Data)> &&>(m_temp);
}
template<class H>
Reactive && operator+(Reactive<H> && rhs)
{
Reactive && m_Temp
}
T & Get()
{
return m_Data;
}
void Update()
{
if(m_Affecter)
m_Data = m_Affecter();
}
void Unreact()
{
m_Affecter = nullptr;
(*m_SendDestruct)();
}
private:
std::unique_ptr<boost::signals2::signal<void()> > m_SendDestruct;
std::shared_ptr<Proxy> m_Proxy;
Slot m_ReceiveDestruct;
std::function<T()> m_Affecter;
T m_Data;
};
and the simple test
int main()
{
Reactive<int> vel(10);
Reactive<int> acc(5);
Reactive<int> time(5);
Reactive<int> result = vel + acc + time;
system("PAUSE");
return 0;
}
and here are the warnings/ errors:
1>main.cpp(86): warning C4355: 'this' : used in base member initializer list
1> main.cpp(83) : while compiling class template member function 'Reactive<T>::Reactive(Reactive<T> &&)'
1> with
1> [
1> T=int
1> ]
1> main.cpp(174) : see reference to class template instantiation 'Reactive<T>' being compiled
1> with
1> [
1> T=int
1> ]
1>main.cpp(66): warning C4355: 'this' : used in base member initializer list
1> main.cpp(174) : see reference to function template instantiation 'Reactive<T>::Reactive<int>(const H &)' being compiled
1> with
1> [
1> T=int,
1> H=int
1> ]
1>main.cpp(56): warning C4355: 'this' : used in base member initializer list
1> main.cpp(53) : while compiling class template member function 'Reactive<T>::Reactive(void)'
1> with
1> [
1> T=int
1> ]
1>c:\program files (x86)\boost\boost_1_53_0\boost\signals2\detail\slot_template.hpp(156): error C2248: 'Reactive<T>::Proxy::Proxy' : cannot access private member declared in class 'Reactive<T>::Proxy'
1> with
1> [
1> T=int
1> ]
1> main.cpp(32) : see declaration of 'Reactive<T>::Proxy::Proxy'
1> with
1> [
1> T=int
1> ]
1> main.cpp(30) : see declaration of 'Reactive<T>::Proxy'
1> with
1> [
1> T=int
1> ]
1> c:\program files (x86)\boost\boost_1_53_0\boost\signals2\detail\slot_template.hpp(81) : see reference to function template instantiation 'void boost::signals2::slot0<R,SlotFunction>::init_slot_function<F>(const F &)' being compiled
1> with
1> [
1> R=void,
1> SlotFunction=boost::function<void (void)>,
1> F=Reactive<int>::Proxy
1> ]
1> main.cpp(135) : see reference to function template instantiation 'boost::signals2::slot0<R,SlotFunction>::slot0<Reactive<T>::Proxy>(const F &)' being compiled
1> with
1> [
1> R=void,
1> SlotFunction=boost::function<void (void)>,
1> T=int,
1> F=Reactive<int>::Proxy
1> ]
1> main.cpp(178) : see reference to function template instantiation 'Reactive<T> &&Reactive<T>::operator +<int>(Reactive<T> &)' being compiled
1> with
1> [
1> T=int
1> ]
1>
1>Build FAILED.
1>
1>Time Elapsed 00:00:04.20
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
I think you have a design problem.
The logical lifetime of a reactive variable should not be tied to the lifetime of the C++ variable that names it.
Use the pImpl
pattern to have the lifetime of each reactive variable be as long as there is a C++ variable that names it, or so long as there is another reactive variable alive that references it. (std::shared_ptr
, with maybe something to detect circular references, which are bad mojo in a reactive graph anyhow).
When you move, you move the state inside the pImpl
, not the pImpl
itself -- the pImpl
has its own pointer-to-reactive-state (a ppImpl
), which is really useful for temporary reactives (such as A+B
).
When you depend on A
, you actually depend on A->pImpl
, and you up its reference count.
And if you set A = B
, that means that A->pImpl
depends on B->pImpl
, not A->pImpl
's reactive state is a copy of B->pImpl
's reactive state. This is a subtle difference.
This still requires some effort to have changes propagate forward. I'd have back weak_ptr
references towards those who depend on you, and a SetIsDirty
that propagates backwards through the tree. Anything that is dirty gets recalculated when it is read from, and otherwise the cached value is used.
If you want the effect of A = B
to cause A->pImpl
to be a copy of B->pImpl
, use a different syntax (like A = *B
, to steal a C++ism). This is the only situation that requires copying a pImpl
state!
Note that A = std::move(B)
requires moving a pImpl
state, hence the use of ppImpl
to store the state for optimization purposes.
You should find that no extra threads or slots/sockets are really needed. And unless you implement unary operator*
, no need to copy the state of a given pImpl
is needed.
Do note that my offhand comment about circular dependencies is important. A reactive graph with a circular dependency will leak under the above design, and more importantly can be logically impossible to calculate any value.