Skip to main content

Static and Parameterized Classes

Every property covered so far belongs to one specific object — tr1.data and tr2.data are two independent pieces of storage, even though both objects came from the same class. Occasionally, though, a piece of data or behavior genuinely belongs to the class itself, not to any one instance — a running count of how many objects have been created, for instance. And separately, a class is sometimes written generically enough that it shouldn't be tied to one specific data type at all — a reusable queue-like container class, say, that should work identically whether it holds transactions or plain integers.

static properties: shared across every instance​

class transaction;
static int total_created = 0;
int id;

function new();
total_created++;
id = total_created;
endfunction
endclass
transaction t1 = new(); // t1.id = 1, total_created = 1
transaction t2 = new(); // t2.id = 2, total_created = 2
transaction t3 = new(); // t3.id = 3, total_created = 3

$display(transaction::total_created); // 3 — accessible without any instance at all

static int total_created has exactly one copy, shared by every object of the class — incrementing it in one object's new() is visible to every other object (and to code with no object at all, via transaction::total_created using the scope-resolution operator). Compare this to id, an ordinary (non-static) property — every object gets its own independent copy of id. A running instance counter, like this one, is the classic static-property use case; a shared configuration value read by every instance of a class is another common one.

static methods: callable without an object​

class transaction;
static int total_created = 0;

static function int get_total();
return total_created;
endfunction
endclass
$display(transaction::get_total()); // called via the class name — no object needed

A static method can be called through the class name alone, with no object required to exist first — appropriate for utility-style operations that don't need any particular instance's data, only shared (static) class-level data. A static method cannot access non-static (per-object) properties, since there's no specific object in scope to read them from — only other static members are reachable from inside one.

Parameterized classes: a class generic over a type​

class fifo #(type T = int);
T queue [$];

function void push(T item);
queue.push_back(item);
endfunction

function T pop();
return queue.pop_front();
endfunction
endclass
fifo #(int) int_fifo = new();
fifo #(transaction) txn_fifo = new();

int_fifo.push(42);
txn_fifo.push(new());

#(type T = int) declares a type parameter — T stands in for whatever concrete type is supplied at instantiation (fifo #(int), fifo #(transaction)), exactly the same idea as parameter/generate scaling a Verilog module's width at instantiation time in Parameters and generate, except here what's parameterized is a type, not a number. One fifo class definition serves as a queue of ints, a queue of transactions, or a queue of any other type, without duplicating the class once per type it needs to hold.

Each distinct #(...) specialization is its own separate type to the compiler, even though both come from the same class declaration — fifo #(int) and fifo #(transaction) are unrelated types with no assignment compatibility between them, exactly as if they'd been two entirely separate hand-written classes. int_fifo = txn_fifo; above would be a compile-time type-mismatch error, the same as assigning an int to a string.

Why this matters for UVM specifically

UVM's TLM ports (uvm_analysis_port #(my_transaction), uvm_tlm_fifo #(my_transaction), and similar) are parameterized classes exactly like fifo #(T) above — the #(my_transaction) is supplying UVM's own generic port/FIFO class with the specific transaction type your testbench uses. Recognizing this pattern here removes a piece of syntax that otherwise looks like unexplained UVM magic the first time it's encountered in TLM Basics & Analysis Ports.

More than one parameter, and mixing type with int​

A parameterized class isn't limited to a single type parameter — it can take several, and can mix a type parameter with an ordinary int parameter in the same declaration:

class packet #(type T = int, int DEPTH = 16);
T queue [$];

function void push(T item);
if (queue.size() < DEPTH)
queue.push_back(item);
endfunction
endclass

packet #(.T(transaction), .DEPTH(8)) txn_packet = new();

DEPTH here behaves exactly like a module's ordinary parameter — an overridable constant, not a type — while T is still the type parameter from before; both can be overridden independently at instantiation (.T(...), .DEPTH(...)), or left at their declared defaults.

Combining static and private: the singleton pattern​

static properties, static methods, and one more piece — a local (private) constructor — combine into a common and genuinely useful pattern: guaranteeing a class only ever has exactly one instance, shared by every piece of code that asks for it.

class sim_config;
static local sim_config m_instance;
int timeout = 1000;

local function new(); // local: nothing outside this class can call new() directly
endfunction

static function sim_config get();
if (m_instance == null)
m_instance = new();
return m_instance;
endfunction
endclass

sim_config::get().timeout = 500; // no object ever declared or new()'d by the caller

The local function new() is what makes this a genuine singleton rather than just a convention — since new() can't be called from outside the class, get() is the only way any code, anywhere, can obtain a sim_config handle, and it always hands back the same one. This is the same shape UVM itself uses internally in places a single, globally-shared object is needed — a central configuration object or a shared resource manager being the usual motivating case.

Section C summary​

Section C's four pages — Classes Basics, Inheritance and Polymorphism, Encapsulation and Abstraction, and this page — cover every core OOP mechanic SystemVerilog offers, and every one of them underlies how UVM's class library is actually built, whether or not that page called it out explicitly.

What's next​

Classes give a testbench structure. Section D turns to what testbenches actually do with that structure: generating stimulus. The next page covers rand/randc, constraint blocks, and randomize() — SystemVerilog's answer to writing every test case by hand.