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

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

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 type keyword
  • Assigning a value that doesn't fit into a parameterized width truncates silently, rather than raising an error
  • typedef can give a specific parameterization of a class its own convenient name for repeated use
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5