Part 2 · OOP for Verification · Intermediate
Shallow vs Deep Copy
new src shallow-copy semantics, the shared sub-object bug, writing copy()/clone(), and why scoreboards need deep copies.
Shallow copy: what 'new src' actually does
SystemVerilog has one built-in copy operation: dst = new src;. It allocates a fresh object and copies every property of src into it bit-for-bit, one level deep . Scalar fields (ints, bit vectors, strings, enums) get independent copies. But any property that is itself a class handle is copied as a handle — both objects end up pointing at the same sub-object. The constructor is NOT called during a shallow copy, so any bookkeeping new() does (ID assignment, registration) is skipped.
SHALLOW COPY: t2 = new t1;
BEFORE AFTER
t1 ──► ┌────────────┐ t1 ──► ┌────────────┐
│ addr=1000 │ │ addr=1000 │
│ hdr ───────┼──►┌──────┐ │ hdr ───────┼──►┌──────┐
└────────────┘ │ id=7 │ └────────────┘ │ id=7 │◄─┐
└──────┘ └──────┘ │
t2 ──► ┌────────────┐ │
│ addr=1000 │ │
│ hdr ───────┼──────┘
└────────────┘
Scalars duplicated. Sub-object SHARED.
t2.hdr.id = 9 → t1.hdr.id is ALSO 9. ← the shallow-copy bugclass header;
rand bit [7:0] id;
endclass
class packet;
rand bit [31:0] addr;
header hdr;
function new(); hdr = new(); endfunction
endclass
packet p1 = new();
p1.addr = 32'h1000;
p1.hdr.id = 7;
packet p2 = new p1; // shallow copy — constructor NOT run
p2.addr = 32'h2000; // independent: p1.addr still 1000
p2.hdr.id = 9; // SHARED: p1.hdr.id is now 9 too!Writing copy() and clone() for deep copies
A deep copy duplicates the whole object graph: every class-handle property gets its own freshly copied sub-object. SystemVerilog gives you nothing automatic here — you write it. The standard testbench idiom is a pair of virtual methods: copy(rhs) copies fields into an existing object, and clone() constructs a new object of the correct runtime type and then calls copy. Making them virtual matters: a monitor holding a base handle to a derived transaction must clone the derived object, not slice it down to the base.
class packet;
rand bit [31:0] addr;
header hdr;
function new(); hdr = new(); endfunction
virtual function void copy(packet rhs);
this.addr = rhs.addr;
if (this.hdr == null) this.hdr = new();
this.hdr.id = rhs.hdr.id; // copy CONTENTS, not the handle
endfunction
virtual function packet clone();
packet c = new();
c.copy(this);
return c;
endfunction
endclass
class tagged_packet extends packet;
rand bit [3:0] tag;
virtual function void copy(packet rhs);
tagged_packet trhs;
super.copy(rhs); // base fields + sub-objects
if ($cast(trhs, rhs)) this.tag = trhs.tag;
endfunction
virtual function packet clone();
tagged_packet c = new(); // correct runtime type
c.copy(this);
return c;
endfunction
endclasscopy vs clone — which to call
clone() — when you need a brand-new independent object: scoreboard expected entries, coverage snapshots.
copy(rhs) — when you already own an object and want its fields overwritten in place.
Both virtual — so base-handle callers duplicate the full derived object (UVM follows the same do_copy/clone split).
When scoreboards need deep copies — the monitor reuse bug
The classic field bug: a monitor builds one transaction object, publishes its handle to the scoreboard, then reuses the same object for the next bus transfer. The scoreboard stored a handle, not a snapshot — so every entry in its expected queue silently mutates into the latest transaction. Comparisons then fail (or worse, falsely pass). The fix is to clone at the publishing boundary, or construct a fresh object per observed transaction.
// BUG: one object reused — every queue entry aliases it
task monitor::run();
bus_txn t = new();
forever begin
collect(t); // overwrite fields of the SAME object
ap_to_sb(t); // scoreboard queue now holds N copies
end // of ONE handle → all entries identical
endtask
// FIX 1: fresh object per transaction
task monitor::run();
forever begin
bus_txn t = new();
collect(t);
ap_to_sb(t);
end
endtask
// FIX 2: scoreboard snapshots defensively
function void scoreboard::write_exp(bus_txn t);
exp_q.push_back(t.clone()); // own an independent deep copy
endfunctionInterview angle
'What does p2 = new p1 do?' — shallow copy: new object, scalar fields duplicated, sub-object handles shared, constructor skipped.
'How do you implement deep copy?' — virtual copy()/clone() pair; each level copies its own fields and recurses into sub-objects.
'Why did the scoreboard see identical entries?' — handle stored, object reused; the answer they want is clone-at-boundary.
Key takeaways
new src copies one level: scalars duplicated, nested class handles shared, constructor not called.
Deep copy is hand-written: virtual copy()/clone() that recurse into sub-objects.
Clone at ownership boundaries — anything stored for later comparison must be an independent snapshot.
Make copy/clone virtual so base-handle callers duplicate the full derived object.
Common pitfalls
Trusting new src to fully duplicate a transaction that contains sub-objects — shared-header mutation bugs.
Forgetting that shallow copy skips the constructor — IDs, registrations, and allocated sub-objects are not refreshed.
Storing monitor handles directly in scoreboard queues — entries mutate when the monitor reuses its object.
Non-virtual clone() — cloning through a base handle slices off derived fields.