在这种情况下是否可以避免使用虚拟方法调用?



我有一类数据,必须存储在一个连续的数组中,为了更新该数据而迭代该数组。棘手的部分是我希望能够动态更改任何对象的更新方式。

这是我到目前为止想出的:

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);

据我所知,这被称为策略模式。

我的问题:有没有办法更有效地做到这一点?某种方法可以在不调用虚拟方法的成本的情况下实现相同的灵活性?

虚函数调用的开销是多少?那么,实现必须做两件事:

  1. 从对象加载 vtable 指针。
  2. 从 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::variantboost::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);
}

相关内容

最新更新