I edited the following on StackOverflow in an attempt for it to pass review so I could answer the question. The question had many comments saying it was bad or inappropriate. Others accepted that after my edit, it was valid. Since it has been a couple hours, I'm posting my answer after the quoted section.
Would someone please review the procedure below on how the compiler works with polymorphism and let me know if there's anything incorrect? Thanks!
Step 1: Creating and maintaining the vtable from the most base class to the most derived class based on the following rules:
If a function is declared virtual in a class (with or without ancestors), it will appear in the vtable.
If a derived class defines a function with the same name and signature as a virtual function in its ancestor, the derived class's version will replace the ancestor's version in the vtable, regardless of whether the derived class uses the override keyword. Otherwise, the derived function does not replace the base class's virtual function in the vtable. Instead, it is treated as a new function.
If a function in the derived class is declared as override in the derived class, then there should be a virtual function with the same name and signature in its ancestor class.
Non-virtual functions do not participate in the vtable and are resolved at compile time based on the static type.
Step 2: If we attempt to call a function (say, func()) using a pointer created by A* ptr_A = new C()
If the function is non-virtual in the inheritance hierarchy between A and C, ptr_A->func() calls that are declared in A based on static type.
If the function is virtual: At compile time, the compiler determines which functions are accessible via a static type of ptr_A (i.e., all public/protected virtual functions declared in A are accessible).
If the function is accessible at compile time, we search for the function name in the vtable and determine which specific virtual function implementation is executed.
Step 3: If we dynamic cast a pointer like this: C* ptr_C = dynamic_cast<C*>(ptr_A);
First, check if ptr_C is null. If (1) the C is not the descendant of A or if (2) all ancestors of C are non-polymorphic, then dynamic_cast returns null.
If ptr_C is not null, at runtime, dynamic_cast grants access to all public/protected functions declared in C, and those inherited from C's ancestors (A and B).
My Answer
The procedure has some misunderstandings, so it is good you asked. Rather than go line by line, let me simply explain it.Virtual / Override / VTables
- Any class with virtual functions has its own vtable holding the address of the virtual functions.
- A base class with virtual functions has a vtable - which holds the addresses of its functions. A hidden pointer in the base class holds the address of the vtable.
- A derived class has its own vtable which contains pointers to the virtual function from the base class and the virtual functions that override a base class function.
- This pattern continues if a class derives from Derived. New virtual functions are appended to the table; any override functions replace those from Derived or Base.
Class Function Points to Base virtual a Base::a virtual b Base::b Derived a override Derived::a no b Base::b virtual c Derived::c
Operation
- Non-virtual functions are straightforward. They are called directly with no lookup in the vtable. The same is true of virtual functions, which are directly called.
- The vtable is used only for virtual functions, which are called using a pointer to a class.
- The Base is simple, with virtual functions called through the Base vtable. The compiler doesn't know whether the pointer is to Base or Derived.
- Similarly, a Derived pointer uses its vtable for calls.
- Now it gets tricky with a pointer from Derived in a Base pointer. The Derived pointer in Base* points to Derived's vtable.
- When this Base pointer is called for function a, the compiler looks (this is at run-time) in the vtable for the address and retrieves the address of Derived::a.
- When this Base pointer is called for function b, the compiler looks (this is at run-time) in the vtable for the address and retrieves the address of Base::b.
- In short, virtual functions for any class are found through that class's vtable.
Some Code to Illustrate
class A {
public:
virtual ~A() {}
virtual void meow() { }
};
class B : public A {
public:
virtual void meow() override { }
virtual void woof() { }
};
class C : public B {
public:
virtual void moo() { }
};
int main() {
A* ptr_AC = new C();
ptr_AC->woof(); // calls B::woof
ptr_AC->moo(); // calls C::moo
C* ptr_C = dynamic_cast<C*>(ptr_A);
ptr_C->foo();
}
Hi Rud,
ReplyDeleteI’m the OP of this question. I appreciate you taking the time to reply to me. Thanks to your clarification, I now understand that each class with virtual functions has its own vtable.
I do have one point that needs further clarification. You mentioned:
"Non-virtual functions are straightforward. They are called directly with no lookup in the vtable. The same is true of virtual functions, which are directly called."
Does this mean that when calling a function on an object directly (e.g., B b; b.meow();), without using a pointer, the vtable lookup is not used—even if B and its ancestors have virtual functions??
Thanks again for your help!
-- Rocky
That is correct. `b.meow()` is called without accessing the `vtable.` I specifically mentioned it because it is often misunderstood.
DeleteGlad you came here to read the answer and thanks for getting back to me.
Delete