SystemVerilog Classes and OOP: A Practical Guide

In SystemVerilog, a class is the foundation of every modern verification environment. If you have written RTL you already know modules, nets, and always blocks. A class is a different kind of tool: it is a blueprint for a dynamic object that you create, use, and discard at run time, exactly like software. Transactions, generators, scoreboards, and drivers in a UVM-style testbench are all classes. This guide builds the SystemVerilog class from first principles, with small, self-contained examples you can compile today on a free simulator.

What a class actually is

A class groups data and the code that acts on that data into one named unit. The data members are called properties and the code members are called methods. You write a class once, between the class and endclass keywords, then make as many objects from it as you need. Think of the class as the design of a packet, and each object as one physical packet travelling through your testbench. The class itself occupies no memory for data; only objects do.

Declaring a class and constructing objects

A class declaration by itself allocates nothing. To get a usable object you declare a handle, then call the built-in new constructor. A handle is a reference that points at an object on the heap. Before you assign it, a handle holds the special value null, which points at nothing; dereferencing a null handle is a classic beginner crash.

The new function is the constructor. It runs exactly once, when the object is created, and is the right place to initialise properties. Arguments passed to new let you configure each object differently. The this keyword refers to the current object; it resolves the clash when a constructor argument shares a name with a property. A static property is shared by every object of the class rather than copied per object, which makes it ideal for counting how many objects have been built. In the example below, count is static, so both packets see the same running total.

module tb;
  class Packet;
    int unsigned addr;
    byte payload;
    static int count = 0;

    function new(int unsigned addr = 0, byte payload = 0);
      this.addr    = addr;
      this.payload = payload;
      count++;
    endfunction

    function void print(string tag);
      $display("[%s] addr=0x%02h payload=%0d (objects so far=%0d)",
               tag, addr, payload, count);
    endfunction
  endclass

  initial begin
    Packet p1;
    Packet p2;
    p1 = new(8'hA0, 42);
    p2 = new();
    p1.print("p1");
    p2.print("p2-default");
    p2.addr    = 8'hFF;
    p2.payload = 7;
    p2.print("p2-updated");
    $finish;
  end
endmodule
[p1] addr=0x000000a0 payload=42 (objects so far=2)
[p2-default] addr=0x00000000 payload=0 (objects so far=2)
[p2-updated] addr=0x000000ff payload=7 (objects so far=2)
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpr9r4j9jy/packet.sv:29: $finish called at 0 (1s)

Notice three things. The handles p1 and p2 are declared first and are null until new runs. The constructor supplies default argument values, so new() with no arguments is legal and produces a zeroed packet. Finally, the properties are reached from outside the class with the dot operator, as in p2.addr, while inside a method they are visible directly.

Methods: functions and tasks

Methods come in two flavours. A function executes in zero simulation time and may return a value, so it suits computation such as building a packet or comparing results. A task may consume time with delays or event controls, so it suits driving pins or waiting for a clock edge. The print method above is a void function. Inside any method you access properties by their bare names; you only need this when a local name would otherwise shadow a property.

Inheritance with extends and super

Inheritance lets a new class reuse and extend an existing one. The child class uses the extends keyword and automatically gains every property and method of the parent; you add only what is new. This is how a verification library keeps one base transaction and derives specialised read and write transactions from it. The super keyword reaches the parent version of a member. A child constructor must call super.new first, so the parent portion of the object is constructed before the child adds its own fields.

module tb;
  class Transaction;
    int unsigned addr;
    int unsigned data;

    function new(int unsigned addr, int unsigned data);
      this.addr = addr;
      this.data = data;
    endfunction

    function void show();
      $display("  addr=0x%0h data=0x%0h", addr, data);
    endfunction
  endclass

  class WriteTxn extends Transaction;
    bit [3:0] strobe;

    function new(int unsigned addr, int unsigned data, bit [3:0] strobe);
      super.new(addr, data);
      this.strobe = strobe;
    endfunction

    function void show();
      $display("WriteTxn:");
      super.show();
      $display("  strobe=0b%04b", strobe);
    endfunction
  endclass

  initial begin
    Transaction t;
    WriteTxn    w;
    t = new(32'h1000, 32'hDEAD);
    $display("Transaction:");
    t.show();
    w = new(32'h2000, 32'hBEEF, 4'b1010);
    w.show();
    $finish;
  end
endmodule
Transaction:
  addr=0x1000 data=0xdead
WriteTxn:
  addr=0x2000 data=0xbeef
  strobe=0b1010
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp08tq4sjv/inherit.sv:39: $finish called at 0 (1s)

Here WriteTxn inherits addr and data and adds a strobe. Its show method overrides the parent’s, but reuses it through super.show() rather than duplicating the print logic. This layering keeps shared behaviour in one place, which is exactly the discipline a large testbench needs.

Polymorphism and virtual methods

Polymorphism is the real reason OOP matters in verification. When a method is declared virtual, the version that runs is chosen from the actual object type at run time, not from the handle type. So you can keep a collection of base-class handles, each pointing at a different derived object, call one method on all of them, and watch each object do the right thing. A driver can accept a base-transaction handle and still invoke the correct derived behaviour. Drop the virtual keyword and the call binds to the handle type instead, always running the base version, which is almost never what you intended.

module tb;
  class Stimulus;
    int v;
    function new();
      v = 0;
    endfunction
    virtual function int next();
      return 0;
    endfunction
  endclass

  class Counter extends Stimulus;
    virtual function int next();
      v = v + 1;
      return v;
    endfunction
  endclass

  class Doubler extends Stimulus;
    function new();
      super.new();
      v = 1;
    endfunction
    virtual function int next();
      v = v * 2;
      return v;
    endfunction
  endclass

  Stimulus gens[$];
  Counter  c;
  Doubler  d;
  int i, a, b;

  initial begin
    c = new();
    d = new();
    gens.push_back(c);
    gens.push_back(d);
    for (i = 0; i < gens.size(); i++) begin
      a = gens[i].next();
      b = gens[i].next();
      $display("gen[%0d]: first=%0d second=%0d", i, a, b);
    end
    $finish;
  end
endmodule
gen[0]: first=1 second=2
gen[1]: first=2 second=4

The queue gens holds base Stimulus handles, yet each slot points at a different derived object. Because next is virtual, element 0 increments while element 1 doubles, from one identical call site. That single loop stands in for how a real environment applies the same action to a heterogeneous mix of stimulus objects.

Compiling and running

Save each block to its own file. With Icarus Verilog, compile with the 2012 standard and run the generated output; the first two examples run directly. Class handles stored in a queue, as in the polymorphism example, are best run on a simulator with full class support such as Verilator built as a timed binary, with the top module named tb. Either way you can experiment without any licensed tool.

Common pitfalls

MistakeResultFix
Using a handle before newNull-handle dereference, run-time crashAlways construct with new before use
Omitting super.new in a childElaboration errorCall super.new as the first statement
Forgetting virtualBase method runs instead of the derived oneMark overridable methods virtual
Assigning one handle to anotherBoth alias the same objectDeep-copy fields if you need a separate object

Master these mechanics and the SystemVerilog class stops being abstract jargon and becomes what it really is: a precise, reusable way to model the moving parts of a testbench.

Quick check

What value does a class handle hold before you call new?

It holds null. The handle is only a reference; until new allocates an object on the heap and returns a reference to it, there is nothing to point at, and using the handle is a null dereference.

Why must a derived constructor call super.new first?

Because the parent portion of the object must be constructed before the child adds its own fields. super.new runs the parent constructor; it has to be the first statement in the child’s new.

What does the virtual keyword change about a method call?

It makes the call resolve by the actual object type at run time rather than the handle type. This is what lets a base-class handle invoke the correct derived override, enabling polymorphism.

How does a static property differ from an ordinary one?

A static property has a single copy shared by every object of the class, instead of one copy per object. It is useful for class-wide state such as counting how many objects have been created.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Compile and simulate this in the browser — no install. Edit the code and re-run instantly.

Open in Verification Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD