At times it's much easier to write a generic class that can be instantiated in multiple ways to achieve different array sizes or data types. This avoids rewriting nearly-identical code for specific features like size or type, letting a single specification serve many different objects instead — achieved by extending SystemVerilog's parameter mechanism to classes.
Parameters are like constants local to the specified class. Classes are allowed a default value for each parameter, which can be overridden at instantiation.
9 min read | Intermediate Level
What You'll Learn
- Why parameterizing a class avoids rewriting nearly-identical code for different sizes or types
- How to override a class parameter's value, or its entire data type, at instantiation
- How the same parameter mechanism controls a value's size in one case, and a completely different data type in another
- Why assigning a value into a narrower parameterized type silently truncates it, rather than raising an error
Syntax
// Declare parameterized class
class #();
class Trans #(addr = 32);
// Override class parameter
#() ;
Trans #(.addr(16)) obj;
Parameterizing a Value (Size)
Here, size is the parameter, defaulting to 8, and controls how wide the out property is:
// A class is parameterized by #()
// Here, we define a parameter called "size" and gives it
// a default value of 8. The "size" parameter is used to
// define the size of the "out" variable
class something #(int size = 8);
bit [size-1:0] out;
endclass
module tb;
// Override default value of 8 with the given values in #()
something #(16) sth1; // pass 16 as "size" to this class object
something #(.size (8)) sth2; // pass 8 as "size" to this class object
typedef something #(4) td_nibble; // create an alias for a class with "size" = 4 as "nibble"
td_nibble nibble;
initial begin
// 1. Instantiate class objects
sth1 = new;
sth2 = new;
nibble = new;
// 2. Print size of "out" variable. $bits() system task will return
// the number of bits in a given variable
$display ("sth1.out = %0d bits", $bits(sth1.out));
$display ("sth2.out = %0d bits", $bits(sth2.out));
$display ("nibble.out = %0d bits", $bits(nibble.out));
end
endmodule
Output
ncsim> run sth1.out = 16 bits sth2.out = 8 bits nibble.out = 4 bits ncsim: *W,RNQUIE: Simulation is complete.
Note that sth2 explicitly restates the default value (8), showing the two equivalent ways of overriding a parameter: positional (#(16)) or named (#(.size(8))). td_nibble shows a third pattern — using typedef to give a specific parameterization of the class its own convenient name.
Quick Check
Q: If a fourth object were declared as plain something sth3;, with no #() at all, how many bits would sth3.out have?
Show Answer
8 bits — the parameter's own declared default value, exactly like leaving any other default argument unspecified.
Parameterizing a Data Type
Instead of parameterizing a specific value, a class can parameterize an entire data type, using the type keyword:
// "T" is a parameter that is set to have a default value of "int"
// Hence "items" will be "int" by default
class stack #(type T = int);
T item;
function T add_a (T a);
return item + a;
endfunction
endclass
module tb;
stack st; // st.item is by default of int type
stack #(bit[3:0]) bs; // bs.item will become a 4-bit vector
stack #(real) rs; // rs.item will become a real number
initial begin
st = new;
bs = new;
rs = new;
// Assign different values, and add 10 to these values
// Then print the result - Note the different values printed
// that are affected by change in data type
st.item = -456;
$display ("st.item = %0d", st.add_a (10));
bs.item = 8'hA1;
$display ("bs.item = %0d", bs.add_a (10));
rs.item = 3.14;
$display ("rs.item = %0.2f", rs.add_a (10));
end
endmodule
Any type can be supplied as a parameter here, including a user-defined type such as a class or struct.
Output
ncsim> run st.item = -446 bs.item = 11 rs.item = 13.14 ncsim: *W,RNQUIE: Simulation is complete.
Each line here actually prints the return value of add_a(10), not the object's item property directly — the label just happens to say "item" for readability. bs is the interesting one to trace through: T is bit[3:0] (4 bits, range 0–15), so assigning 8'hA1 (161 decimal) into bs.item truncates it down to just the lower 4 bits — 0x1 — before anything else happens. add_a(10) then computes 1 + 10 = 11, which fits comfortably within 4 bits, matching the printed 11.
Quick Check
Q: If bs.item were assigned 8'hAF instead of 8'hA1, what would bs.add_a(10) print?
Show Answer
9. 8'hAF truncates to the lower 4 bits, 1111 (15 decimal). Adding 10 gives 25, which no longer fits in 4 bits — since add_a()'s return type is also T (4 bits), the result is truncated the same way, keeping only the lower 4 bits of 25 (11001 binary): 1001, or 9 decimal.
Where Are Parameterized Classes Used?
UVM's own sequence and sequencer base classes are heavily parameterized — uvm_sequencer#(REQ, RSP), for instance — letting the exact same infrastructure work across completely different transaction types from one project to the next, without any of that shared code needing to know in advance what type it will eventually be used with.
Common Beginner Mistakes
Mistake #1: Assuming a value that doesn't fit raises an error
As the bs.item example shows, assigning a wider value into a narrower parameterized type silently truncates it instead of raising an error or warning. Why? This can produce results that look correct at a glance but are actually based on a truncated value — always double-check the parameterized width actually fits the values you intend to use.
Mistake #2: Misreading what a $display statement is actually printing
As the note above points out, $display("bs.item = %0d", bs.add_a(10)) prints the return value of a method call, not the object's current item property. Why? A display label doesn't have to match what's actually being evaluated — always check the actual expression being printed, not just its string label.
Mistake #3: Assuming an un-parameterized instance behaves unpredictably
Declaring an object with no #() at all, like plain something sth3;, simply uses the parameter's own declared default value. Why? This works exactly like any other default function argument — there's nothing special or unpredictable about omitting the override.
Mistake #4: Confusing a value parameter with a type parameter
Both use the same #(...) syntax at instantiation, but they override fundamentally different things — one overrides a numeric value (like size), the other overrides an entire data type (like T). Why? Mixing them up — like trying to pass a type where a plain value parameter is expected — produces a compile error, since the class declaration itself fixes which kind of parameter each one is.
Practice Exercise
Challenge: A Parameterized Fixed-Size Buffer
Write a class Buffer #(type T = int, int depth = 4) with a property data declared as an array of depth elements of type T (T data[depth];). Instantiate it twice: once with the defaults, and once overriding both T to byte and depth to 2. Print $bits(data) for each.
Hint
A class can declare more than one parameter, separated by commas, mixing value parameters and type parameters freely — override either, both, or neither at instantiation.
Solution
class Buffer #(type T = int, int depth = 4);
T data [depth];
endclass
module tb;
Buffer b1; // defaults: int, depth=4
Buffer #(byte, 2) b2; // byte, depth=2
initial begin
b1 = new;
b2 = new;
$display ("b1 data = %0d bits", $bits(b1.data));
$display ("b2 data = %0d bits", $bits(b2.data));
end
endmodule
b1 uses the defaults: 4 elements of int (32 bits each) = 128 bits total. b2 overrides both parameters: 2 elements of byte (8 bits each) = 16 bits total — both parameters, value and type, are overridden together in a single #(...) list.
Summary
In this tutorial, you learned:
- A parameterized class avoids rewriting nearly-identical code for different sizes or types, using the same
#()syntax already familiar from parameterized modules - A class parameter can be overridden positionally or by name, and defaults to whatever value the class declares if left unspecified
- A parameter can control a plain value (like a bit-vector width) or an entire data type, using the
typekeyword - Assigning a value that doesn't fit into a parameterized width truncates silently, rather than raising an error
typedefcan give a specific parameterization of a class its own convenient name for repeated use