A class variable is never the object itself. It is a handle: a reference to an object on the heap, or null.
Every operation on a class variable goes through the reference. Reading t.addr means following handle t, finding the object, and reading its field.
Statement | What really happens | Objects after |
|---|---|---|
| A handle is declared with value null | 0 |
| One object is built; t points at it | 1 |
| The reference is copied; no object is built | Still 1 |
| A fresh object copies scalar fields one level | 2, sharing sub-objects |
a = new(); a.addr = 'h1000; b = a; b.addr = 'h2000; What does a.addr read now?'h2000. b = a copied the reference, not the object, so a and b name the same object. The write through b is the write a sees.
Assignment between handles copies the reference, not the object. After b = a there is one object with two names, and a write through either handle is visible through both.
class txn_l2;
int unsigned addr;
endclass
module tb_l2_alias;
int errors = 0;
initial begin
txn_l2 a;
txn_l2 b;
txn_l2 c;
a = new();
a.addr = 32'h1000;
b = a;
b.addr = 32'h2000;
if (a.addr != 32'h2000) errors++;
if (!(a == b)) errors++;
c = new();
c.addr = 32'h2000;
if (a == c) errors++;
if (a.addr != c.addr) errors++;
if (errors == 0) $display("PASS: aliasing and handle identity compare as specified");
else $fatal(1, "FAIL: aliasing and identity checks");
$finish;
end
endmodulePASS: aliasing and handle identity compare as specifiedThe testbench proves both halves. Handles a and b share one object, while c holds equal fields in a different object that == still rejects.
PASS: aliasing and handle identity compare as specifiedIdentity and equality differ. Comparison with == tests the reference. Field-by-field equality needs a hand-written compare method.
A declared handle starts as null. Dereferencing it is a runtime fatal, and the compiler cannot catch it because nullness is a runtime question.
The usual shape is a part of the testbench used without construction. One function builds the parts, another uses them, and the fatal lands far from the missing line.
// snippet
class pair;
comp left;
comp right;
function void build();
left = new();
// forgot: right = new();
endfunction
function void run();
left.run();
right.run(); // FATAL at runtime: right was never constructed
endfunction
endclassThe guard below is runnable. It catches the missing construction before use, then constructs and proceeds.
class comp_l2;
string name;
function new(string name = "comp");
this.name = name;
endfunction
function void run();
endfunction
endclass
module tb_l2_null;
int errors = 0;
int caught = 0;
initial begin
comp_l2 drv;
comp_l2 mon;
drv = new("drv");
if (mon == null) caught = 1;
if (caught != 1) errors++;
mon = new("mon");
if (mon == null) errors++;
drv.run();
mon.run();
if (errors == 0) $display("PASS: null handle caught by guard before use");
else $fatal(1, "FAIL: null guard checks");
$finish;
end
endmodulePASS: null handle caught by guard before useAn object stays alive while any chain of references reaches it: a named handle, a queue entry, a mailbox slot, or a property of another live object. When the last reference drops, the object becomes collectable.
Reclamation timing is implementation-dependent. The guarantee is only that a reachable object is never freed. There is no delete for class objects and no dangling handle.
class txn_life;
int unsigned addr;
endclass
module tb_l2_life;
int errors = 0;
initial begin
txn_life h;
txn_life keep;
txn_life t;
h = new();
h.addr = 32'hAAAA;
keep = h;
h = new();
h.addr = 32'hBBBB;
if (keep.addr != 32'hAAAA) errors++;
if (h.addr != 32'hBBBB) errors++;
if (h == keep) errors++;
keep = null;
for (int i = 0; i < 1000; i++) begin
t = new();
t.addr = i;
end
if (t.addr != 999) errors++;
if (errors == 0) $display("PASS: reassignment drops one reference and old object stays intact");
else $fatal(1, "FAIL: lifetime reassignment checks");
$finish;
end
endmodulePASS: reassignment drops one reference and old object stays intactReassignment never destroys an object. It drops one reference, and the old object survives while keep reaches it.
One object exists. Both handles name it, so a.addr reads 5.
In build. A part was used without new. Guard the place where parts are connected with explicit null checks.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. Icarus 13 cannot run the ones that compare class handles or keep a queue inside a class.
When an unreachable object is freed is up to the simulator; the benches check only that a reachable object is never lost.