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
virtualmethods 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
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
$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
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:
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
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
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;
$castis required to attempt it explicitly $castsucceeds or fails based on the object a handle actually points to at run time, not the handle's declared type — and it returns0on failure rather than stopping simulationvirtualmethods (covered in their own article) are what let a base-typed handle correctly dispatch to a child's overridden method