C++:virtual function 怎么实现?
virtual function 能 inline 吗?
先弄清楚:每个概念是什么
- Virtual function 与 dynamic dispatch
- 通过 base 的 pointer 或 reference 调用时,执行的是 object 真实类型(dynamic type)的版本,而不是 pointer 声明的类型。例:
Base* b = new Derived; b->f();调的是Derived::f。 - vtable 与 vptr
- 标准没规定怎么实现,主流 ABI 的做法是每个 polymorphic class 一张函数指针表(vtable),同类 object 共用,每个 object 开头藏一个指向它的指针(vptr)。virtual call = 读 vptr → 查表 → indirect call。例:x86-64 上
struct { int x; virtual void f(); }的 sizeof 是 16(8 字节 vptr + int + padding),去掉 virtual 是 4。 - inline
- 原意是建议编译器把函数体展开,现在的语义是允许多个 translation unit 里各有一份相同的定义,所以 header 里定义的函数要加。class 内定义的 member function 自动 inline。展不展开由编译器决定。
- Devirtualization
- 编译器能确定 dynamic type 时,把 virtual call 换成直接调用,于是也能 inline。常见情况是直接对 object 调用、
final、qualified call。 - Virtual destructor 与 pure virtual
- 通过 base pointer
delete一个 derived object,base 的 destructor 不是 virtual 就是 undefined behavior。virtual void f() = 0;是 pure virtual,带它的 class 是 abstract class,不能直接创建 object。
看图:b->f() 怎么查 vtable
b->f() 怎么经过 vptr 找到 Derived::f。Derived override 过的格(蓝色)换成了 Derived 的函数。橙红色实线是代码里 g1 的两条指令,先读 vptr,再取第 0 格 indirect jump。橙红色虚线是 delete b,同样经过 vptr 取到 ~Derived。30 秒回答
A virtual call is dispatched on the object's dynamic type. Typical implementations give each polymorphic class a vtable of function pointers and put a vptr in every object, so a call through a base pointer loads the vptr, loads the slot and makes an indirect call. The main cost is that such a call usually can't be inlined. Today inline mainly means the function may be defined in several translation units; actual inlining is the compiler's choice. A virtual function can be inline, but it's only expanded when the compiler knows the dynamic type: a call on an object, a final class, or a qualified call. A polymorphic base needs a virtual destructor.
代码:什么时候能 devirtualize
struct Base {
virtual int f() { return 1; } // 类内定义 → 隐式 inline
virtual ~Base() = default; // 会被继承:destructor 要 virtual
};
struct Derived final : Base {
int f() override { return 2; }
};
int g1(Base* b) { return b->f(); } // 类型未知
int g2() { Derived d; return d.f(); } // 类型已知
int g3(Derived* d) { return d->f(); } // final
int g4(Base* b) { return b->Base::f(); } // qualified call
// gcc / clang -O2:
// g1: mov rax, QWORD PTR [rdi] ; 读 vptr(在 object 开头)
// jmp [QWORD PTR [rax]] ; 取 vtable 第 0 格,indirect jump
// g2、g3 → mov eax, 2;g4 → mov eax, 1(直接 inline)
必答点
- 按 dynamic type 调用;实现上 vtable 按 class 共用,每个 object 存一个 vptr
- 代价:两次 load + indirect call,最主要的是挡住了 inline;object 多一个 pointer
inline的真正语义:允许多个 translation unit 重复定义;展不展开由编译器决定- virtual 可以 inline,但只有 devirtualize(已知类型、
final、qualified call)后才会展开 - polymorphic base 要 virtual destructor,否则通过 base pointer
delete是 UB;不让外部 delete 时用 protected non-virtual
追问
在 constructor / destructor 里调 virtual function 会调到谁?
调的是正在构造或析构的那个 class 自己的版本,因为这时 derived 部分还没构造或已经析构。这时 virtual call 一个 pure virtual function 是 UB。
constructor 能是 virtual 吗?
不能。分派要靠已经建好的 object 的 dynamic type,构造之前还没有。
virtual function 的默认参数按哪个类型取?
按 static type,编译期就替换好了。Base 写 f(int x = 1)、Derived 写 f(int x = 2),通过 Base* 调 f() 执行的是 Derived::f,但 x = 1。
来源cppreference: virtual function specifier(Explanation、In detail、Virtual destructor、During construction and destruction)· cppreference: inline specifier(Explanation)· cppreference: abstract class · cppreference: constructors · Itanium C++ ABI §1.1(virtual table 定义)、§2.4(vptr 在 offset 0)、§2.5(Virtual Table Layout)