我有一类数据,必须存储在一个连续的数组中,为了更新该数据而迭代该数组。棘手的部分是我希望能够动态更改任何对象的更新方式。
这是我到目前为止想出的:
struct Update {
virtual void operator()(Data & data) {}
};
struct Data {
int a, b, c;
Update * update;
};
struct SpecialBehavior : public Update {
void operator()(Data & data) override { ... }
};
然后,我会为每个数据对象分配某种类型的更新。然后在更新期间,所有数据都会传递到它自己的更新函子:
for (Data & data : all)
data->update(data);
据我所知,这被称为策略模式。
我的问题:有没有办法更有效地做到这一点?某种方法可以在不调用虚拟方法的成本的情况下实现相同的灵活性?
虚函数调用的开销是多少?那么,实现必须做两件事:
- 从对象加载 vtable 指针。
- 从 vtable 加载函数指针。
这恰恰是两个内存间接。您可以通过将函数指针直接放置在对象中来避免两者之一(避免从对象查找 vtable 指针),这是 ralismarks 答案给出的方法。
这样做的缺点是它只适用于单个虚拟函数,如果添加更多,则会使用函数指针使对象膨胀,从而导致缓存压力增加,从而可能降低性能。只要你只是替换一个虚拟函数,没关系,再添加三个,你的对象就膨胀了 24 个字节。
除非确保编译器可以在编译时派生Update
的实际类型,否则无法避免第二个内存间接寻址。而且由于这似乎是使用虚函数在运行时执行决策的全部意义所在,因此您不走运:任何"删除"该间接寻址的尝试都会导致性能变差。
(我在引号中说"删除",因为您当然可以避免从内存中查找函数指针。代价将是您在从对象加载的某种类型标识值上执行类似switch()
或else if()
梯之类的东西,这将比仅从对象加载函数指针的成本更高。ralismarks答案中的第二个解决方案明确地做到了这一点,而维托里奥·罗密欧(Vittorio Romeo)的std::variant<>
方法将其隐藏在std::variant<>
模板中。间接寻址并没有真正删除,它只是隐藏在更慢的操作中。
你可以改用函数指针。
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;
}
}
如果你不需要开集多态性(即你事先知道所有派生自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
类。
或者,如果你想允许开放集多态性,但只能通过operator()(Data&)
接口,你可以使用类似function_view
的东西,它基本上是对具有特定签名的函数对象的类型安全引用。
struct Data {
int a, b, c;
function_view<void(Data&)> update_function;
};
for (Data & data : all)
{
data.update_function(data);
}