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.
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
[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
[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
| Property | Shallow Copy (new p1) | Deep Copy (custom copy()) |
|---|---|---|
Primitive fields (int, bit) | Copied independently | Copied independently |
| Nested object fields | Handle shared - same object | Values copied - separate object |
| Syntax | p2 = new p1 | p2 = new(...); p2.copy(p1) |
| Built-in support | Yes | No - must implement manually |
| Modification isolation | Partial (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