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

Viewed 634

I have a question regarding the resolving timing of a C++ virtual function. From chapter OOP in C++ Primer, it mentioned that:

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.

I understand what the above statement describes: when a virtual function is executed, which version is really resolved depends on the actual type of calling pointer/reference. If the actual type of pointer/reference is base class, the virtual function of base class is the one actually being run, vice versa. It obviously needs to be done in run time.

However, the example following by the above statement in C++ primer has confused me for a while:

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

My questions are:

  1. For my understanding, in this example, compiler is able to know in compile time the "real type" of instance base and instance derived as they are just declared obviously in sketch! So, I think the resolved timing of this example can be in compile time. Am I right about that?
  2. Can resolved timing of virtual functions be compile time? Or as a matter of convenience, C++ just makes all virtual functions resolved in run time? Since C++ primer says: Calls to Virtual Functions May Be Resolved at Run Time, I am not quite sure if run time is all the case.

I think to really understand the resolved time of virtual functions is very important to every C++ user. I tried to find the knowledge about compile time/run time but none of them can help me figure out my question. Do anyone have any thoughts on my questions. Thank you in advance!

2 Answers

In general, the compiler will create a vtable and virtual method calls are dispatched through it, i.e., there is one added level of indirection in calls.

But optimizing compilers do try to avoid this. This optimization is generally called "devirtualization". When and how this works very much depends on the compiler and code in question. Here is a nice blog post about it.

Some more resources:

Talk: Matt Godbolt on speculative devirtualization in LLVM

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

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

Paper: Ishizaki et al, 2000, "A Study of Devirtualization Techniques for a Java™ Just-In-Time Compiler"

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

I found it is super useful to use Compiler Explorer to check the actual assembly codes generate by various compilers (the link is provided in Benjamin Maurer comment's article).

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

Function "three" will be transferred into assembly without vtable in x86-64 gcc. It means it is resolved in compiler time (static binding):

three(bool):
  movl $1, %eax
  ret

However, x86-64 clang transfers function "three" into assembly with vtable (resolved in run time, dynamic binding)

three(bool): # @three(bool)
  subq $24, %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 $24, %rsp
  retq

In conclusion, I think the devirtualization in C++ really depends on how compiler optimizes to decide whether to create vtable or not. I suppose if compiler can confirm the binding as early as possible (like in compile time), it can avoid the extra resource usage for vtable also be helpful for performance as program doesn't need to dispatch function in run time.

Related