C++ 虚函数总是在 运行 时间内解析吗?

Is C++ virtual function always resolved in run time?

我有一个关于 C++ 虚函数解析时序的问题。在 C++ Primer 的 OOP 章节中,它提到:

Calls to Virtual Functions May Be Resolved at Run Time When a virtual function is called through a reference or pointer, the compiler generates code to decide at run time which function to call. The function that is called is the one that corresponds to the dynamic type of the object bound to that pointer or reference.

我明白上面的说法是什么意思:执行虚函数时,真正解析的是哪个版本,要看实际的调用类型pointer/reference。如果 pointer/reference 的实际类型是 base class,则 base class 的虚函数实际上是 运行,反之亦然。显然需要在 运行 时间内完成。

然而,C++ primer 中上述语句后面的示例让我困惑了一段时间:

double print_total(ostream &os, const Quote &item, size_t n)
{
  // depending on the type of the object bound to the item parameter
  // calls either Quote::net_price or Bulk_quote::net_price
  double ret = item.net_price(n);
}

// Quote is base class, Bulk_quote is derived class
Quote base("0-201-82470-1", 50);
print_total(cout, base, 10); // calls Quote::net_price
Bulk_quote derived("0-201-82470-1", 50, 5, .19);
print_total(cout, derived, 10); // calls Bulk_quote::net_price

我的问题是:

  1. 根据我的理解,在这个例子中,编译器能够在编译时知道实例基础的“真实类型”并且实例派生 因为它们只是在草图中明确声明!所以,我认为这个例子的解析时机可以在编译期。我说得对吗?
  2. 解析虚函数的时序可以是编译时吗?或者为了方便,C++ 只是让所有虚函数在 运行 时间内解析?由于 C++ primer 说:Calls to Virtual Functions May Be Resolved at 运行 Time,我不太确定 运行 时间是否都是这样。

我认为真正理解虚函数的解析时间对于每个C++用户来说都是非常重要的。我试图找到有关编译 time/run 时间的知识,但其中 none 可以帮助我弄清楚我的问题。有没有人对我的问题有任何想法。提前致谢!

通常,编译器会创建一个 vtable 并通过它调度虚方法调用,即在调用中增加一层间接。

但是优化编译器确实会尽量避免这种情况。这种优化通常称为“去虚拟化”。 这在何时以及如何工作在很大程度上取决于所讨论的编译器和代码。 Here is a nice blog post about it.

更多资源:

对话:Matt Godbolt on speculative devirtualization in LLVM

对话:2016 LLVM Developers’ Meeting: P. Padlewski “Devirtualization in LLVM”

对话:2018 LLVM Developers’ Meeting: P. Padlewski “Sound Devirtualization in LLVM”

论文:Ishizaki 等人,2000 年,“去虚拟化技术研究 对于 Java™ 即时编译器

论文:Padlewski et al, 2020, "Modeling the Invariance of Virtual Pointers in LLVM"

我发现使用 Compiler Explorer 检查各种编译器生成的实际汇编代码非常有用(Benjamin Maurer 评论文章中提供了 link)。

struct Base {
    virtual int f() { return 0; }
};

struct Derived : public Base {
    int f() override { return 1; }
};

int three(bool cond) {
    Derived da;
    Derived db;
    Base *p = cond ? &da : &db;
    return p->f();
}

函数“三”将在 x86-64 gcc 中被转移到程序集 没有 vtable。这意味着它在 编译器时间(静态绑定):

中解析
three(bool):
  movl , %eax
  ret

但是,x86-64 clang 将函数“三”传输到程序集 with vtable在 运行 时间内解决,动态绑定)

three(bool): # @three(bool)
  subq , %rsp
  movq $vtable for Derived+16, 16(%rsp)
  movq $vtable for Derived+16, 8(%rsp)
  testl %edi, %edi
  leaq 16(%rsp), %rax
  leaq 8(%rsp), %rdi
  cmovneq %rax, %rdi
  movq (%rdi), %rax
  callq *(%rax)
  addq , %rsp
  retq

总而言之,我认为 C++ 中的去虚拟化实际上取决于编译器如何优化来决定是否创建 vtable。我想如果编译器可以尽早确认绑定(比如在编译时),它可以避免 vtable 的额外资源使用也有助于提高性能,因为程序不需要在 运行 时间内分派函数。