在为我的代码设计观察者模式时,我遇到了以下任务:我有一个包含变量Observer
的类std::shared_ptr<Receiver>
,我想对此共享使用weak_ptr<Receiver>
-pointer安全地调用update()
中的函数Observer
(有关更详细的动机,包括一些分析测量,请参阅下面的编辑)。
以下是一个示例代码:
struct Receiver
{
void call_update_in_observer() { /* how to implement this function? */}
};
struct Observer
{
virtual void update() = 0;
std::shared_ptr<Receiver> receiver;
};
如前所述,我要通过weak_ptr<Receiver>
来调用Observer::update()
至少一次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:update()
的调用应该最多发生一次,因为它更新了某个派生类中的shared_ptr
,这是实际的观察者。但是,它是否被调用一次或多次不会影响关于“安全性”的问题。)
问题:
Observer
和Receiver
以安全的方式执行该过程的适当实施是什么?
这是一个最小化实现的尝试 - 想法是Receiver
管理一组当前有效的Observer
对象,其中一个成员被调用:
struct Receiver
{
std::set<Observer *> obs;
void call_update_in_observer() const
{
for(auto& o : obs)
{
o->update();
break; //one call is sufficient
}
}
};
班级Observer
必须注意std::shared_ptr<Receiver>
对象是最新的:
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>();
};
问题:
这已经安全了吗? - “安全”意味着不会调用过期的
Foo
对象。或者是否存在一些必须陷入的陷阱 认为如果此代码是安全的,那么如何实现移动构造函数和赋值?
(我知道这有一种适合CodeReview的感觉,但它更适合这个任务的合理模式而不是我的代码,所以我在这里发布了......而且移动构造函数仍然缺失。)< / p>
由于上述要求在评论中被称为“令人困惑”(我不能否认),这就是动机:考虑一个自定义Vector
类,为了节省内存,执行浅拷贝: / p>
struct Vector
{
auto operator[](int i) const { return v[i]; }
std::shared_ptr<std::vector<double> > v;
};
下一个具有表达模板类,例如对于两个向量的总和:
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.
根据我的测量结果,在Visual Studio 2015中,按值(而不是通过共享指针)存储矢量表达式的备选方案1更快两倍。在simple test on Coliru中,速度提升是甚至是六分之一:
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%
特别是operator[](int i)
执行昂贵的计算会导致呼叫开销微不足道时,会出现加速。
现在考虑对矢量表达式的算术运算太昂贵而不能每次重新计算(例如指数移动平均值)的情况。然后需要记住结果,就像使用std::shared_ptr<std::vector<double> >
之前一样。
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.
}
};
在此设置中,当在程序中的某处修改向量表达式v
时,需要将该更改传播到从属Average
类(其中可存在许多副本),以便{重新计算{1}} - 否则它将包含错误的值。但是,在此更新过程中,store
只需重新计算一次,无论有多少store
个对象存在。
这种共享指针和值语义的混合是我在如上所述的某种令人困惑的情况下运行的原因。我的解决方案尝试是在观察者中强制执行与更新对象中相同的基数 - 这就是Average
的原因。