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.

diagram
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 bug
systemverilog
class 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.

systemverilog
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
endclass

copy 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.

systemverilog
// 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
endfunction

Interview 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.