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 virtual changes dispatch to the object's actual type instead
  • Whether a child class's override needs to repeat the virtual keyword 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

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

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 virtual makes dispatch depend on the object's actual type instead, correctly reaching a child class's override
  • Only the base declaration strictly needs virtual for this to work, though repeating it on overrides is common practice for readability
  • virtual only affects method dispatch — it has no effect on how class properties are accessed through a handle
  • The general rule: declare a base class method virtual whenever a child class might reasonably want to override it
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts