我有一种必须存储在连续数组中的数据,为了更新该数据,迭代数组被重复。棘手的部分是我希望能够动态地改变任何对象的更新方式。
这是我到目前为止所提出的:
struct Update {
virtual void operator()(Data & data) {}
};
struct Data {
int a, b, c;
Update * update;
};
struct SpecialBehavior : public Update {
void operator()(Data & data) override { ... }
};
然后我会为每个数据对象分配一些类型的Update。然后在更新期间,所有数据都会传递给它自己的更新函数:
for (Data & data : all)
data->update(data);
据我所知,这就是战略模式。
我的问题:有没有办法更有效地做到这一点?有一些方法可以实现相同的灵活性而无需调用虚方法的成本吗?
答案 0 :(得分:16)
虚拟函数调用的开销是多少?那么,实现必须做两件事:
这恰恰是两个记忆间接。您可以通过将函数指针直接放在对象中来避免两者中的一个(避免从对象中查找vtable指针),这是ralismarks answer给出的方法。
这有一个缺点,它只适用于单个虚函数,如果你添加更多,你将使用函数指针膨胀你的对象,导致你的缓存压力更大,因此可能会降低性能。只要您只是替换单个虚函数,那就没问题,再添加三个,并且您的对象膨胀了24个字节。
除非确保编译器可以在编译时派生Update
的实际类型,否则无法避免第二个内存间接。而且由于似乎是使用虚函数在运行时执行决策的重点,所以你运气不好:任何“删除”间接的尝试都会产生更差的性能。
(我在引号中说“删除”,因为你当然可以避免从内存中查找一个函数指针。价格是你在某种类型上执行类似switch()
或else if()
梯形的东西识别从对象加载的值,这将比仅从对象加载函数指针更昂贵。ralismarks answer中的第二个解决方案明确地执行此操作,而{{1 Vittorio Romeo方法隐藏它在std::variant<>
模板中。间接不会被删除,它只是隐藏在更慢的操作中。)
答案 1 :(得分:10)
您可以改为使用函数指针。
struct Data;
using Update = void (*)(Data &);
void DefaultUpdate(Data & data) {};
struct Data {
int a, b, c;
Update update = DefaultUpdate;
};
void SpecialBehavior(Data & data) { ... };
// ...
Data a;
a.update = &SpecialBehaviour;
这避免了虚函数的成本,但仍然具有使用函数指针(较少)的成本。从C ++ 11开始,你也可以使用非捕获lambda(可以隐式转换为函数指针)。
a.update = [](Data & data) { ... };
或者,您可以使用枚举和开关语句。
enum class UpdateType {
Default,
Special
};
struct Data {
int a, b, c;
UpdateType behavior;
};
void Update(Data & data) {
switch(data.behavior) {
case UpdateType::Default:
DoThis(data);
break;
case UpdateType::Special:
DoThat(data);
break;
}
}
答案 2 :(得分:5)
如果您不需要 open-set polymorphism (即事先知道所有类型将来自Update
),您可以使用<强>变体,例如std::variant
或boost::variant
:
struct Update0 { void operator()(Data & data) { /* ... */ } };
struct Update1 { void operator()(Data & data) { /* ... */ } };
struct Update2 { void operator()(Data & data) { /* ... */ } };
struct Data {
int a, b, c;
std::variant<Update0, Update1, Update2> update;
};
for (Data & data : all)
{
std::visit(data.update, [&data](auto& x){ x(data); });
}
这将允许您:
避免 virtual
函数调用的费用。
以缓存友好的方式存储您的Data
个实例。
让Update
个类具有不同的接口或任意不同的状态。
另外,如果你想允许 open-set polymorphism 但只能通过operator()(Data&)
接口,你可以使用类似function_view
的东西,这基本上是一种类型 - 安全引用具有特定签名的功能对象。
struct Data {
int a, b, c;
function_view<void(Data&)> update_function;
};
for (Data & data : all)
{
data.update_function(data);
}