Inheritance in SystemVerilog OOPs:

In short: Inheritance lets one class (the child) reuse the fields and methods of another class (the parent) using the extends keyword. The child gets everything the parent has, can add new members, and can replace a parent method by overriding it. Mark methods virtual so the right version runs when you call through a parent handle. This is the mechanism the whole UVM library is built on.

Inheritance is the piece of object-oriented SystemVerilog that makes UVM possible. Every UVM class you use, from uvm_sequence_item to uvm_driver, is something you extend. This guide explains inheritance from the ground up: the keywords, base and derived classes, super, method overriding, the crucial role of virtual, what a child does and does not inherit, and when inheritance is the right tool. Every idea comes with runnable code and a comparison table, and we build one running example, a family of packet classes, so the pieces connect.

A simple way to picture it

Think of a form template at an office. There is a general application form with the common fields everyone fills in: name, date, and signature. A job application form starts from that general template and adds fields for work history. A leave application form also starts from the same template but adds fields for dates off. Both specialized forms inherit the common fields for free and only add what is special about them. If the office later changes the signature box on the base template, both specialized forms get the change automatically. Inheritance in code works the same way: the parent class is the template, and each child adds only what makes it different.

The words you need

TermMeaning
Base class (parent, superclass)The class others extend from
Derived class (child, subclass)A class that extends another and adds to it
extendsKeyword that creates the parent-child link
superReference to the parent inside a child
virtualMarks a method so the child version runs through a parent handle
overrideReplacing a parent method with a child version of the same name

A base class

Here is a base packet with the fields and behavior every packet shares. Notice print() is marked virtual; that choice matters later.

class packet;
  int    addr;
  int    payload;

  function new(int addr = 0, int payload = 0);
    this.addr    = addr;
    this.payload = payload;
  endfunction

  virtual function void print();
    $display("packet: addr=%0d payload=%0d", addr, payload);
  endfunction
endclass

A derived class

A child extends the base, reuses its fields, and adds its own. This error_packet adds an error-code field and its own behavior while keeping addr and payload from the parent.

class error_packet extends packet;
  int err_code;                 // new field, only in the child

  function new(int addr = 0, int payload = 0, int err_code = 0);
    super.new(addr, payload);   // build the parent part first
    this.err_code = err_code;   // then the child part
  endfunction
endclass

The error_packet now has three fields: addr and payload inherited from packet, plus its own err_code. You wrote the two common fields once, in the parent, and reused them.

Using super to reach the parent

Inside a child, super refers to the parent. The most common use is in the constructor: super.new(…) builds the parent portion before the child sets up its own fields. You also use super to call the parent version of a method you are overriding, so you keep the parent behavior and add to it rather than replacing it entirely.

function new(int addr, int payload, int err_code);
  super.new(addr, payload);   // required: initialize parent fields
  this.err_code = err_code;
endfunction

If a parent constructor takes arguments, the child must call super.new with them; skipping it is a common compile error.

Overriding a method

Overriding means the child defines a method with the same name and signature as the parent, replacing the behavior. Here error_packet overrides print() to also show the error code, while reusing the parent print through super.

class error_packet extends packet;
  int err_code;
  function new(int a=0, int p=0, int e=0);
    super.new(a, p); err_code = e;
  endfunction

  virtual function void print();
    super.print();                       // reuse parent output
    $display("  err_code=%0d", err_code); // add child detail
  endfunction
endclass

Calling super.print() first means you do not duplicate the parent formatting; you extend it. This add-on pattern keeps behavior consistent as the parent evolves.

Why virtual is the key to polymorphism

This is the idea that trips up most newcomers, so it is worth slowing down. A parent handle can point to a child object. What happens when you call a method through that handle depends entirely on one keyword: virtual.

packet p;                       // parent-type handle
error_packet ep = new(10, 99, 7);
p = ep;                         // parent handle points at a child
p.print();                      // which print runs?

If print() is virtual, SystemVerilog looks at the real object (an error_packet) and runs the child version, so you see the error code. If print() is not virtual, it runs the parent version based on the handle type, and the error code is lost. The rule is simple: mark a method virtual whenever a child might override it. This is exactly why UVM marks methods like build_phase and run_phase virtual, so your overrides are the ones that actually run.

Method isCall through parent handle runsResult
virtualChild version (based on real object)Override takes effect (what you usually want)
not virtualParent version (based on handle type)Override ignored (a frequent bug)

A second example: a scoreboard that stores base handles

Polymorphism is what lets one queue hold many packet types and treat them uniformly. The correct version of print() runs for each, because it is virtual.

packet       p1;
error_packet e1;
p1 = new(1, 10);
e1 = new(2, 20, 5);
packet q[$];
q.push_back(p1);
q.push_back(e1);
foreach (q[i]) q[i].print();   // each prints as its real type

What a child inherits, and what it does not

A child does not blindly get everything. Access modifiers decide what crosses the boundary.

Member kindInherited by child?Accessible in child code?
public (default)YesYes
protectedYesYes, inside the child only
localExists in the objectNo, hidden even from the child
Constructor new()Not inheritedMust call via super.new()
Static membersShared, not copiedYes, one copy across all instances

The two surprises here are local and the constructor. A local member of the parent physically exists inside the child object but the child code cannot touch it directly; use protected instead when you want children to have access. And constructors are never inherited, which is why you always write a child new() that calls super.new().

Single inheritance in SystemVerilog

SystemVerilog supports single inheritance: a class extends exactly one parent, not several. This keeps the model simple and avoids the ambiguity that multiple inheritance can cause. When you need behavior from more than one source, the usual answer is composition: hold a handle to a helper object and delegate to it, rather than trying to inherit from two parents. UVM itself is built entirely on single inheritance, so this is the pattern you will use in real code.

// composition instead of multiple inheritance
class logged_packet extends packet;
  logger log;                 // has-a logger, does not inherit from it
  function new(int a=0, int p=0);
    super.new(a, p);
    log = new();
  endfunction
endclass

When to use inheritance, and when not to

Reach for inheritance when there is a genuine is-a relationship: an error_packet is a packet, a read transaction is a transaction. In that case sharing the common parts through a parent is the right call. Avoid inheritance when the relationship is really has-a. A driver is not a logger, it has a logger, so you compose rather than extend. Overusing inheritance to share unrelated code produces tall, tangled class trees that are hard to follow, so prefer composition unless a real is-a link exists.

Up casting and down casting of class handles

When classes are related by inheritance, a handle of a parent type can point to a child object. Moving between a parent handle and a child handle is called casting. There are two directions: up casting and down casting.

A car analogy: think of a parent handle named vehicle and a child object that is a car. Every car is a vehicle, so you can always park a car in a “vehicle” spot without any paperwork. That is up casting, and it is always safe. But if a friend hands you a key labelled only “vehicle”, you cannot assume it opens a car. It might open a truck or a bike. Before you treat it as a car, you must check. That check is down casting, and SystemVerilog makes you do it with $cast.

The class tree used in these examples

Take a base class A and build a small family from it. B and C both extend A. D and E both extend C. Each class adds its own variable on top of what it inherits.

Class tree: A is the base, B and C extend A, D and E extend C, with arrows showing extends going down and upcasting going up
The same class family as a picture: arrows going down show extends (A to B, C, then C to D, E), and the arrow going up shows the safe up cast direction toward the parent.
class A;
  int a;
endclass

class B extends A;   // B gets: a, b
  int b;
endclass

class C extends A;   // C gets: a, c
  int c;
endclass

class D extends C;   // D gets: a, c, d
  int d;
endclass

class E extends C;   // E gets: a, c, e
  int e;
endclass

Here is what each handle can legally see:

ClassExtendsVariables visible through its own handle
A–a
BAa, b
CAa, c
DCa, c, d
ECa, c, e

Up casting: child to parent (always safe)

Up casting means assigning a child object to a parent handle. It never fails, because the child is guaranteed to contain everything the parent defines. No $cast is needed here, a plain assignment works.

A  a_h;      // parent handle
C  c_h;      // child handle
c_h = new();
a_h = c_h;   // up cast: legal, no $cast needed
// Through a_h you can only see 'a'.
// The 'c' field still exists in memory, but a_h cannot reach it.

The object still holds field c. Up casting does not delete anything, it only narrows what the handle is allowed to touch.

Down casting: parent to child (must be checked)

Down casting means taking a parent handle and assigning it to a child handle. The compiler cannot prove this is safe, because a parent handle might be pointing at a plain parent object, or at a different child. So SystemVerilog forbids a plain assignment and requires $cast, which checks at run time whether the real object is compatible.

A  a_h;
C  c_h;
a_h = new();          // a_h points at a plain A object

if ($cast(c_h, a_h))  // down cast attempt
  $display("Cast passed");
else
  $display("Cast failed: object is not a C");   // this runs

Because the real object is only an A, the down cast fails and $cast returns 0. Handle it with an if so your testbench does not crash.

Now the case that succeeds: the parent handle actually points at a C object.

A  a_h;
C  c_h;
c_h = new();
a_h = c_h;            // up cast first: a_h now points at a real C

if ($cast(c_h, a_h))  // down cast back to C
  $display("Cast passed, c = %0d", c_h.c);  // this runs

This mirrors what UVM does every day: uvm_object and uvm_component base handles are passed around, then $cast converts them back to the real transaction or component type before you use the extra fields.

Common mistakes to avoid

  • Forgetting super.new() when the parent constructor takes arguments, which fails to compile.
  • Overriding a method that is not virtual, so calls through a parent handle silently run the parent version.
  • Using local for a field you later want a child to access; use protected instead.
  • Trying to inherit from two classes; SystemVerilog allows only one parent, so use composition.
  • Reaching for inheritance to share code between unrelated classes, which creates a fragile class tree.

Keep learning

Inheritance is the base every UVM class stands on. See it in action in the UVM testbench architecture guide, where every component extends a UVM base class, and read how the UVM factory uses subclassing to swap behavior. Find more language articles on the SystemVerilog page and more testbench topics on the UVM page. Inheritance and virtual methods are favorite interview questions.

Frequently asked questions

Inheritance sits at the center of SystemVerilog OOP. Build on it with polymorphism, see how a base handle behaves, revisit the logic data type these classes are built from, and compare instance members with static properties and methods.

What is the difference between a base class and a derived class?

A base class is the parent that others build from; a derived class extends the base and adds or changes behavior. The derived class inherits the base class members and can add new ones, but the base class knows nothing about its children.

Why do I need super.new()?

Because constructors are not inherited. When a child object is created, its parent portion must be initialized, and super.new() does that. If the parent constructor takes arguments, the child must pass them through super.new(), or the code will not compile.

What does virtual actually change?

virtual decides which version of a method runs when you call it through a parent-type handle. With virtual, SystemVerilog picks the version matching the real object, so a child override runs. Without it, the parent version runs based on the handle type, which usually is not what you want.

Can a SystemVerilog class inherit from more than one class?

No. SystemVerilog supports single inheritance, so a class extends exactly one parent. When you need capabilities from several sources, use composition: hold handles to helper objects and delegate to them instead of inheriting.

What is the difference between local and protected members?

Both restrict outside access, but protected members are still visible to child classes, while local members are hidden even from children. Use protected when you want subclasses to reach a field, and local when even subclasses should not touch it.

How does UVM use inheritance?

Every UVM component and object you write extends a UVM base class, such as uvm_driver or uvm_sequence_item, and overrides virtual methods like build_phase and run_phase. The factory then relies on these subclass relationships to substitute one class for another, which is the backbone of UVM reuse.

Similar Posts