An instance property gets a fresh copy in every object made by new(). A static property gets exactly one copy owned by the class, shared by every object.
The static copy is initialized once at its declaration and lives for the whole simulation. It never waits for the first new().
Member | Copies | Lifetime | Typical use |
|---|---|---|---|
Instance property | One per object | From new() until the object is reclaimed | Address, data, per-object id |
Static property | One per class | Whole simulation, set once at declaration | Population count, next unique id |
The standard idiom stamps each transaction with a unique id by incrementing a shared static counter inside new(). The id makes every log line traceable to one object.
Read statics through the class scope, as bus_txn::count. Handle access such as a.count is legal but reads like instance state.
A static method belongs to the class and runs with no object behind the call. It therefore has no this handle, reaches instance members only through an explicit object handle, and can never be virtual.
Virtual dispatch needs an object whose type selects the override. A static call has no object, so the combination is illegal.
a, b and c in that order. What are c.id and bus_txn::count afterwards?2 and 3. Each new() stamps the current count as the id, then advances the one shared counter.
The counter, the class-scope reads and the static-method rules in one self-checking file.
class bus_txn;
static int count = 0; // one copy for the whole class
int id; // one copy per object
int unsigned data;
function new(int unsigned d);
id = count; // stamp unique id
count++; // shared counter advances
data = d;
endfunction
// Legal: no 'this'; reads static state directly
static function int how_many();
return count;
endfunction
// ILLEGAL: static methods have no 'this', so these never compile:
// static function int get_id();
// return id; // no instance 'id' to read
// endfunction
// static virtual function void note(); // static cannot be virtual
endclass
module top;
bus_txn a;
bus_txn b;
bus_txn c;
int errors = 0;
initial begin
a = new(32'hA1); // a.id = 0, count = 1
b = new(32'hB2); // b.id = 1, count = 2
c = new(32'hC3); // c.id = 2, count = 3
if (a.id !== 0) errors++;
if (b.id !== 1) errors++;
if (c.id !== 2) errors++;
if (bus_txn::count !== 3) errors++;
if (bus_txn::how_many() !== 3) errors++;
if (a.count !== bus_txn::count) errors++; // handle read sees the same variable
if (errors == 0) $display("PASS: three txns stamped 0,1,2 with one shared count of 3");
else $fatal(1, "FAIL: id or shared-count values differ from 0,1,2 and 3");
$finish;
end
endmodulePASS: three txns stamped 0,1,2 with one shared count of 3Static state is shared across the entire simulation, not per environment instance. Two agents sharing a static queue silently couple environments that must stay independent.
The id counter is safe because ids only need global uniqueness. A static expected queue or static configuration knob is a bug of the same shape.
a is declared but no object exists yet. Which read is legal: bus_txn::count or a.count?Both. A static is reached through the class, and even through a null handle of that class. Prefer bus_txn::count, because a.count reads like instance state.
It reports an error. The static method has no this handle, so there is no instance field to read.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
class pkt_txn;
static int next_id = 0; // one counter for every packet in the run
int id; // this packet's own number
kind_e kind;
bit [1:0] dest;
bit [3:0] len;
function new(kind_e k = DATA, bit [1:0] d = 0, bit [3:0] n = 1);
id = next_id++;
kind = k; dest = d; len = n;
endfunction
function string sprint();
return $sformatf("pkt#%0d %s dest=%0d len=%0d", id, kind.name(), dest, len);
endfunction
endclass
module tb_pkt_id;
pkt_txn q[$];
pkt_txn p;
int errors = 0;
initial begin
p = new(DATA, 2, 2); q.push_back(p);
p = new(CTRL, 1, 1); q.push_back(p);
p = new(DATA, 0, 8); q.push_back(p);
foreach (q[i]) $display("%s", q[i].sprint());
foreach (q[i]) if (q[i].id != i) errors++;
if (pkt_txn::next_id != 3) errors++;
if (errors == 0) $display("PASS: three packets numbered 0, 1, 2 from one static counter");
else $fatal(1, "FAIL: %0d id checks failed", errors);
$finish;
end
endmodulepkt#0 DATA dest=2 len=2
pkt#1 CTRL dest=1 len=1
pkt#2 DATA dest=0 len=8
PASS: three packets numbered 0, 1, 2 from one static counterRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.