In Inheritance and Polymorphism, we saw that a method called through a base class handle pointing to a child class instance still ends up executing the base class's own version, not the child's. If that method is declared virtual, only then does the child class's version actually run:
bc = sc; // Base class handle is pointed to a sub class
bc.display (); // This calls the display() in base class and
// not the sub class as we might think
This article puts both versions side by side, using the exact same classes from those two articles, so the effect of adding virtual is completely isolated — nothing else about the code changes.
7 min read | Intermediate Level
What You'll Learn
- Why a non-virtual method dispatches based on the handle's declared type, not the object's actual type
- How declaring a method
virtualchanges dispatch to the object's actual type instead - Whether a child class's override needs to repeat the
virtualkeyword itself - The general rule of thumb for when a base class method should be declared
virtual
Without virtual
// Without declaring display() as virtual
class Packet;
int addr;
function new (int addr);
this.addr = addr;
endfunction
// This is a normal function definition which
// starts with the keyword "function"
function void display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
// ExtPacket from the Inheritance article, repeated here since this
// article's examples build on it directly
class ExtPacket extends Packet;
int data;
function new (int addr, data);
super.new (addr);
this.data = data;
endfunction
function void display ();
$display ("[Child] addr=0x%0h data=0x%0h", addr, data);
endfunction
endclass
module tb;
Packet bc;
ExtPacket sc;
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
bc.display ();
end
endmodule
Output
ncsim> run [Base] addr=0xfeedfeed ncsim: *W,RNQUIE: Simulation is complete.
The base class's own display() runs, even though bc actually points to an ExtPacket object — dispatch here is based on bc's declared type, Packet.
With virtual
class Packet;
int addr;
function new (int addr);
this.addr = addr;
endfunction
// Here the function is declared as "virtual"
// so that a different definition can be given
// in any child class
virtual function void display ();
$display ("[Base] addr=0x%0h", addr);
endfunction
endclass
// ExtPacket from the Inheritance article, repeated here since this
// article's examples build on it directly
class ExtPacket extends Packet;
int data;
function new (int addr, data);
super.new (addr);
this.data = data;
endfunction
virtual function void display ();
$display ("[Child] addr=0x%0h data=0x%0h", addr, data);
endfunction
endclass
module tb;
Packet bc;
ExtPacket sc;
initial begin
sc = new (32'hfeed_feed, 32'h1234_5678);
bc = sc;
bc.display ();
end
endmodule
Output
ncsim> run [Child] addr=0xfeedfeed data=0x12345678 ncsim: *W,RNQUIE: Simulation is complete.
The exact same bc.display() call now runs ExtPacket's own version instead — with virtual, dispatch is based on the object's actual type, not the handle's declared type. The key takeaway: declare base class methods virtual whenever a child class might override them, so that existing base class handles correctly reach the override.
Quick Check
Q: In the "With virtual" example, does ExtPacket's own display() need the virtual keyword too, or only Packet's?
Show Answer
Only the base class's method strictly needs to be declared virtual for dispatch to work correctly — once a method is virtual anywhere in the hierarchy, every override of it behaves virtually too. That said, repeating virtual on the override (as shown here) is common practice for readability, so anyone reading the child class alone can immediately see it's participating in virtual dispatch.
Where Is This Used?
UVM's base classes declare nearly every method a user is expected to override — build_phase(), run_phase(), write(), and many others — as virtual, precisely so that user-extended components work correctly when the testbench handles them generically through their base class type, exactly the scenario demonstrated in this article.
Common Beginner Mistakes
Mistake #1: Forgetting to declare a base class method virtual
As the first example shows, this silently causes a base-typed handle to always run the base class's own version, no matter what child object it actually points to. Why? This can be a genuinely hard bug to spot, since the code compiles and runs without any error — it just quietly produces the base class's behavior instead of the override you expected.
Mistake #2: Assuming virtual needs to be repeated on every override
As the Quick Check above explains, only the base declaration strictly needs virtual. Why? It's not wrong to repeat it on each override — and it's often good practice for clarity — but functionally, the override participates in virtual dispatch either way, once the base method is virtual.
Mistake #3: Assuming virtual affects property access too
Declaring a method virtual has no effect on how class properties are accessed. Why? As shown in SystemVerilog Polymorphism, accessing a child-only property through a base-typed handle is a compile-time error regardless of whether any methods are virtual — virtual only changes method dispatch, not what members a handle's declared type can see.
Practice Exercise
Challenge: Make a Three-Level Chain Virtual
Using Packet, ExtPacket, and a third class ExtPacket2 (extending ExtPacket, adding a crc property and its own display()), declare display() as virtual at every level. Create an ExtPacket2 object, assign it to a Packet-typed handle, and call display() through that handle. Which version runs?
Hint
Virtual dispatch always resolves to the most-derived override that actually exists on the real object — it doesn't matter how many levels of inheritance separate the handle's declared type from the object's actual type.
Solution
class ExtPacket2 extends ExtPacket;
int crc;
function new (int addr, data, crc);
super.new (addr, data);
this.crc = crc;
endfunction
virtual function void display ();
$display ("[Grandchild] addr=0x%0h data=0x%0h crc=0x%0h", addr, data, crc);
endfunction
endclass
module tb;
Packet bc;
ExtPacket2 gc;
initial begin
gc = new (32'hfeed_feed, 32'h1234_5678, 32'habab_cdcd);
bc = gc; // bc is Packet-typed, but points to an ExtPacket2 object
bc.display ();
end
endmodule
ExtPacket2's own display() runs, printing the [Grandchild] version with all three fields — even though bc is declared as plain Packet, three inheritance levels away from the object's actual type. Virtual dispatch reaches all the way down to whichever override the real object actually has.
Summary
In this tutorial, you learned:
- Without
virtual, a method called through a handle always runs the version matching the handle's declared type, not the object's actual type - Declaring a base class method
virtualmakes dispatch depend on the object's actual type instead, correctly reaching a child class's override - Only the base declaration strictly needs
virtualfor this to work, though repeating it on overrides is common practice for readability virtualonly affects method dispatch — it has no effect on how class properties are accessed through a handle- The general rule: declare a base class method
virtualwhenever a child class might reasonably want to override it