Virtual Method

A virtual method is a method declared within a base class and is re-defined (overridden) by a derived class.

The virtual keyword is used for achieving dynamic dispatch for polymorphic types.

Reference

Also see Pure Virtual Function.

Should I declare this method as virtual or not?

YES: Declare virtual if you expect this method to be overridden by a descendant class;

  • the run-time system will always check to make sure the right implementation is used for any caller

NO: don’t use virtual if you are sure it won’t be overwritten, since it’s more efficient to let the compiler know the compiler can hardcode method address instead of doing run-time lookup.

  • Risk: If it is overridden, might get “wrong” definition in some situations. See example below.

Example

In the above example, Moan() uses Dynamic Dispatching, while Eat() uses Static Dispatching.

As you can see, calling Eat() doesn’t not print “cat is eating”.

See Static vs. Dynamic Type of Pointer, and Static and Dynamic Dispatching.

From CS247: Which method is called?

This is REALLY important to remember. To understand the differences in dispatching, ask the following questions:

1. Is the method being called on an object?

If so, always use the static type to determine which method is called.

Book b{...}; // b.isHeavy -> calls Book::isHeavy
Text t{...}; // t.isHeavy -> calls Text:::isHeavy
 
Book b = Text{...}; 
b.isHeavy(); // b.isHeavy -> calls Book::isHeavy

Note

When I was learning this, I never do polymorphism on objects, but rather pointers. Which is one the above doesn’t behave as I expect it.

2. Is the method called via pointer or reference?

This is the way I know it.

  1. Is the method NOT declared as virtual? Use the static type to determine which method to call.
Book* b = new Text{...}
b->nonVirtual(); // Calls Book::nonVirtual
  1. Is the method virtual? Use the dynamic type to determine which method is called.
Book* b = new Text{...};
b->isHeavy(); calls text::isHeavy

How virtual methods can be useful

Thanks to Virtual Methods, we can support

vector<Book*> bookcase;
bookcase.push_back(new Book{...});
bookcase.push_back(new Text{...});
bookcase.push_back(new Comic{...});
for (auto book: bookcase) {
	cout << book->isHeavy() << endl;
}

Each iteration calls a different isHeavy method.

What about (*book).isHeavy()?

(*book).isHeavy() calls the correct method as well, because *book yields a Book&, i.e. a reference.

Declare all methods as virtual?

Why not just declare everything as virtual for simplicity?

Because it takes an additional 8 bytes at least to store a vptr, in addition to the vtable. Consider the example below.

struct Vec {
	int x,y;
	void doSomething();
}
 
struct Vec2 {
	int x,y;
	virtual void doSomething();
}
 
Vec v{1,2};
Vec2 w{3,4};
 
cout << sizeof(v); // 8
cout << sizeof(w); // 16
  • Declaring doSomething as virtual doubles the size of our vec object, program consumes more RAM, slower in general.
  • This extra 8 bytes is storing the vptr - virtual pointer.
  • vptr allows us to achieve dynamic dispatch with virtual functions.

Remember, in MIPS, function calls use the JALR instruction. Saves a register, jumps PC to a specific function’s memory address, hardcoded in that machine instruction.

With dynamic dispatch, which function to jump to could depend on user input. Cannot be hardcoded.

Struct vec2 {
	int x,y;
	virtual void doSomething();
}
 
struct vec3: public vec2 {
	int z;
	void doSomething() override;
}

string choice;
cin >> choice;
vec2* v;
if (choice == "vec2")  {
	v = new vec2{...};
} else {
	v = new vec3{...};
}
v->doSomething();

How are virtual functions implemented?

See Virtual Method Table.