Classes, Objects & Methods
The full SystemVerilog object model — class syntax, object handles and garbage collection, constructors with arguments, properties and methods, static members, this, handle assignment vs shallow copy vs deep copy, inheritance, super, data hiding, virtual methods, polymorphism, scope resolution, parameterised classes, and memory management.
📚 What is a Class?
A class is a user-defined type that bundles data (properties) with the operations that act on that data (methods). An object is an instance of a class, created dynamically at runtime. Classes are the foundation of SystemVerilog’s verification capabilities — constrained-random testbenches, scoreboards, drivers, monitors, and UVM components are all built from classes.
Four things distinguish classes from structs in SV:
- Dynamic allocation — objects exist only when explicitly created with
new; the declaration just creates a handle. - Strong typing — unlike packed structs, objects cannot be implicitly assigned to objects of different types.
- Safe handles — like Java references, not raw C pointers; no pointer arithmetic.
- True polymorphism — inheritance, virtual methods, abstract classes.
📄 Class Syntax
// Basic class declaration class MyClass; // class items here endclass // With lifetime class automatic MyClass; endclass // explicit automatic (default for classes) // With closing name class MyClass; endclass : MyClass // SV: closing name must match // Extending another class class SubClass extends BaseClass; endclass // Parameterised class class Stack #(type T = int); endclass // Abstract class (cannot be instantiated) virtual class BasePacket; endclass
Packet, Driver, Scoreboard) to distinguish class types from variables at a glance.
📦 The Packet Example — A Complete Class
This annotated example introduces every element of a basic class definition.
class Packet; // ── Class properties (data fields) ────────────────────────────── bit [3:0] command; bit [40:0] address; bit [4:0] master_id; integer time_requested; integer time_issued; integer status; // ── Constructor: called automatically when new is used ────────── function new(); command = IDLE; address = 41'b0; master_id = 5'bx; endfunction // ── Public methods ─────────────────────────────────────────────── task clean(); command = 0; address = 0; master_id = 5'bx; endtask task issue_request(int delay); // ... send request to bus after delay endtask function integer current_status(); current_status = status; // assign to function name = return value endfunction endclass
📌 Objects — Handles and null
A class variable holds an object handle — essentially a safe reference to a dynamically-allocated object. Declaring a class variable does not create an object; the handle starts as null. The object is created by calling new.
{command=IDLE
address=0
master_id=x}
Packet p; // handle declared — p is null p = new; // object created — p now holds valid handle // Combine into one line Packet q = new; // declare + create together // Null check before use task task1(integer a, obj_example myexample); if (myexample == null) myexample = new; // lazy creation endtask
if (h == null) before using a handle that might not have been initialised.
📋 Handle vs C Pointer vs chandle
| Operation | C pointer | SV object handle | SV chandle |
|---|---|---|---|
| Arithmetic (increment, etc.) | Allowed | Not allowed | Not allowed |
| For arbitrary data types | Allowed | Not allowed | Not allowed |
| Dereference when null | Error (possible crash) | Not allowed (defined error) | Not allowed |
| Casting | Allowed (unsafe) | Limited (via $cast) | Not allowed |
| Assign to address of data type | Allowed | Not allowed | Not allowed |
| Unreferenced objects garbage collected | No (manual free) | Yes (automatic) | No |
| Default value | Undefined | null | null |
| For class hierarchies | C++ only | Allowed | Not allowed |
📌 Object Properties
A class property is accessed via the dot operator on an object handle. Any SV data type can be a class property except net types (wire, tri) — nets are incompatible with dynamically allocated storage.
Packet p = new; // Reading and writing properties via handle p.command = INIT; p.address = $random; packet_time = p.time_requested; // Chained access through nested objects b1.a.j // reach into a (property of b1), access j p.next.next.val // traverse linked list — chain of handles
▶ Object Methods
A method is called via the dot operator — the object is the implicit “first argument”. Properties of the class are directly accessible inside the method without any qualification.
Packet p = new; status = p.current_status(); // access method through handle p.clean(); // call task method — modifies p's properties // NOT: status = current_status(p); — not OOP style // The object is the focus, not the function call. // Methods operate on their own instance's properties. // All class methods have AUTOMATIC lifetime for arguments/locals by default // (unlike module-level tasks which default to static)
🛠 Constructors (new)
Every class has a new constructor — either explicitly defined or auto-generated. The constructor runs every time an object is created. It cannot have timing controls (it must be non-blocking).
// Default constructor: initialises all properties to type defaults // Called automatically even if you don't define new() // Custom constructor: initialise specific properties class Packet; integer command; function new(); command = IDLE; endfunction endclass // Constructor with arguments (allows runtime customisation) class Packet; integer command; bit [12:0] address; integer time_requested; function new(int cmd = IDLE, bit[12:0] adrs = 0, int cmd_time); command = cmd; address = adrs; time_requested = cmd_time; endfunction endclass // Calling with arguments Packet p = new(STARTUP, $random, $time); // Initialiser in declaration runs BEFORE the constructor body class Demo; int x = 10; // set to 10 first function new(); x = 20; // then overridden by constructor → x = 20 endfunction endclass
Packet p = new(...)) determines the object type. You cannot call new to get an integer or any other non-class type. The return type is always the class being constructed.
📌 Static Properties and Methods
A static property has one shared copy across all instances of the class. A static method can be called without a class instance and can only access static members.
class Packet; static integer fileId = $fopen("data", "r"); // shared by ALL Packet objects integer seq_num; // per-instance endclass // Access static property through any instance (or via class name) Packet p; c = $fgetc(p.fileId); // via instance — OK even before calling new c = $fgetc(Packet::fileId); // via class name — preferred for statics // Static method: callable without an instance class id; static int current = 0; static function int next_id(); next_id = ++current; // OK — static can access static property // x = this.y; // ILLEGAL — static cannot access 'this' or non-static endfunction endclass int my_id = id::next_id(); // call without any instance
static task foo() — a static class method: callable without an instance, no access to this or non-static members.task static bar() — a non-static class method with static variable lifetime: normal instance method, but its local variables persist between calls.
📌 this Keyword
this is a predefined handle that refers to the current object inside a non-static method. It is used to disambiguate when a local variable shadows a class property of the same name.
class Demo; integer x; // class property function new(integer x); // argument shadows class property this.x = x; // this.x = class property, x = argument endfunction endclass // Without 'this': x = x would be a no-op (argument assigned to argument) // With 'this': the class property gets the argument value — correct // this can also be passed as an argument to another task/function class Observer; task observe(Demo d); endtask endclass class Demo2; Observer obs; task report(); obs.observe(this); // pass a handle to myself endtask endclass
🔂 Assignment, Renaming, and Copying
SV has three distinct ways to “copy” an object. Understanding which one is happening is essential to writing correct testbench code.
Handle assignment — aliases (NOT a copy)
Packet p1 = new; Packet p2; p2 = p1; // p2 now points to the SAME object p2.command = 5; // ALSO changes p1.command! // one object, two names
new p1 — shallow copy (new object, shared sub-objects)
Packet p1 = new; Packet p2 = new p1; // copies all properties of p1 p2.command = 99; // only p2 changes // BUT: nested objects (handles) are still shared!
Shallow copy in detail — the B.a.j example
class A; integer j = 5; endclass class B; integer i = 1; A a = new; // B creates its own A object endclass function integer test; B b1 = new; // creates B object, which creates A object inside B b2 = new b1; // shallow copy: b2 gets its own B fields BUT shares b1's A object b2.i = 10; // b2.i = 10, b1.i still = 1 (own copy of 'i') b2.a.j = 50; // ALSO changes b1.a.j = 50 (shared 'a' handle!) test = b1.i; // = 1 (not changed) test = b1.a.j; // = 50 (shared A object was modified) endfunction
Deep copy — must be implemented manually
class Packet; bit[7:0] data[]; Packet next; function Packet deep_copy(); Packet copy = new this; // shallow copy first copy.data = new[data.size](data); // deep copy the dynamic array if(next != null) copy.next = next.deep_copy(); // recursively deep copy return copy; endfunction endclass
🔁 Inheritance and extends
A subclass extends a parent class, inheriting all its properties and methods. SystemVerilog uses single inheritance — each class has at most one parent.
class LinkedPacket extends Packet; LinkedPacket next; // new property not in Packet function LinkedPacket get_next(); get_next = next; // new method not in Packet endfunction // LinkedPacket inherits: command, address, master_id, // time_requested, time_issued, status, clean(), issue_request(), current_status() endclass // Subclass handle can be assigned to parent class variable (widening) LinkedPacket lp = new; Packet p = lp; // OK — every LinkedPacket IS a Packet // Through p, ONLY Packet members are visible (not LinkedPacket-specific ones) // Overridden non-virtual methods show the PARENT's version when called via p
📌 super Keyword
super accesses members of the immediate parent class from within a derived class, needed when a member is overridden and both versions are needed.
class Packet; // parent integer value; function integer delay(); delay = value * value; endfunction endclass class LinkedPacket extends Packet; // derived integer value; // overrides parent's value function integer delay(); // overrides parent's delay delay = super.delay() // call parent's delay() + value * super.value; // use both this class's AND parent's value endfunction endclass // super.new() must be the FIRST statement in a derived class constructor class EtherPacket extends Packet; function new(); super.new(); // MUST be first — initialises parent before child value = 10; // child-specific init endfunction endclass // Can also pass arguments with extends class EtherPacket extends Packet(5); // always passes 5 to Packet::new endclass
super.super.x is illegal. To access a grandparent member, the parent class method must itself call super.
🔒 Data Hiding — local, protected, public
class Packet; local integer i; // only visible inside Packet's methods protected integer j; // visible in Packet and its subclasses integer k; // public — visible everywhere function integer compare(Packet other); // local class properties CAN be accessed from a different instance of the SAME class compare = (this.i == other.i); // both i's accessible here endfunction endclass class LinkedPacket extends Packet; task do_stuff(); j = 10; // OK — j is protected, visible in subclass // i = 5; // ILLEGAL — i is local, NOT visible in subclass k = 20; // OK — k is public endtask endclass Packet p = new; // p.i = 5; // ILLEGAL — local: cannot access from outside the class // p.j = 5; // ILLEGAL — protected: cannot access from outside the hierarchy p.k = 5; // OK — public
📌 const Class Properties
Two kinds of constant properties exist in SV classes: global constants (value fixed at declaration) and instance constants (value set once per object in the constructor).
class Jumbo_Packet; const int max_size = 9 * 1024; // GLOBAL constant — same for all instances // typically also declared static byte payload[]; function new(int size); payload = new[size > max_size ? max_size : size]; // max_size = 100; // ILLEGAL — global const cannot be re-assigned endfunction endclass class Big_Packet; const int size; // INSTANCE constant — unique per object // no initial value here byte payload[]; function new(); size = $random % 4096; // ONE assignment in constructor — OK // cannot assign again anywhere else payload = new[size]; endfunction endclass
static const saves memory — only one copy exists. An instance constant cannot be static because that would prevent the per-object assignment in the constructor.
▶ Virtual Methods and Abstract Classes
A virtual method in a base class defines a contract that all derived classes must fulfil. When called through a base-class handle, the most-derived version of the method runs — this is the key to polymorphism.
virtual class BasePacket; // abstract: cannot instantiate directly virtual function integer send(bit[31:0] data); endfunction // body empty — pure virtual endclass class EtherPacket extends BasePacket; function integer send(bit[31:0] data); // Ethernet-specific implementation return 1; endfunction endclass // Non-virtual override: only dispatches correctly if called via a derived handle class Packet; integer i = 1; function integer get(); get = i; endfunction // NOT virtual endclass class LinkedPacket extends Packet; integer i = 2; function integer get(); get = -i; endfunction // overrides, NOT virtual endclass LinkedPacket lp = new; Packet p = lp; int j; j = p.i; // j = 1 (Packet's i, not LinkedPacket's) j = p.get(); // j = 1 (Packet's get(), NOT -2 or -1 — not virtual!) j = lp.get(); // j = -2 (LinkedPacket's get() — called via derived handle)
🔄 Polymorphism — Dynamic Method Dispatch
With virtual methods, a base-class handle calls the correct derived-class implementation at runtime, even when the compiler didn’t know which derived class would be stored.
BasePacket packets[100]; // array of base-class handles // Store different derived types EtherPacket ep = new; // extends BasePacket TokenPacket tp = new; // extends BasePacket GPSSPacket gp = new; // extends EtherPacket (which extends BasePacket) packets[0] = ep; packets[1] = tp; packets[2] = gp; // Virtual dispatch: calls the RIGHT implementation at runtime packets[1].send(); // calls TokenPacket::send(), not EtherPacket::send() packets[2].send(); // calls GPSSPacket::send() (if overridden), or EtherPacket::send() // Downcast using $cast — required for derived-class-specific operations EtherPacket ep2; if($cast(ep2, packets[0])) // succeeds: packets[0] holds an EtherPacket ep2.ether_specific_method(); if(!$cast(ep2, packets[1])) // fails: packets[1] is a TokenPacket $display("Not an EtherPacket");
:: Class Scope :: Operator
The :: operator accesses a specific named scope’s members — class names, package names, or static members — without needing an instance.
class Base; typedef enum {bin, oct, dec, hex} radix; static task print(radix r, integer n); endtask endclass int bin = 123; // local variable also named 'bin' Base b = new; // Disambiguate enum constant from local variable b.print(Base::bin, bin); // Base::bin = enum value, bin = integer 123 Base::print(Base::hex, 66); // call static method without instance // :: also accesses type declarations inside classes class StringList; class Node; // nested class string name; Node link; endclass endclass class StringTree; class Node; // different Node in different scope string name; Node left, right; endclass endclass // StringList::Node ≠ StringTree::Node — unambiguous StringList::Node n1; StringTree::Node n2;
📄 Out-of-Block Declarations (extern)
Method bodies can be defined outside the class using extern for the prototype and the class name qualifier for the implementation. This separates interface from implementation.
class Packet; Packet next; function Packet get_next(); // inline — short enough get_next = next; endfunction // extern: prototype only — body is outside the class extern protected virtual function int send(int value); endclass // Out-of-block body: drop qualifiers, add ClassName:: function int Packet::send(int value); // implementation here endfunction // Must be in the same scope as the class declaration. // Prototype and body must match exactly (same argument types, same return type).
📋 Parameterised Classes
Classes can be parameterised like modules — using value parameters for sizes and type parameters for element types. Each combination of parameters is a separate specialisation.
// Value parameter class vector #(int size = 1); bit [size-1:0] a; static int count = 0; // EACH specialisation has its OWN count endclass vector #(10) vten; // 10-bit vector vector #(.size(2)) vtwo; // named parameter typedef vector#(4) Vfour; // typedef for reuse // Type parameter — generic container class stack #(type T = int); local T items[]; task push(T a); endtask task pop(ref T a); endtask endclass stack is; // default: stack of int stack#(bit[1:10]) bs; // stack of 10-bit vectors stack#(real) rs; // stack of reals typedef stack#(Vfour) Stack4; // Parameterised inheritance class C #(type T = bit); endclass class D1 #(type P = real) extends C; // C with T=bit (default) class D2 #(type P = real) extends C #(integer); // C with T=integer class D3 #(type P = real) extends C #(P); // C with T=P (forwarded)
📌 typedef class — Forward Declarations
When two classes need handles to each other, declare one forward with typedef class before defining it.
typedef class C2; // forward declaration — C2 is of type class class C1; C2 c; // uses C2 before it is fully declared endclass class C2; C1 c; endclass
🔌 Memory Management
SystemVerilog uses automatic garbage collection — objects are reclaimed when no handle refers to them. You never call free(). This eliminates memory leaks and dangling pointers.
myClass obj = new; fork task1(obj); // both tasks hold a reference to obj task2(obj); join_none // obj cannot be freed here — task1 and task2 are still using it // When BOTH tasks finish AND obj itself goes out of scope, // the garbage collector reclaims the memory automatically obj = null; // parent no longer needs obj — but task1 and task2 still hold refs // Object stays alive until all references are dropped
null or goes out of scope, the object becomes eligible for collection. You never need to explicitly free anything — and you cannot accidentally cause a dangling pointer.
📋 Quick Reference
Class keyword glossary
| Keyword/syntax | What it does |
|---|---|
| class Name; endclass | Declare a class type |
| virtual class | Abstract class — cannot be instantiated |
| extends Base | Inherit all members of Base |
| Name h = new(args) | Create object, run constructor, store handle |
| h == null | Test for uninitialised handle |
| h2 = new h1 | Shallow copy of h1 into new object h2 |
| static property/method | Shared across all instances; no this in static method |
| this | Handle to current object; only in non-static methods |
| super.member | Access overridden parent member from derived class |
| super.new(args) | Call parent constructor — must be first in child’s new() |
| local | Private — class-only; not visible in subclasses |
| protected | Visible in class and subclasses; not outside |
| virtual function | Polymorphic — derived version called even via base handle |
| extern function | Prototype inside class; body defined outside with Class:: |
| class Name #(type T=int) | Parameterised class — separate specialisation per parameter set |
| Class::member | Scope resolution — static members, type declarations |
| typedef class C2 | Forward declaration for mutually-referencing classes |
rand and randc properties, constraint blocks, randomize(), constraint inheritance, inline randomize() with, disabling constraints with constraint_mode(), and pre/post randomisation hooks.
