SystemVerilog · Module 9
Handles — Shallow Copy, Deep Copy, Comparison
The handle-is-a-pointer mental model, the shallow-vs-deep copy trap, UVM-style copy()/clone()/compare(), and the scoreboard aliasing bugs that haunt every new verification engineer.
Module 9 · Page 9.14
First Understand: Class Variables Are Handles
Before talking about copy, you must be clear about one thing: a class variable in SystemVerilog is not an object. It is a handle — a pointer, a reference, an address. The object only exists after you call new().
class Packet;
int data;
endclass
Packet p; // p is a HANDLE — value is null, NO object yet
p = new(); // NOW an object exists in memory, p holds its addressWith that clear, there are three distinct copy operations in SystemVerilog. Each has different syntax and different behaviour:
Handle Assignment(Reference Copy)b2 = b1;
No new object. Both handles point to the same memory. Change one — the other changes too.
Shallow Copy(new object, shared nested)b2 = new b1;
New object created. Primitive fields are independent. But nested class handles are still shared.
Deep Copy(fully independent)b2 = b1.copy();
New object. Every field independent — including nested objects. Must write copy() yourself.
Handle Assignment (Reference Copy)
b2 = b1 simply copies the handle value — the memory address — from b1 to b2. No new object is created. Both variables now point to the exact same object. Change anything through b2 and you will see it through b1 too, because there is only one object.
Handle Assignment — b2 = b1b1→ data = 10 addr = 15 mem[]: {3,4,5,7,...} a → [Object A: i=5] ←b2 Both b1 and b2 point to the SAME object. Modifying b2.data changes b1.data too. Modifying b2.a.i changes b1.a.i too.
class A;
int i;
endclass
class bustran;
logic [31:0] data;
logic [3:0] addr;
int mem [15:0];
A a = new();
function new(int a, b, c[15:0]);
this.data = a;
this.addr = b;
foreach(mem[i]) mem[i] = c[i];
endfunction
endclass
module m;
bustran b1, b2;
initial begin
b1 = new(10, 15, '{3,4,5,7,6,2,3,45,8,54,23,23,43,45,65,75});
b1.a.i = 5;
b2 = b1; // b1 and b2 both point to the SAME memory location
$display("b2 = %p, b2.a.i = %0d", b2, b2.a.i);
// b2 = '{data:'ha, addr:'hf, mem:'{3,4,...}}, b2.a.i = 5
b2.data = 20;
b2.addr = 35;
b2.a.i = 10; // modifies the shared nested object
$display("b1 = %p, b1.a.i = %0d", b1, b1.a.i);
// b1 = '{data:'h14, addr:'h3, ...}, b1.a.i = 10
// b1 changed! Same object as b2.
end
endmoduleShallow Copy (new b1)
b2 = new b1 creates a new object and copies all field values from b1 into it. Primitive fields (data, addr, array values) are independent — changing them in b2 does not affect b1.
But — nested class handles are copied by reference. b2.a points to the same A object as b1.a. Changing b2.a.i still changes b1.a.i. This is the key difference from a deep copy.
Shallow Copy — b2 = new b1b1→ data = 10 (own) addr = 15 (own) mem[]: own copya → [Object A: shared]b2→ data = 10 (own) addr = 15 (own) mem[]: own copya → [Object A: shared] Primitives are independent. But both b1.a and b2.a still point to the same Object A.
class A;
int i;
endclass
class bustran;
logic [31:0] data;
logic [3:0] addr;
int mem [3:0];
A a = new();
function new(int a, b, c[3:0]);
this.data = a;
this.addr = b;
foreach(mem[i]) mem[i] = c[i];
endfunction
endclass
module m;
bustran b1, b2;
initial begin
b1 = new(10, 15, '{12, 15, 25, 35});
b1.a.i = 5;
b2 = new b1; // SHALLOW COPY — new object, but nested handle shared
$display("b2 = %p, b2.a.i = %0d", b2, b2.a.i);
// b2 is a NEW object — b2.data, b2.addr are independent of b1
// BUT b2.a points to the same A object as b1.a
b2.data = 20; // independent — b1.data unchanged
b2.addr = 35; // independent — b1.addr unchanged
b2.a.i = 10; // SHARED — b1.a.i also becomes 10!
$display("b1 = %p, b1.a.i = %0d", b1, b1.a.i);
// b1.data unchanged, b1.addr unchanged
// BUT b1.a.i = 10 — shared nested object was modified
end
endmoduleDeep Copy (Fully Independent Duplicate)
A deep copy creates a completely independent duplicate — a new object for the outer class and new objects for every nested class handle inside it. Changing anything in the copy has absolutely no effect on the original.
SystemVerilog does not provide automatic deep copy. You write a copy() method yourself. Every nested class must also have its own copy() method that you call recursively.
Deep Copy — b2 = b1.copy()b1→ data = 10 (own) addr = 15 (own) mem[]: own copy a → [Object A #1: i=5] b2→ data = 10 (own) addr = 15 (own) mem[]: own copy a → [Object A #2: i=5] ← new object Completely independent. Two separate Object A instances. Changing b2.a.i does NOT affect b1.a.i.
class A;
int i;
function A copy();
copy = new();
copy.i = this.i; // deep copy the nested object
endfunction
endclass
class bustran;
logic [31:0] data;
logic [3:0] addr;
int mem [3:0];
A a = new();
function new(int a, b, c[3:0]);
this.data = a;
this.addr = b;
foreach(mem[i]) mem[i] = c[i];
endfunction
function bustran copy();
copy = new(10, 15, '{12, 15, 25, 35});
copy.a = this.a.copy(); // deep copy the nested A object
endfunction
endclass
module m;
bustran b1, b2;
initial begin
b1 = new(10, 15, '{12, 15, 25, 35});
b1.a.i = 5;
b2 = b1.copy(); // DEEP COPY — everything independent
$display("b2 = %p, b2.a.i = %0d", b2, b2.a.i);
b2.data = 20; // independent — b1.data unchanged
b2.addr = 35; // independent — b1.addr unchanged
b2.a.i = 10; // independent — b1.a.i STILL 5
$display("b1 = %p, b1.a.i = %0d", b1, b1.a.i);
// b1.a.i = 5 — unchanged, b2.a was a new object
end
endmoduleCopying Dynamic Arrays
Dynamic arrays are not class handles — they behave differently. Assigning a dynamic array with tmp.payload = this.payload copies the elements, not just a reference. The two arrays are independent after the assignment.
class Packet;
rand byte payload[];
function new(int size = 4);
payload = new[size];
endfunction
function Packet copy_payload();
Packet tmp = new(payload.size());
tmp.payload = this.payload; // dynamic array assignment copies elements
return tmp;
endfunction
function void display(string tag = "PKT");
$write("[%s] payload = ", tag);
foreach(payload[i]) $write("%0h ", payload[i]);
$write("\n");
endfunction
endclass
module tb;
initial begin
Packet p1 = new(4);
Packet p2;
p1.payload = '{8'hAA, 8'hBB, 8'hCC, 8'hDD};
p2 = p1.copy_payload();
p2.payload[0] = 8'h11; // only p2 changes
p1.display("p1"); // [p1] payload = aa bb cc dd
p2.display("p2"); // [p2] payload = 11 bb cc dd
end
endmoduleInterview Example: Wrong vs Right copy()
A very common interview question — spot the bug in copy_wrong() and explain why copy_right() is correct.
class Header;
int id;
endclass
class Packet;
int addr;
Header h;
function new();
h = new();
endfunction
// ── WRONG — nested handle is shallow-copied ───────────────
function Packet copy_wrong();
Packet tmp = new();
tmp.addr = addr;
tmp.h = h; // BUG: copies the handle — tmp.h and this.h
// point to the SAME Header object
return tmp;
endfunction
// ── CORRECT — nested object is independently copied ───────
function Packet copy_right();
Packet tmp = new();
tmp.addr = addr;
tmp.h.id = h.id; // copy the VALUE into the existing new Header
// tmp already has its own h from new() in constructor
return tmp;
endfunction
endclassBest Practice: copy() and clone() Style
UVM distinguishes two operations with different names:
- copy() — copies data into an existing object. Caller provides the destination.
- clone() — creates and returns a new object. No existing destination needed.
class Packet;
int addr;
int data;
// copy() — copies rhs values INTO this object
function void copy(Packet rhs);
this.addr = rhs.addr;
this.data = rhs.data;
endfunction
// clone() — creates a brand new independent object
function Packet clone();
Packet tmp = new();
tmp.copy(this); // reuse copy() logic
return tmp;
endfunction
endclass
module tb;
initial begin
Packet p1 = new();
p1.addr = 32'h4000_0000;
p1.data = 32'hA5A5_A5A5;
Packet p2 = p1.clone(); // new independent object
p2.data = 32'h0;
$display("p1.data = 0x%08h", p1.data); // 0xa5a5a5a5 — unchanged
$display("p2.data = 0x%08h", p2.data); // 0x00000000
end
endmoduleCopy with Inheritance
When a class inherits from another, the child's copy() must call super.copy() to also copy the parent's fields. If you forget, the base fields are never copied and your clone is incomplete.
class BasePkt;
int addr;
function void copy(BasePkt rhs);
this.addr = rhs.addr;
endfunction
endclass
class ExtPkt extends BasePkt;
int data;
function void copy(ExtPkt rhs);
super.copy(rhs); // copies base class fields (addr)
this.data = rhs.data; // copies child-specific fields
endfunction
function ExtPkt clone();
ExtPkt tmp = new();
tmp.copy(this);
return tmp;
endfunction
endclass
module tb;
initial begin
ExtPkt p1 = new();
p1.addr = 32'h5000_0000;
p1.data = 32'h1234_5678;
ExtPkt p2 = p1.clone();
$display("p2.addr = 0x%08h", p2.addr); // 0x50000000 — copied by super
$display("p2.data = 0x%08h", p2.data); // 0x12345678
end
endmoduleQuick Reference — All Three Copy Types
| Type | Syntax | New object? | Primitives independent? | Nested handles independent? |
|---|---|---|---|---|
| Handle Assignment | b2 = b1 | ❌ No | ❌ No — same object | ❌ No — same object |
| Shallow Copy | b2 = new b1 | ✅ Yes | ✅ Yes — new values | ❌ No — handles shared |
| Deep Copy | b2 = b1.copy() | ✅ Yes | ✅ Yes | ✅ Yes — new nested objects |
| Pattern | Purpose | Key Rule |
|---|---|---|
copy(rhs) | Copy data into an existing object | Destination object must already exist |
clone() | Create and return a new independent object | Internally calls new() then copy(this) |
super.copy(rhs) | Copy base class fields in a child's copy() | Always call this first before copying child fields |
| Dynamic array assignment | tmp.arr = this.arr | Copies elements — arrays are independent after assignment |
Verification Usage — Where Copy/Clone/Compare Decide Whether Your Testbench Works
Copy, clone, and compare are not OOP curiosities — they sit on the critical path of every UVM scoreboard, every replay framework, every coverage post-processor, and every reference model. Get them right and the rest of the environment is easy; get them wrong and you spend weeks chasing mysterious mismatches.
Scoreboard "expected" queue — needs deep copy in
A scoreboard receives a transaction from the monitor and pushes a copy into its expected queue. If you push the original handle, the next transaction's randomisation will mutate the same object — and your expected queue is suddenly full of pointers to the same overwritten state. Always deep-clone before storing.
<code>function void scoreboard::write(apb_xact obs);
apb_xact exp;
if (!$cast(exp, obs.clone())) // independent copy
`uvm_fatal("SB", "clone failed")
expected.push_back(exp); // safe — outside code can mutate obs freely
endfunction</code>Replay framework — captured transactions need deep clone
Replay frameworks capture observed transactions, log them, and re-issue them later for regression. Every captured transaction must be a deep clone, not a handle to the live transaction the monitor saw — otherwise the recorded log is full of identical pointers to whichever transaction happened last.
Reference model output — needs deep copy out
When a reference model produces a predicted transaction, it should hand the scoreboard a clone — never an internal handle. Returning an internal handle exposes the reference model's private state to mutation by the scoreboard, which usually produces silent prediction drift.
UVM compare() hook — needs structural deep compare
Comparing transactions field-by-field with == only checks handle equality for any nested class field. Two transactions whose nested handles point to different objects with identical contents would incorrectly compare unequal. Every override of do_compare must recursively call nested_field.compare(rhs_nested_field) for class-typed fields.
Simulation Behavior — What the Simulator Does With Handles, Copies, and Compares
Handles are pointer-sized opaque values
Every class handle stores a single pointer-sized value: the address of the object on the simulator's heap, or a special null sentinel. Handle assignment is a one-word copy — fast, no allocation, no side effects. $display("%p", h) can sometimes reveal the underlying address on some simulators, useful for confirming two handles really do point to the same object.
new b1 performs a member-wise shallow copy at elaboration of the expression
The expression new b1 tells the simulator: allocate a fresh object of the same class as b1, then field-by-field copy every member from b1 into the new object. Field-by-field means scalars get a value copy, and handle fields get a pointer copy. The simulator does not recurse into nested objects — that responsibility is yours.
Garbage collection reclaims unreferenced objects
SystemVerilog simulators garbage-collect class objects automatically — once the last handle to an object goes out of scope or is overwritten, the object becomes eligible for reclamation. This is why "shallow copy sharing the same nested object" is dangerous in subtle ways: even if the outer copies disappear, the shared nested object remains as long as any handle points at it, leaking heap until reset.
Comparison with == is handle equality, not deep equality
h1 == h2 returns 1 if and only if both handles point at the same heap object (or both are null). Two distinct objects with identical contents compare unequal. For value-level comparison you must implement (or call) a compare() method that examines each field — recursively for nested handles.
<code>class pkt;
int hdr;
payload p; // nested class
virtual function pkt clone();
pkt c = new();
c.hdr = this.hdr;
c.p = this.p.clone(); // recursive
return c;
endfunction
endclass
pkt a = new(); a.hdr = 1; a.p.data = 7;
pkt b = a.clone();
b.p.data = 99;
$display(a.p.data); // 7 ✓ unchanged</code><code>class pkt;
int hdr;
payload p;
endclass
pkt a = new(); a.hdr = 1; a.p.data = 7;
pkt b = new a; // SHALLOW copy
b.p.data = 99; // mutates shared object
$display(a.p.data); // 99 ✗ — surprise!
// a and b share the
// same 'p' object.</code>Waveform Analysis — Making Copy/Compare Decisions Visible in Debug
Copy, clone, and compare are method-call events — they do not appear as waveform signals. The debugging discipline is to log each call with the transaction IDs and pointers involved, so post-sim forensics can reconstruct who copied what when.
The instrumentation pattern
<code>virtual function pkt clone();
pkt c = new();
c.do_copy(this);
`uvm_info("CLONE", $sformatf("orig=%0d -> new=%0d", this.tid, c.tid), UVM_HIGH)
return c;
endfunction
virtual function bit compare(uvm_object rhs);
bit ok = do_compare(rhs);
`uvm_info("COMPARE", $sformatf("%0d vs %0d -> %s",
this.tid, ((pkt'(rhs))).tid, ok ? "MATCH" : "MISMATCH"), UVM_HIGH)
return ok;
endfunction</code>ASCII log view — a healthy scoreboard run
<code>100ns CLONE orig=1 -> new=10001
100ns COMPARE 10001 vs 1 -> MATCH
150ns CLONE orig=2 -> new=10002
150ns COMPARE 10002 vs 2 -> MATCH
200ns CLONE orig=3 -> new=10003
200ns COMPARE 10003 vs 3 -> MISMATCH ← actual bug visible
↑ ↑
expected (cloned) observed (live monitor)</code>Industry Insights — Hard-Won Lessons About Copy, Clone, and Compare
Debugging Academy — Five Real Bugs From Handle and Copy Misuse
"All my scoreboard expected entries are the same transaction"
DEBUGThe scoreboard reports all expected transactions have the same addr and data — the last one observed. Mismatches start failing as soon as the second transaction arrives.
<code>function void scoreboard::write(apb_xact obs);
expected.push_back(obs); // BUG: stores handle, not a copy
endfunction
// Later: when the monitor randomises a new obs, the *same* object's
// fields change — every entry in 'expected' suddenly mirrors the new
// transaction because they all point at the same object.</code>Storing the handle aliases the live monitor transaction. The next randomisation overwrites the object that every expected-queue entry points to.
Clone before storing: apb_xact exp; if (!$cast(exp, obs.clone())) uvm_fatal(...); expected.push_back(exp);`. Every entry becomes an independent object insensitive to later mutations.
"new b1 doesn't deep-copy my nested payload"
DEBUGAfter b2 = new b1; a developer modifies b2.payload.crc and is surprised that b1.payload.crc changed too.
<code>class packet;
int hdr;
payload p; // nested class handle
endclass
packet b1 = new(); b1.p = new(); b1.p.crc = 1;
packet b2 = new b1; // SHALLOW: b2.p == b1.p (same object)
b2.p.crc = 99;
$display(b1.p.crc); // 99 — surprise!</code>new b1 performs a shallow member-wise copy — scalar fields are duplicated, but handle fields just have their pointer copied. b1.p and b2.p point at the same payload object.
Write an explicit deep copy: function packet clone(); packet c = new(); c.hdr = hdr; c.p = p.clone(); return c; endfunction — and have payload implement its own clone() too, recursively all the way down.
"Regression bloats memory — leaked transactions visible in heap profiler"
DEBUGA long regression OOMs after ~6 hours; the heap profiler shows ~5 million live packet objects, far more than the regression should ever have created.
<code>class log;
static packet history[$]; // unbounded static queue
static function void record(packet p);
history.push_back(p); // BUG: nothing ever pops
endfunction
endclass</code>The static queue holds a live handle to every packet ever created. Because the queue is itself static, the GC cannot reclaim either the queue or any packet it references. Classic handle-driven memory leak.
Either bound the queue (if (history.size() > 1000) history.pop_front();) or record only a compact summary (id, time, type) instead of the full handle. The general principle: never hold an unbounded number of handles in a static container.
"do_compare returns 0 even when transactions look identical"
DEBUGA scoreboard reports mismatch on transactions whose printed contents are byte-for-byte identical.
<code>function bit do_compare(uvm_object rhs);
packet r;
if (!$cast(r, rhs)) return 0;
return (this.hdr == r.hdr) &&
(this.p == r.p); // BUG: handle equality, not value equality
endfunction</code>this.p == r.p tests whether the two payload handles point at the same object — which they don't, because expected and observed transactions came from different sources. The compiler accepts the syntax, but the semantics are wrong.
Recurse into the nested class's compare: return (this.hdr == r.hdr) && this.p.compare(r.p);. For arrays of handles, iterate and call .compare() on each element.
"Deep-copy override silently drops base-class fields"
DEBUGA child write_xact overrides do_copy and the clone has correct child-specific fields, but base-class fields (paddr, psel, txn_id) are all zero.
<code>class apb_write_xact extends apb_base_xact;
bit [31:0] pwdata;
function void do_copy(uvm_object rhs);
apb_write_xact r;
// BUG: missing super.do_copy(rhs)
if (!$cast(r, rhs)) return;
this.pwdata = r.pwdata;
endfunction
endclass</code>Without super.do_copy(rhs), the parent's fields are never copied. The clone has only what the child override touched.
Always start every do_copy with super.do_copy(rhs);. The base class handles its own fields; the child handles its own additions. Same discipline for do_compare, convert2string, and all the UVM hooks.
Interview Q&A — Twelve Questions You Will Be Asked
A class variable in SystemVerilog stores a handle — a pointer-sized value pointing to a heap-allocated object (or the special value null). The variable does not contain the object's data; it contains the object's address. Assigning one handle to another (h2 = h1) copies the pointer, not the underlying object.
Best Practices — Ten Rules for Disciplined Handle and Copy Usage
- Internalise that class variables are pointers, not containers. Every aliasing, copying, and comparison surprise reduces to misreading the pointer semantics.
- Default to deep copy. Implement
clone()that recursively duplicates every nested handle field. Switch to shallow only with explicit performance evidence and a comment justifying the sharing. - Always clone transactions before storing them in scoreboard queues. Storing a handle aliases the source and mutates with it; cloning produces a safe independent copy.
- Implement
copy(rhs),clone(), andcompare(rhs)on every transaction class. The UVM idiom:copyoverwrites self,clonereturns a new object,comparerecursively checks every field. - Call
super.do_copyandsuper.do_compareat the top of every override. Forgetting silently drops every parent-level field from the operation. - Never compare class handles with
==insidedo_compare. That tests pointer equality; the correct form isnested.compare(rhs.nested)recursively. - Never store unbounded numbers of handles in static containers. The handles keep the objects alive past their useful life, producing heap leaks that fail long regressions.
- For dynamic arrays of class handles, deep copy element-by-element. Just copying the array still shares all the underlying objects — shallow at one level of indirection deeper than scalar fields.
- Instrument
clone()andcompare()with$typenameand transaction IDs during bring-up. Distinguishing "clone failed" from "compare failed" early saves hours of downstream debugging. - Assign
nullto handles you genuinely want collected. The GC reclaims when the last reference goes out of scope; nulling out a long-lived variable accelerates reclamation when memory pressure is a concern.