问题
Problem description
In designing an observer pattern for my code, I encountered the following task: I have a class Observer
which contains a variable std::shared_ptr<Receiver>
and I want to use a weak_ptr<Receiver>
to this shared-pointer to safely call a function update()
in Observer
(for a more detailed motivation including some profiling measurements, see the EDIT below).
Here is an example code:
struct Receiver
{
void call_update_in_observer() { /* how to implement this function? */}
};
struct Observer
{
virtual void update() = 0;
std::shared_ptr<Receiver> receiver;
};
As mentioned there is a weak_ptr<Receiver>
from which I want to call Observer::update()
-- at most once -- via Receiver::call_update_in_observer()
:
Observer observer;
std::weak_ptr<Receiver> w (observer.receiver);
auto s = w.lock();
if(s)
{
s->call_update_in_observer(); //this shall call at most once Observer::update()
//regardless how many copies of observer there are
}
(Fyi: the call of update()
should happen at most once because it updates a shared_ptr
in some derived class which is the actual observer. However, whether it is called once or more often does not affect the question about "safeness" imo.)
Question:
- What is an appropriate implementation of
Observer
andReceiver
to carry out that process in a safe manner?
Solution attempt
Here is an attempt for a minimal implementation -- the idea is that Receiver
manages a set of currently valid Observer
objects, of which one member is called:
struct Receiver
{
std::set<Observer *> obs;
void call_update_in_observer() const
{
for(auto& o : obs)
{
o->update();
break; //one call is sufficient
}
}
};
The class Observer
has to take care that the std::shared_ptr<Receiver>
object is up-to-date:
struct Observer
{
Observer()
{
receiver->obs.insert(this);
}
Observer(Observer const& other) : receiver(other.receiver)
{
receiver->obs.insert(this);
}
Observer& operator=(Observer rhs)
{
std::swap(*this, rhs);
return *this;
}
~Observer()
{
receiver->obs.erase(this);
}
virtual void update() = 0;
std::shared_ptr<Receiver> receiver = std::make_shared<Receiver>();
};
DEMO
Questions:
Is this already safe? -- "safe" meaning that no expired
Foo
object is called. Or are there some pitfalls which have to be considered?If this code is safe, how would one implement the move constructor and assignment?
(I know this has the feeling of being appropriate for CodeReview, but it's rather about a reasonable pattern for this task than about my code, so I posted it here ... and further the move constructors are still missing.)
EDIT: Motivation
As the above requirements have been called "confusing" in the comments (which I can't deny), here is the motivation: Consider a custom Vector
class which in order to save memory performs shallow copies:
struct Vector
{
auto operator[](int i) const { return v[i]; }
std::shared_ptr<std::vector<double> > v;
};
Next one has expression template classes e.g. for the sum of two vectors:
template<typename _VectorType1, typename _VectorType2>
struct VectorSum
{
using VectorType1 = std::decay_t<_VectorType1>;
using VectorType2 = std::decay_t<_VectorType2>;
//alternative 1: store by value
VectorType1 v1;
VectorType2 v2;
//alternative 2: store by shared_ptr
std::shared_ptr<VectorType1> v1;
std::shared_ptr<VectorType2> v2;
auto operator[](int i) const
{
return v1[i] + v2[i];
}
};
//next overload operator+ etc.
According to my measurements, alternative 1 where one stores the vector expressions by value (instead of by shared-pointer) is faster by a factor of two in Visual Studio 2015. In a simple test on Coliru, the speed improvement is even a factor of six:
type Average access time ratio
--------------------------------------------------------------
Foo : 2.81e-05 100%
std::shared_ptr<Foo> : 0.000166 591%
std::unique_ptr<Foo> : 0.000167 595%
std::shared_ptr<FooBase>: 0.000171 611%
std::unique_ptr<FooBase>: 0.000171 611%
The speedup appears particularly when operator[](int i)
does not perform expensive calculations which would make the call overhead negligible.
Consider now the case where an arithmetic operation on a vector expression is too expensive to calculate each time anew (e.g. an exponential moving average). Then one needs to memoize the result, for which as before a std::shared_ptr<std::vector<double> >
is used.
template<typename _VectorType>
struct Average
{
using VectorType = std::decay_t<_VectorType>;
VectorType v;
std::shared_ptr<std::vector<double> > store;
auto operator[](int i) const
{
//if store[i] is filled, return it
//otherwise calculate average and store it.
}
};
In this setup, when the vector expression v
is modified somewhere in the program, one needs to propagate that change to the dependent Average
class (of which many copies can exists), such that store
is recalculated -- otherwise it will contain wrong values. In this update process, however, store
needs to be recalculated only once, regardless how many copies of the Average
object exist.
This mix of shared-pointer and value semantics is the reason why I'm running in the somewhat confusing situation as above. My solution attempt is to enforce the same cardinality in the observer as in the updated objects -- this is the reason for the shared_ptr<Receiver>
.
来源:https://stackoverflow.com/questions/35160431/c-safe-idiom-to-call-a-member-function-of-a-class-through-a-shared-ptr-class-m