Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
    • Classes, Objects & Handles
      • Class Declaration & Objects
      • Handles vs Objects
      • Constructors & super.new()
      • Shallow vs Deep Copy
      • this, Scope & Nested Classes
    • Inheritance and Polymorphism
    • Static, Encapsulation & Parameterized Classes
    • Virtual Interfaces: the Class-to-Pins Bridge
    • Events, Semaphores, Mailboxes and Processes
    • OOP Patterns for Testbenches
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
Loading lesson

‹Classes, Objects & Handles
  1. Topics
  2. SystemVerilog
  3. Object-Oriented SystemVerilog for Verification
  4. Classes, Objects & Handles
  5. Shallow vs Deep Copy

Part 2 · OOP for Verification · Intermediate

Shallow vs Deep Copy

Shallow copy shares sub-objects, deep copy duplicates them, and scoreboards snapshot.

On this page

    Why this matters

    All eight stored entries show the last packet. A copy shared the packet's sub-objects, or no copy was made at all, so saving a packet saved a pointer to something that kept changing.

    After this lesson you can: tell what new src copies, write a null-safe copy and clone, and store snapshots that cannot change after they are saved.

    new src copies one level

    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.

    Predict first p1 has a header object with id 7. After 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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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
    endmodule
    Terminal
    PASS: shallow copy duplicates scalars and shares the nested header
    A shallow copy duplicates the packet's own fields and shares everything it points to.

    Deep copy is hand-written

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

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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
    
    Terminal
    PASS: clone and null-safe copy give independent headers

    A 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

    clone()

    Scoreboard entries and coverage snapshots

    Overwrite an object already owned

    copy(rhs)

    Reuse without reallocation, layered per hierarchy

    Copy through a base handle

    Virtual copy and clone (next topic)

    Derived fields survive instead of slicing

    Saved lists store snapshots

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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
    
    Terminal
    PASS: 8 cloned scoreboard entries stay stable and drain to zero

    Leaks are logical, not dangling

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

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // 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
    endclass
    A healthy bench builds and releases objects packet by packet. One list that only grows makes memory climb in a straight line.

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

    Common pitfall

    Trusting new src to duplicate a transaction with sub-objects shares the nested handles. Forgetting that shallow copy skips the constructor leaves IDs and registrations stale.

    Interview

    The copy trio recurs in interviews.

    • What p2 = new p1 does: shallow copy with shared sub-objects and no constructor.

    • How deep copy works: a virtual copy and clone pair recursing into sub-objects.

    • Why the scoreboard saw identical entries: stored handles alias one reused object.

    Check yourself p2 = new p1, and p1 holds a header handle. Which parts of p2 are independent?

    Scalars are independent. The header handle is shared, so header writes through p2 reach p1.

    Check yourself A list stores the handles it is given, while the code that makes packets reuses one object. What do the entries show?

    The latest transaction repeated. Clone at the boundary so each entry is an independent snapshot.

    Project step

    Give pkt_txn a clone() and push a clone into the expected-packet queue instead of the handle. Run the experiment from the last lesson again: the three entries now keep their own payloads.

    One way to write it. Assigning a queue copies its elements, so c.payload = payload gives the clone its own payload.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    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
    endmodule
    Terminal
    handles: 12 12 12
    clones:  10 11 12
    PASS: three saved handles all show the last packet; three clones keep 10 11 12

    Remember

    • p2 = new p1 copies one level: scalars are duplicated, sub-objects are shared, and the constructor does not run.

    • A deep copy is hand-written: copy fills an existing object, clone builds a new one.

    • Clone at the boundary where a packet is saved.

    Rule of thumb: save a clone, never the handle of an object someone else keeps changing.

    How this was checked

    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.

    The copy and clone here are not virtual, so a derived class's extra fields would not be copied; the inheritance topic adds that.

    Related topics

    • Handles vs Objects

    • Inheritance & Polymorphism

    Previous

    38 / 259

    Next: this, Scope & Nested Classes

    Up next

    Up nextthis, Scope & Nested Classes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Class Declaration & Objects
    • Handles vs Objects