这个问题是关于如何设计程序,以便进行某些修改。
我有一个类,它包含一些(非平凡的)数据,并且有几个成员函数可以更改这些数据。
有时我需要计算这些数据的某些属性。但是,每次改变都要从头开始重新计算它。相反,计算这些属性的小更新要快得多。
我有几个这样的属性,我需要能够轻松地添加到我的班级或从班级中删除(或打开/关闭)以进行一些数值实验。该类仅由我自己修改,用于数值模拟(科学代码)。
我们说我有一个包含数字x
的班级。但我还需要2^x
("属性" x
)。基本课程是:
class C {
double x;
public:
C() : x(0.0)
{ }
void inc() { x += 1; }
void dec() { x -= 1; }
void set(double x_) { x = x_; }
};
但现在我需要跟踪2^x
,并在每次x
更改时不断更新此值。所以我最终得到了
class expC {
double expx;
public:
expC(const double &x) {
recompute(x);
}
void inc() { expx *= 2; } // fast incremental change
void dec() { expx /= 2; } // fast incremental change
void recompute(const double &x) {
expx = std::pow(2, x); // slow recomputation from scratch
}
};
class C {
double x;
expC prop1; // XX
public:
C() : x(0.0), prop1(x) // XX
{ }
void inc() {
x += 1;
prop1.inc(); // XX
}
void dec() {
x -= 1;
prop1.dec(); // XX
}
void set(double x_) {
x = x_;
prop1.recompute(x); // XX
}
};
XX
标记我需要对班级C
进行的更改。这是很多变化,容易出错。有几个属性变得更加复杂,我甚至相互依赖。
class C {
double x;
expC prop1; // XX
someC prop2; // XX
public:
C() : x(0.0), prop1(x), prop2(x, prop1) // XX
{ }
void inc() {
x += 1;
prop1.inc(); // XX
prop2.inc(); // XX
}
void dec() {
x -= 1;
prop1.dec(); // XX
prop2.dec(); // XX
}
void set(double x_) {
x = x_;
prop1.recompute(x); // XX
prop2.recompute(x, prop1); // XX
}
};
问题:这样的程序有什么好的设计?我确信它可以比上面做的更好。目标是:1)轻松添加/删除此类属性或打开/关闭其计算2)性能至关重要。 inc
和dec
在紧密的内循环中被调用并且相对较少。出于性能原因,它们不能虚拟化。
实际上x
是一个更复杂的数据结构。想想例如在graph中添加/删除边缘,并在此过程中跟踪其度数序列。
更新
@ tobi303要求我展示如何使用这个类。它的方式类似于:
void simulate(C &c) {
for (/* lots of iterations */) {
c.inc();
double p1 = c.prop1.value();
double p2 = c.prop2.value();
if (condition(p1,p2))
c.dec();
}
}
或者说:
它实际上是类似于Metropolis-Hasting algorithm的蒙特卡罗模拟。
一个具体的例子可能是"数据"在类C
(状态)中是Ising model的自旋状态(对于那些熟悉它的人),属性是系统的总能量和总磁化强度。单次旋转翻转后更新速度要快于从头开始重新计算。在实践中,我没有Ising模型,我有一些更复杂的东西。我有几个属性,一些快速计算,一些慢(实际上我有一些辅助数据结构,有助于计算属性)。我需要尝试不同属性的组合,所以我经常更改我在代码中包含的内容。有时我会实现新属性。当我不需要已经实现的属性时,我需要能够出于性能原因关闭计算(有些计算速度很慢)。
答案 0 :(得分:2)
只需be lazy,无需在需要时计算属性。它将删除大量代码和不必要的计算。
当您做需要您的属性时,如果它尚未在缓存中,请进行计算。因此,每个属性都需要一个布尔值来判断缓存是否是最新的,并且每次x
本身更新时都需要使布尔值无效。
基本上:
class C {
double x;
template <typename Value> struct cachedProp {
bool cache = false;
Value value;
}
cachedProp<expC> prop1;
cachedProp<someC> prop2;
//...
void invalidateCache() {
prop1.cache = false;
prop2.cache = false;
//...
}
public:
expC getProperty1() {
if (!prop1.cache) {
recalculateProp1();
prop1.cache = true;
}
return prop1.value;
}
void inc() {
x += 1;
invalidateCache();
}
};
编辑:甚至更懒惰的解决方案是在cache
中存储一个布尔值,而不是在上一次更新中存储一个整数,并在C
中维护一个计数器。每次缓存失效时,C
中的计数器都会增加。获取propX
后,如果计数器与propX.lastUpdate
不匹配,则更新`propX。
这样,使缓存无效只是一个操作,并且不必更新所有属性&#39;高速缓存中。
答案 1 :(得分:1)
这是一种可能适合您的方法:
class C {
double x;
expC prop1;
someC prop2;
.
.
.
template <typename F>
void for_each_property(const F &f)
{
f(prop1,x);
f(prop2,x,prop1);
.
.
.
}
public:
C() : x(0.0), prop1(x), prop2(x, prop1)
{ }
void inc()
{
x += 1;
for_each_property([](auto &prop,auto&& ...) {
prop.inc();
});
}
void dec()
{
x -= 1;
for_each_property([](auto &prop,auto&& ...) {
prop.dec();
});
}
void set(double x_)
{
x = x_;
for_each_property([](auto &prop,auto&& ... args) {
prop.recompute(args...);
});
}
};
添加新媒体资源时,只需在for_each_property()
中添加一个电话即可。变量的使用避免了为不同参数提供新的重载的需要,只要你坚持使用相同的公式。
这不会消除构造函数中的重复,除非您愿意切换到属性的默认初始化,然后调用set(0.0)
。