The built-in copy dst = new src allocates a fresh object and copies every property bit-for-bit, one level deep. Scalar fields become independent, but class-handle properties are copied as handles.
Both objects then point at the same sub-object. The constructor does not run during a shallow copy, so constructor bookkeeping is skipped too.
p2 = new p1; p2.hdr.id = 9; what is p1.hdr.id?9. new p1 copied the header handle, not the header, so p1 and p2 share one header object. A write through either packet changes it for both.
class hdr_l4;
int unsigned id;
endclass
class pkt_l4;
int unsigned addr;
hdr_l4 hdr;
function new();
hdr = new();
endfunction
endclass
module tb_l4_shallow;
int errors = 0;
initial begin
pkt_l4 p1;
pkt_l4 p2;
p1 = new();
p1.addr = 32'h1000;
p1.hdr.id = 7;
p2 = new p1; // shallow copy: constructor NOT run
p2.addr = 32'h2000;
if (p1.addr != 32'h1000) errors++;
p2.hdr.id = 9; // SHARED sub-object
if (p1.hdr.id != 9) errors++;
if (errors == 0) $display("PASS: shallow copy duplicates scalars and shares the nested header");
else $fatal(1, "FAIL: shallow copy checks");
$finish;
end
endmodulePASS: shallow copy duplicates scalars and shares the nested headerA deep copy duplicates the whole object graph. The standard pair is copy, which moves fields into an existing object, and clone, which builds a fresh object and then copies.
The form below is null-safe: it checks the source header before reading it, so a packet without a header copies as a packet without a header.
class hdr_c;
int unsigned id;
endclass
class pkt_c;
int unsigned addr;
hdr_c hdr;
function new();
hdr = new();
endfunction
// copy: move every field into an object that already exists.
function void copy(pkt_c rhs);
this.addr = rhs.addr;
if (rhs.hdr == null) this.hdr = null;
else begin
if (this.hdr == null) this.hdr = new();
this.hdr.id = rhs.hdr.id; // copy the header's contents, not its handle
end
endfunction
// clone: build a fresh object, then copy into it.
function pkt_c clone();
pkt_c c = new();
c.copy(this);
return c;
endfunction
endclass
PASS: clone and null-safe copy give independent headersA class that extends pkt_c adds fields that this clone would not copy. The inheritance topic makes copy and clone virtual, so each derived class copies its own fields too.
Need | Call | Why |
|---|---|---|
A brand-new independent object |
| Scoreboard entries and coverage snapshots |
Overwrite an object already owned |
| Reuse without reallocation, layered per hierarchy |
Copy through a base handle | Virtual copy and clone (next topic) | Derived fields survive instead of slicing |
A scoreboard keeps a list of the packets it expects (see Words You Will Meet). If the code that makes packets reuses one object and the list stores its handle, every saved entry is the same object, so all entries turn into the latest packet.
The fix clones at the boundary. The testbench reuses one live object across 8 transactions while the scoreboard keeps clones, then drains the queue.
class sb_pkt;
int unsigned addr;
int unsigned data;
function sb_pkt clone();
sb_pkt c = new();
c.addr = this.addr;
c.data = this.data;
return c;
endfunction
endclass
PASS: 8 cloned scoreboard entries stay stable and drain to zeroCollection prevents dangling handles but not leaks. A list that is only ever pushed keeps every object reachable, so memory climbs with the number of packets on long runs.
// snippet
class expected_list;
pkt_c seen_q[$];
function void add(pkt_c t);
seen_q.push_back(t); // LEAK: pushed, matched, never popped
endfunction
endclassThe hygiene rules are: pop entries once they are matched, and drop long-lived debug handles after use. A mailbox nobody reads leaks the same way; the mailbox lesson shows how bounding it makes a dead reader visible.
Scalars are independent. The header handle is shared, so header writes through p2 reach p1.
The latest transaction repeated. Clone at the boundary so each entry is an independent snapshot.
One way to write it. Assigning a queue copies its elements, so c.payload = payload gives the clone its own payload.
typedef enum bit [1:0] {DATA, CTRL, ERR} kind_e;
class pkt_txn;
kind_e kind;
bit [1:0] dest;
bit [3:0] len;
bit [7:0] payload[$];
// A fresh object with the same packet. Assigning a queue copies its elements.
function pkt_txn clone();
pkt_txn c = new();
c.kind = kind;
c.dest = dest;
c.len = len;
c.payload = payload;
return c;
endfunction
endclass
module tb_pkt_clone;
int errors = 0;
initial begin
pkt_txn live;
pkt_txn shared_q[$]; // stores handles to the live object
pkt_txn expect_q[$]; // stores clones
live = new();
live.kind = DATA; live.dest = 1; live.len = 1;
for (int i = 0; i < 3; i++) begin
live.payload = '{8'(8'h10 + i)};
shared_q.push_back(live);
expect_q.push_back(live.clone());
end
$display("handles: %h %h %h", shared_q[0].payload[0], shared_q[1].payload[0], shared_q[2].payload[0]);
$display("clones: %h %h %h", expect_q[0].payload[0], expect_q[1].payload[0], expect_q[2].payload[0]);
for (int i = 0; i < 3; i++) begin
if (shared_q[i].payload[0] != 8'h12) errors++; // every handle shows the last packet
if (expect_q[i].payload[0] != 8'(8'h10 + i)) errors++; // every clone kept its own
end
if (errors == 0) $display("PASS: three saved handles all show the last packet; three clones keep 10 11 12");
else $fatal(1, "FAIL: %0d clone checks failed", errors);
$finish;
end
endmodulehandles: 12 12 12
clones: 10 11 12
PASS: three saved handles all show the last packet; three clones keep 10 11 12Run 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.
The copy and clone here are not virtual, so a derived class's extra fields would not be copied; the inheritance topic adds that.