A struct and a class both group related data together, but they solve genuinely different problems. A struct is a plain, synthesizable value type for organizing data — nothing more. A class is a reference type built for verification, bundling data together with behavior, inheritance, and dynamic construction. Confusing the two — especially around assignment and equality — is a common source of subtle bugs.
10 min read | Intermediate Level
What You'll Learn
- The core distinction: a struct is a value type, a class is a reference type
- Why assigning one struct to another copies its fields, while assigning one class handle to another just shares the same object
- Why comparing two structs with
==checks their contents, while comparing two class handles checks identity - Which features — methods, constructors, inheritance — exist on a class but not a struct, and why only one of the two is synthesizable
At a Glance
| struct | class | |
|---|---|---|
| Type category | Value type | Reference type (handle) |
Assignment (a = b) | Copies every field | Copies the handle; both point to the same object |
Equality (a == b) | Compares field values | Compares handle identity (same object?) |
| Methods (functions/tasks) | Not supported | Supported |
| Constructor | None — initialized directly | new() |
Inheritance (extends) | Not supported | Supported |
Access control (local/protected) | Not supported | Supported |
| Synthesizable | Yes, especially when packed | No — simulation and testbench only |
| Memory | Exists wherever declared | Allocated dynamically via new(), garbage collected once unreferenced |
Assignment: Copy vs Share
This is the single most important difference, and the one most likely to cause a real bug if missed:
typedef struct {
int addr;
} packet_t;
class Packet;
int addr;
endclass
module tb;
packet_t s1, s2;
Packet c1, c2;
initial begin
s1.addr = 10;
s2 = s1; // struct assignment COPIES every field
s2.addr = 99;
$display ("s1.addr=%0d, s2.addr=%0d", s1.addr, s2.addr);
c1 = new (); c1.addr = 10;
c2 = c1; // class assignment copies only the HANDLE
c2.addr = 99;
$display ("c1.addr=%0d, c2.addr=%0d", c1.addr, c2.addr);
end
endmodule
This prints s1.addr=10, s2.addr=99 for the struct — changing s2 after the assignment has no effect on s1, since s2 got its own independent copy of every field the moment s2 = s1; ran. For the class, it prints c1.addr=99, c2.addr=99 — c2 = c1; only copied the handle, so c1 and c2 point to the exact same object, and changing one changes both. See SystemVerilog Copying Objects for how to actually get an independent copy of a class object when you need one.
Equality: Comparing Values vs Comparing Identity
This follows directly from the same value-vs-reference distinction, and it's the part most likely to catch someone off guard:
typedef struct {
int addr;
int data;
} packet_t;
class Packet;
int addr;
int data;
endclass
module tb;
packet_t s1, s2;
Packet c1, c2;
initial begin
// Two SEPARATE structs, with identical field values
s1.addr = 10; s1.data = 20;
s2.addr = 10; s2.data = 20;
// Two SEPARATE class objects, also with identical property values
c1 = new (); c1.addr = 10; c1.data = 20;
c2 = new (); c2.addr = 10; c2.data = 20;
if (s1 == s2)
$display ("Structs are equal");
else
$display ("Structs are NOT equal");
if (c1 == c2)
$display ("Classes are equal");
else
$display ("Classes are NOT equal");
end
endmodule
This prints Structs are equal, then Classes are NOT equal — even though c1 and c2 hold exactly the same data. == on a struct compares every field's value; == on a class handle only checks whether both handles reference the same object in memory. c1 and c2 are two genuinely separate objects that simply happen to contain matching data — as far as == is concerned, that doesn't count as equal.
Quick Check
Q: If the code were changed to c2 = c1; instead of constructing c2 separately, would c1 == c2 now return true?
Show Answer
Yes. c2 = c1; makes both handles point to the exact same object, so the identity check == performs succeeds — it's not that the data happens to match anymore, it's that there's only one object for both handles to compare against.
Methods, Constructors, and Inheritance
A struct is data only — it can't have a method, a constructor, or a base type:
// Not legal - a struct cannot contain a method
typedef struct {
int addr;
// function void display(); ... endfunction <- not allowed
} packet_t;
A class supports all of this: methods (covered in SystemVerilog Functions), a constructor (see SystemVerilog Class Constructor), and inheritance via extends (see SystemVerilog Inheritance). If you find yourself wanting to bundle behavior together with data — or reuse a data structure across a hierarchy of related types — that's the signal you need a class, not a struct.
Synthesizability
A struct — especially a packed struct with a well-defined bit layout — is fully synthesizable and commonly used to describe register fields or bus transaction layouts directly in RTL. A class is never synthesizable, under any circumstances; it exists purely for simulation and testbench code.
Where Does Each One Fit?
A packed struct is a natural fit for describing a fixed bit-field layout that both the design and the testbench need to agree on — a bus transaction's raw header format, for instance. A class fits a rich testbench transaction object that needs randomization, methods, and reuse across a family of related transaction types through inheritance — something a struct simply has no mechanism for.
Common Beginner Mistakes
Mistake #1: Assuming struct assignment shares data the way class assignment does
As the first example shows, these behave oppositely. Why? A struct is a value type, so assignment always copies every field independently; a class handle is a reference, so assignment only copies the reference itself — expecting one to behave like the other is a common source of confusing, hard-to-trace bugs.
Mistake #2: Assuming class equality checks field contents
As the equality example shows, == on two class handles checks identity, not content. Why? Two separately constructed objects with identical data are still considered unequal — if you actually need to compare a class object's contents field by field, you have to write that comparison logic yourself.
Mistake #3: Trying to add a method directly to a struct
A struct can only hold data members — no functions, no tasks. Why? If you need behavior bundled together with data, that's exactly what a class is for; a struct simply has no mechanism for it.
Mistake #4: Assuming a class can be used anywhere a struct can
A class is never synthesizable, regardless of what it contains. Why? If the same data structure needs to exist in both the design and the testbench — or in synthesizable RTL at all — it has to be a struct (typically packed), not a class.
Practice Exercise
Challenge: Predict Three Comparisons
Given two structs with identical field values, two separately-constructed class objects with identical property values, and a third class handle assigned directly from one of those two objects (c3 = c1;), predict the result of: s1 == s2, c1 == c2, and c1 == c3.
Hint
Only one of these three comparisons depends on whether the two sides are the exact same object in memory — the other two questions have already been answered directly in this article.
Solution
s1 == s2 is true — struct equality compares field values, and they match. c1 == c2 is false — two separate objects, even with matching data, aren't the same object. c1 == c3 is true — c3 = c1; made c3 point to the exact same object as c1, so the identity check succeeds.
Summary
In this tutorial, you learned:
- A struct is a value type; a class is a reference type accessed through a handle
- Assigning one struct to another copies every field independently; assigning one class handle to another only shares the same underlying object
==on structs compares field values;==on class handles compares identity — whether both sides reference the same object, not whether their data matches- Only a class supports methods, a constructor, and inheritance; a struct is data only
- A struct (especially
packed) is synthesizable; a class is never synthesizable, and exists purely for simulation and testbench code