只有一个虚函数会减慢整个班级的速度吗?
或者只是对虚拟函数的调用?如果虚函数实际上被覆盖,速度是否会受到影响,或者只要它是虚函数就没有影响。
拥有虚函数会减慢整个类的速度,因为在处理此类对象时,必须再初始化、复制一项数据……。对于一个有六个左右成员的班级,差异应该可以忽略不计。对于仅包含单个 char 成员或根本不包含成员的类,差异可能很明显。
除此之外,重要的是要注意并非每次对虚函数的调用都是虚函数调用。如果您有一个已知类型的对象,编译器可以为正常的函数调用发出代码,甚至可以内联所述函数,如果它愿意的话。只有当您通过可能指向基类对象或某个派生类对象的指针或引用进行多态调用时,您才需要 vtable 间接并为性能付出代价。
struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
Foo x; x.a(); // non-virtual: always calls Foo::a()
Bar y; y.a(); // non-virtual: always calls Bar::a()
arg.a(); // virtual: must dispatch via vtable
Foo z = arg; // copy constructor Foo::Foo(const Foo&) will convert to Foo
z.a(); // non-virtual Foo::a, since z is a Foo, even if arg was not
}
无论函数是否被覆盖,硬件必须采取的步骤基本相同。从对象中读取 vtable 的地址,从相应的槽中检索函数指针,并通过指针调用函数。就实际性能而言,分支预测可能会产生一些影响。因此,例如,如果您的大多数对象引用给定虚函数的相同实现,那么分支预测器就有可能在检索到指针之前正确预测要调用的函数。但哪个函数是通用函数并不重要:它可能是委托给未覆盖基本案例的大多数对象,或者是属于同一子类并因此委托给同一个覆盖案例的大多数对象。
它们是如何深入实施的?
我喜欢 jheriko 使用模拟实现来演示这一点的想法。但是我会使用 C 来实现类似于上面的代码的东西,以便更容易看到底层。
父类 Foo
typedef struct Foo_t Foo; // forward declaration
struct slotsFoo { // list all virtual functions of Foo
const void *parentVtable; // (single) inheritance
void (*destructor)(Foo*); // virtual destructor Foo::~Foo
int (*a)(Foo*); // virtual function Foo::a
};
struct Foo_t { // class Foo
const struct slotsFoo* vtable; // each instance points to vtable
};
void destructFoo(Foo* self) { } // Foo::~Foo
int aFoo(Foo* self) { return 1; } // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
0, // no parent class
destructFoo,
aFoo
};
void constructFoo(Foo* self) { // Foo::Foo()
self->vtable = &vtableFoo; // object points to class vtable
}
void copyConstructFoo(Foo* self,
Foo* other) { // Foo::Foo(const Foo&)
self->vtable = &vtableFoo; // don't copy from other!
}
派生类 Bar
typedef struct Bar_t { // class Bar
Foo base; // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { } // Bar::~Bar
int aBar(Bar* self) { return 2; } // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
&vtableFoo, // can dynamic_cast to Foo
(void(*)(Foo*)) destructBar, // must cast type to avoid errors
(int(*)(Foo*)) aBar
};
void constructBar(Bar* self) { // Bar::Bar()
self->base.vtable = &vtableBar; // point to Bar vtable
}
函数f执行虚函数调用
void f(Foo* arg) { // same functionality as above
Foo x; constructFoo(&x); aFoo(&x);
Bar y; constructBar(&y); aBar(&y);
arg->vtable->a(arg); // virtual function call
Foo z; copyConstructFoo(&z, arg);
aFoo(&z);
destructFoo(&z);
destructBar(&y);
destructFoo(&x);
}
所以你可以看到,vtable 只是内存中的一个静态块,主要包含函数指针。多态类的每个对象都将指向对应于其动态类型的 vtable。这也使得 RTTI 和虚函数之间的联系更加清晰:你可以通过查看它指向的 vtable 来检查一个类是什么类型。上面的内容在很多方面都得到了简化,例如多重继承,但一般概念是合理的。
如果arg 是Foo* 类型,而您采用arg->vtable,但实际上是Bar 类型的对象,那么您仍然会得到vtable 的正确地址。这是因为 vtable 始终是对象地址的第一个元素,无论它在正确类型的表达式中称为 vtable 还是 base.vtable。