Polymorphism allows a variable of the base class type to hold subclass objects, and to reference the methods of those subclasses directly from the superclass variable. It also allows a child class method to have a different definition than its parent class, if the parent class method is virtual.

12 min read | Intermediate Level

What You'll Learn

  • Why assigning a child object to a base-class handle is allowed, and why that handle only sees the base class's own view of it
  • Why the reverse assignment — base to child handle — is illegal without an explicit cast
  • $cast, and what happens when a downcast fails
  • How virtual methods change all of this by dispatching based on the object's actual type instead of the handle's declared type

Parent and Child Assignment

A class handle is just a container that can hold either parent or child class objects. It's important to understand how a parent class handle holding a child object — and vice versa — actually behaves.

Assign Child Class to Base Class

Taking the same example from SystemVerilog Inheritance, we'll assign a sub/child class instance sc to a base class handle bc:


module tb;
	Packet      bc; 	// bc stands for BaseClass
	ExtPacket   sc; 	// sc stands for SubClass

	initial begin
		sc = new (32'hfeed_feed, 32'h1234_5678);

		// Assign sub-class to base-class handle
		bc = sc;
  
		bc.display ();
		sc.display ();
	end
endmodule

Output

Output
ncsim> run
[Base] addr=0xfeedfeed
[Child] addr=0xfeedfeed data=0x12345678
ncsim: *W,RNQUIE: Simulation is complete.

Even though bc points to the child class instance, calling display() from bc still invokes the base class's own display(). This is because the function was resolved based on the type of the handle, not the type of object it's actually pointing to. Trying to reference a subclass-only member through a base class handle goes even further, producing a compile-time error:


module tb;
	Packet      bc; 	// bc stands for BaseClass
	ExtPacket   sc; 	// sc stands for SubClass

	initial begin
		sc = new (32'hfeed_feed, 32'h1234_5678);
		bc = sc;
  
        // Print variable in sub-class that is pointed to by
        // base class handle
		$display ("data=0x%0h", bc.data);
	end
endmodule

Output

Output
      $display ("data=0x%0h", bc.data);
                                    |
ncvlog: *E,NOTCLM (inheritance.sv,49|36): data is not a class item.

bc's declared type (Packet) simply has no data member, so the compiler rejects the reference entirely — regardless of what the object underneath actually contains.

Quick Check

Q: Would bc.display() behave differently if Packet's display() were declared virtual?

Show Answer

Yes. With virtual, dispatch would be based on the object's actual type instead of the handle's declared type — since bc genuinely points to an ExtPacket object, it would call ExtPacket's own display() instead of Packet's. This is exactly what the Virtual Methods section further down covers.

Assign Base Class to Child Class

It's illegal to directly assign a variable of a superclass type to a variable of one of its subclass types — this is a compile-time error:


module tb;
	initial begin
		bc = new (32'hface_cafe);
  
        // Assign base class object to sub-class handle
		sc = bc;
  
		bc.display ();
	end
endmodule

Output

Output
      sc = bc;
            |
ncvlog: *E,TYCMPAT (inheritance.sv,56|12): assignment operator type check failed (expecting datatype compatible with 'class $unit::ExtPacket' but found 'class $unit::Packet' instead).

$cast can be used instead, to assign a superclass handle to a subclass-typed variable, provided the superclass handle actually refers to an object that's assignment-compatible with the subclass:


module tb;
	initial begin
		bc = new (32'hface_cafe);
  
        // Dynamic cast base class object to sub-class type
		$cast (sc, bc);
  
		bc.display ();
	end
endmodule

This compiles fine, but fails at run time, because bc here is a genuine Packet object — not an ExtPacket in disguise — so it's not actually compatible with sc's type:

Output
ncsim> run
      $cast (sc, bc);
          |
ncsim: *E,BCLCST (./inheritance.sv,57|10): Invalid cast: a value with the class datatype '$unit_0x06d772f8::Packet' cannot be assigned to a class variable with the datatype '$unit_0x06d772f8::ExtPacket'.
[Base] addr=0xfacecafe
ncsim: *W,RNQUIE: Simulation is complete.

Now let's make bc point to another subclass object instead, and try the same cast again. Here, bc simply acts as a carrier for an object that's genuinely an ExtPacket underneath:


initial begin
   ExtPacket sc2;
   bc = new (32'hface_cafe);
   sc = new (32'hfeed_feed, 32'h1234_5678);
   bc = sc;
  
   // Dynamic cast sub class object in base class handle to sub-class type
   $cast (sc2, bc);
  
   sc2.display ();
   $display ("data=0x%0h", sc2.data);
end

Output

Output
ncsim> run
[Child] addr=0xfeedfeed data=0x12345678
data=0x12345678
ncsim: *W,RNQUIE: Simulation is complete.

This time the cast succeeds, and sc2 — properly typed as ExtPacket — can access data directly, no compile error involved.

Quick Check

Q: Both failed and successful $cast calls above cast from a Packet-typed handle called bc. What's actually different between them?

Show Answer

What matters for $cast is the actual object the handle points to at run time, not its declared type. In the failed case, bc points to a genuine Packet object. In the successful case, bc has been reassigned to point at the same object sc does — a real ExtPacket — so the cast has something valid to actually succeed against.

Virtual Methods

A method in the parent class can be declared virtual, which lets every child class override it with a different definition — as long as the return type and arguments (the prototype) stay the same:


class Base;
	rand bit [7:0] addr;
	rand bit [7:0] data;

	// Parent class has a method called 'display' declared as virtual
	virtual function void display(string tag="Thread1");
		$display ("[Base] %s: addr=0x%0h data=0x%0h", tag, addr, data);
	endfunction
endclass

class Child extends Base;
	rand bit en;

	// Child class redefines the method to also print 'en' variable
	function void display(string tag="Thread1");
		$display ("[Child] %s: addr=0x%0h data=0x%0h en=%0d", tag, addr, data, en);
	endfunction
endclass
Calling display() through a Base handle with and without virtual

SystemVerilog Virtual Methods covers this in full depth — with virtual, calling display() through a Base-typed handle pointing at a Child object would correctly dispatch to Child's own version, solving exactly the limitation demonstrated at the top of this article.

Where Is This Used?

UVM's factory and configuration database mechanisms depend directly on this pattern — objects are stored and passed around using base-class handles like uvm_object or uvm_component, and $cast (or UVM's own wrapped equivalents) is how a specific derived type is safely recovered when the code actually needs it.

Rules to Follow

  • Assignment of a derived class handle to a base class handle is allowed
  • Assignment of a base class handle to a derived class handle is not allowed, and results in a compile-time error

base 	b = new;
child 	c = new;
  
b = c; 	// Allowed
c = b; 	// Compilation Error

$cast returns 0 if the cast failed, so use the return value to catch a failure — with if or assert:


base 	b = new;
child 	c = new;
  
if (! $cast(c, b))
	$error("Cast failed !");
  
assert($cast(c, b)) else
	$error("Cast failed !");

Common Beginner Mistakes

Mistake #1: Assuming a base-typed handle can reach child-only members

As the bc.data example shows, this is a compile-time error, not a run-time one. Why? The compiler only knows what the handle's declared type promises — it has no way to know, at compile time, that the actual object underneath happens to have more members.

Mistake #2: Assigning a base handle directly into a child-typed handle

sc = bc; fails to compile, full stop — there's no implicit downcast in SystemVerilog. Why? A base class object isn't guaranteed to actually be the more specific subclass type, so the language requires you to be explicit about that assumption using $cast instead.

Mistake #3: Forgetting to check $cast's return value

A failed $cast doesn't stop simulation on its own — it just returns 0 and the destination handle doesn't end up with the intended object. Why? Code that assumes the cast always succeeds can silently continue operating without the object it expected, rather than failing loudly — always check the return value with if or assert.

Mistake #4: Assuming $cast works between any two unrelated classes

$cast still requires an actual inheritance relationship between the two types involved. Why? It's for safely recovering a more specific type from a handle that's currently typed more generally within the same class hierarchy — not a way to convert between two classes that have nothing to do with each other.

Practice Exercise

Challenge: Predict a Successful and a Failed Cast

Using the Packet/ExtPacket classes from this article, write code that creates a plain Packet object and a separate ExtPacket object, assigns both to Packet-typed handles, and attempts $cast into an ExtPacket-typed handle from each one. Predict which cast succeeds and which fails.

Hint

The declared type of the intermediate Packet-typed handle doesn't matter for $cast — only the actual object each one is currently pointing to does.

Solution

module tb;
  Packet    bc1, bc2;
  ExtPacket sc, result;

  initial begin
    bc1 = new (32'hface_cafe);                       // A plain Packet object
    sc  = new (32'hfeed_feed, 32'h1234_5678);
    bc2 = sc;                                         // bc2 actually points to an ExtPacket

    if ($cast (result, bc1))
      $display ("Cast from bc1 succeeded");
    else
      $display ("Cast from bc1 failed");

    if ($cast (result, bc2))
      $display ("Cast from bc2 succeeded");
    else
      $display ("Cast from bc2 failed");
  end
endmodule

The cast from bc1 fails, since bc1 points to a genuine Packet object with no ExtPacket underneath it. The cast from bc2 succeeds, since bc2 was reassigned to point at the same object sc does — a real ExtPacket — exactly the same distinction demonstrated earlier in this article.

Summary

In this tutorial, you learned:

  • A base class handle can hold a child class object, but without virtual, method calls through it still resolve to the base class's own version
  • A base-typed handle can't access a child-only member at all — that's a compile-time error, regardless of the actual object underneath
  • Assigning a base handle directly into a child-typed handle is illegal; $cast is required to attempt it explicitly
  • $cast succeeds or fails based on the object a handle actually points to at run time, not the handle's declared type — and it returns 0 on failure rather than stopping simulation
  • virtual methods (covered in their own article) are what let a base-typed handle correctly dispatch to a child's overridden method
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5
Cron Job Starts