When you assign one class handle to another in SystemVerilog, you are not creating a new object — both handles point to the same memory. To get a truly independent copy, you need to understand the two copy strategies available: shallow copy and deep copy. Choosing the wrong one is a common source of hard-to-find bugs in verification environments.

11 min read | Intermediate Level

What You'll Learn

  • How shallow copy works and why nested objects are not fully duplicated
  • How to implement a deep copy function to get a fully independent object
  • The key difference in behavior between shallow and deep copy

Background: Handles vs Objects

A SystemVerilog class variable is a handle — a reference (pointer) to an object stored in memory. When you write p2 = p1, you are copying the handle, not the object. Both p1 and p2 now point to the same object — modifying one modifies the other.

To create a new, separate object, you must use the new keyword — either with shallow copy syntax or a custom deep copy function.

Handle assignment, shallow copy and deep copy of a Packet that contains a Header object

Shallow Copy

Shallow copy creates a new object and copies all the direct members of the source object into it. You trigger it by passing an existing object handle to the new constructor:


Packet p1, p2;

p1 = new(32'hface_cafe, 32'h1234_5678, 26);  // Create p1
p2 = new p1;                                  // Shallow copy p1 into p2

This works correctly for primitive members like int, bit, and string — those values are copied by value into the new object. However, if the class contains a nested object (a member that is itself a class handle), shallow copy only copies the handle to that inner object, not the inner object itself.

This means both p1 and p2 will share the same nested object in memory. Changing the nested object through p1 will affect p2 too.

Shallow copy duplicates the top-level object but shares any nested objects between the original and the copy.

Shallow Copy Example

The Packet class below has two primitive members (addr, data) and one nested object (hdr of type Header):


// Nested class - represents a packet header
class Header;
    int id;

    function new(int id);
        this.id = id;
    endfunction
endclass

// Top-level class with a nested Header object
class Packet;
    int    addr;
    int    data;
    Header hdr;    // This is a handle to a Header object, not the object itself

    function new(int addr, int data, int id);
        hdr = new(id);       // Create a Header object and store its handle
        this.addr = addr;
        this.data = data;
    endfunction

    function void display(string name);
        $display("[%s] addr=0x%0h  data=0x%0h  id=%0d", name, addr, data, hdr.id);
    endfunction
endclass

module tb;
    Packet p1, p2;

    initial begin
        p1 = new(32'hface_cafe, 32'h1234_5678, 26);
        p1.display("p1");

        // Shallow copy: p2 gets its own addr and data, but shares p1's Header object
        p2 = new p1;
        p2.display("p2");

        // Modify p1's primitive members AND its nested hdr.id
        p1.addr    = 32'habcd_ef12;
        p1.data    = 32'h5a5a_5a5a;
        p1.hdr.id  = 17;        // This modifies the SHARED Header object

        p1.display("p1");

        // p2.addr and p2.data are unaffected (they were copied by value)
        // but p2.hdr.id HAS changed - p2 shares the same Header object as p1
        p2.display("p2");
    end
endmodule

Output

Output
[p1] addr=0xfacecafe  data=0x12345678  id=26
[p2] addr=0xfacecafe  data=0x12345678  id=26

[p1] addr=0xabcdef12  data=0x5a5a5a5a  id=17
[p2] addr=0xfacecafe  data=0x12345678  id=17

Look closely at the final line: p2.addr and p2.data kept their original values — those primitives were copied by value. But p2.hdr.id changed to 17, even though only p1.hdr.id was modified. This is because p2.hdr and p1.hdr point to the same Header object in memory.

Quick Check

Q: After a shallow copy, if you call p2.hdr = new(99) (creating a brand-new Header for p2), would that affect p1.hdr.id?

Show Answer

No. p2.hdr = new(99) creates a new Header object and points p2.hdr at it. p1.hdr still points to the original Header with its original id. The two handles are now fully independent. The problem occurs only when you modify fields of the shared object rather than replacing the handle itself.

Deep Copy

A deep copy creates a fully independent duplicate — including all nested objects. SystemVerilog has no built-in deep copy mechanism, so you implement it yourself as a custom copy() function inside the class.

The key difference from shallow copy: instead of just copying the nested handle, you copy the values inside the nested object, field by field.


class Packet;
    int    addr;
    int    data;
    Header hdr;

    function new(int addr, int data, int id);
        hdr = new(id);
        this.addr = addr;
        this.data = data;
    endfunction

    // Deep copy: copy primitive fields AND nested object fields individually
    function void copy(Packet p);
        this.addr   = p.addr;
        this.data   = p.data;
        this.hdr.id = p.hdr.id;   // Copy the value, not the handle
    endfunction

    function void display(string name);
        $display("[%s] addr=0x%0h  data=0x%0h  id=%0d", name, addr, data, hdr.id);
    endfunction
endclass

module tb;
    Packet p1, p2;

    initial begin
        p1 = new(32'hface_cafe, 32'h1234_5678, 26);
        p1.display("p1");

        // Deep copy: p2 must already exist before calling copy()
        p2 = new(1, 2, 3);    // Create p2 with placeholder values
        p2.copy(p1);           // Deep copy p1's contents into p2
        p2.display("p2");

        // Modify p1 including the nested hdr.id
        p1.addr   = 32'habcd_ef12;
        p1.data   = 32'h5a5a_5a5a;
        p1.hdr.id = 17;
        p1.display("p1");

        // p2 is now fully independent - none of its fields change
        p2.display("p2");
    end
endmodule

Output

Output
[p1] addr=0xfacecafe  data=0x12345678  id=26
[p2] addr=0xfacecafe  data=0x12345678  id=26

[p1] addr=0xabcdef12  data=0x5a5a5a5a  id=17
[p2] addr=0xfacecafe  data=0x12345678  id=26

This time p2.hdr.id stays at 26 after p1.hdr.id is updated to 17. The two Header objects are independent — copy() transferred the value of id rather than the handle to the same object.

Before calling copy(), p2 must already be a valid, constructed object. The copy() function writes into the existing p2.hdr object — if p2.hdr is still null, it will cause a null pointer error at runtime.

Shallow vs Deep Copy — Side-by-Side

PropertyShallow Copy (new p1)Deep Copy (custom copy())
Primitive fields (int, bit)Copied independentlyCopied independently
Nested object fieldsHandle shared - same objectValues copied - separate object
Syntaxp2 = new p1p2 = new(...); p2.copy(p1)
Built-in supportYesNo - must implement manually
Modification isolationPartial (primitives only)Full

Where Is Object Copying Used?

In verification environments, transactions are often generated by a sequencer and passed to multiple components (scoreboard, coverage collector, driver). Each component needs its own independent copy of the transaction so that modifications in one component do not corrupt the data seen by others. Deep copy functions are standard practice in UVM transaction classes for exactly this reason.

Common Beginner Mistakes

Mistake #1: Expecting shallow copy to fully isolate nested objects

Writing p2 = new p1 and then modifying p1.hdr.id will also change p2.hdr.id, because both handles point to the same Header object. Why? Shallow copy only duplicates the top-level object — use a deep copy function whenever nested objects must be independent.

Mistake #2: Calling copy() before constructing the destination object

The copy() function writes into an existing object's nested members. Why? Writing p2.copy(p1) without first creating p2 (and its internal hdr) causes a null handle dereference error at runtime — always construct the destination first: p2 = new(...); p2.copy(p1);

Mistake #3: Using handle assignment instead of copy

Writing p2 = p1 does not create a new object — it makes p2 an alias for the same object. Why? Any change through either handle modifies the single underlying object — if independent copies are actually needed, this plain assignment is never the right tool.

Practice Exercise

Challenge: Add Deep Copy for a Two-Level Nested Class

Extend the example so that Header itself contains a nested object — a SubHeader class with a field priority_level. Update the copy() function in Packet to perform a complete deep copy of all three levels.

Hint

Your copy() function needs to reach two levels deep: this.hdr.sub.priority_level = p.hdr.sub.priority_level. Alternatively, add a copy() method to Header as well and call it from Packet.copy().

Solution

class SubHeader;
    int priority_level;
    function new(int p); this.priority_level = p; endfunction
endclass

class Header;
    int       id;
    SubHeader sub;

    function new(int id, int pri);
        this.id = id;
        sub = new(pri);
    endfunction

    // Copy all fields including the nested SubHeader
    function void copy(Header h);
        this.id                 = h.id;
        this.sub.priority_level = h.sub.priority_level;
    endfunction
endclass

class Packet;
    int    addr;
    int    data;
    Header hdr;

    function new(int addr, int data, int id, int pri);
        hdr = new(id, pri);
        this.addr = addr;
        this.data = data;
    endfunction

    function void copy(Packet p);
        this.addr = p.addr;
        this.data = p.data;
        this.hdr.copy(p.hdr);   // Delegate nested copy to Header's copy()
    endfunction

    function void display(string name);
        $display("[%s] addr=0x%0h  data=0x%0h  id=%0d  pri=%0d",
                 name, addr, data, hdr.id, hdr.sub.priority_level);
    endfunction
endclass

Delegating this.hdr.copy(p.hdr) to Header's own copy() method — rather than reaching directly into hdr.sub.priority_level from within Packet — keeps each class responsible for copying its own fields, and cascades cleanly no matter how many levels deep the nesting goes.

Summary

In this tutorial, you learned:

  • Shallow copy (p2 = new p1) copies primitive fields by value but shares nested object handles between the original and the copy
  • Modifications to a nested object through the original will be visible through the shallow copy, because both handles point to the same memory
  • Deep copy requires a custom copy() function that explicitly copies the fields of each nested object rather than the handle
  • The destination object must be fully constructed before calling copy() to avoid null handle errors
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5