A parameterized class is one source definition stamped into concrete classes at each use site. Value parameters set sizes and constants, while type parameters let one container hold any payload type with full checking.
Parameter kind | Example | Use |
|---|---|---|
Value parameter | #(int DEPTH = 8) | Queue depth, width, retry limit |
Type parameter | #(type T = int) | Payload type of a container or scoreboard |
Parameterized classes are not full C++ templates: there are no template functions and no specialization metaprogramming. Likewise there is no signature overloading, so sibling APIs get distinct names such as write_int and write_str.
Fifo of byte and fifo of int share source code and nothing else. Handles never cross-assign, and each specialization owns a separate copy of every static member.
Name each specialization once with a typedef in a shared package. Every use site then agrees by construction, and width changes become one-line edits.
fifo #(byte, 4) and 3 into a fifo #(int, 4). What do byte_fifo_t::put_total and int_fifo_t::put_total read?4 and 3. The two specializations are separate classes, so each has its own static counter.
A generic fifo exercised as two types, with per-specialization put tallies proving the statics are separate.
class fifo #(type T = int, int DEPTH = 8);
local T q[$];
static int put_total = 0;
function bit try_put(T item);
if (q.size() >= DEPTH) return 0;
q.push_back(item);
put_total++;
return 1;
endfunction
function bit try_get(output T item);
if (q.size() == 0) return 0;
item = q.pop_front();
return 1;
endfunction
function int level();
return q.size();
endfunction
endclass
typedef fifo #(byte, 4) byte_fifo_t;
typedef fifo #(int, 4) int_fifo_t;
module top;
byte_fifo_t bf;
byte_fifo_t bf_alias;
int_fifo_t wf;
byte bb;
int ww;
int errors = 0;
initial begin
bf = new();
wf = new();
bf_alias = bf; // same typedef: legal handle assignment
for (int i = 0; i < 4; i++) begin
if (!bf.try_put(8'h10 * (i + 1))) errors++; // 10, 20, 30, 40
end
if (bf.try_put(8'hFF) !== 0) errors++; // full: rejected
for (int i = 0; i < 4; i++) begin
if (!bf.try_get(bb)) errors++;
if (bb !== 8'h10 * (i + 1)) errors++; // order kept
end
if (!wf.try_put(1)) errors++;
if (!wf.try_put(2)) errors++;
if (!wf.try_put(3)) errors++;
for (int i = 1; i <= 3; i++) begin
if (!wf.try_get(ww)) errors++;
if (ww !== i) errors++;
end
if (bf_alias.level() !== 0) errors++; // alias sees the same drained queue
if (byte_fifo_t::put_total !== 4) errors++; // byte statics count byte puts
if (int_fifo_t::put_total !== 3) errors++; // int statics count int puts
if (errors == 0) $display("PASS: byte fifo kept 4 in order and each type counted its own puts");
else $fatal(1, "FAIL: fifo order or per-type put tallies differ from plan");
$finish;
end
endmodulePASS: byte fifo kept 4 in order and each type counted its own putsWith the typedefs from the listing above, handles of the two specializations never mix:
// bad-example: distinct specializations are unrelated types (typedefs from the listing above)
byte_fifo_t bf = new();
int_fifo_t wf = new();
bf = wf; // compile error: a byte fifo is never an int fifoOne scoreboard serves every protocol when the payload contract is explicit: the type must provide compare(). The built-in mailbox follows the same idea, with mailbox of a transaction type rejecting wrong-type puts at compile time.
class pkt;
int id;
int unsigned data;
function new(int i, int unsigned d);
id = i;
data = d;
endfunction
function bit compare(pkt rhs);
return (id == rhs.id) && (data == rhs.data);
endfunction
endclass
class frag;
int tag;
function new(int t);
tag = t;
endfunction
function bit compare(frag rhs);
return tag == rhs.tag;
endfunction
endclass
class scoreboard #(type T = pkt);
local T expected_q[$];
local int bad;
function void add_expected(T t);
expected_q.push_back(t);
endfunction
function void check_got(T t);
T exp;
if (expected_q.size() == 0) begin
bad++;
return;
end
exp = expected_q.pop_front();
if (!exp.compare(t)) bad++; // contract: T provides compare()
endfunction
function int get_bad();
return bad;
endfunction
endclass
typedef scoreboard #(pkt) pkt_sb_t;
typedef scoreboard #(frag) frag_sb_t;
module top;
pkt_sb_t psb;
frag_sb_t fsb;
pkt p0;
pkt p1;
pkt q0;
pkt q1;
frag f0;
frag f_wrong;
int errors = 0;
initial begin
psb = new();
fsb = new();
p0 = new(0, 32'hA1);
p1 = new(1, 32'hB2);
q0 = new(0, 32'hA1);
q1 = new(1, 32'hB2);
f0 = new(5);
f_wrong = new(6);
psb.add_expected(p0);
psb.add_expected(p1);
psb.check_got(q0);
psb.check_got(q1);
fsb.add_expected(f0);
fsb.check_got(f_wrong); // wrong tag: one bad check
if (psb.get_bad() !== 0) errors++;
if (fsb.get_bad() !== 1) errors++;
if (errors == 0) $display("PASS: one scoreboard served two payload types with 0 and 1 bad checks");
else $fatal(1, "FAIL: per-protocol bad tallies differ from 0 and 1");
$finish;
end
endmodulePASS: one scoreboard served two payload types with 0 and 1 bad checksExpecting one static counter across specializations reads the wrong variable. Spelling a specialization slightly differently at two sites silently creates two types.
Calling a method on T that some payload lacks breaks the implicit contract. Document the required methods next to the parameter list.
Two. Each specialization owns its own statics, so the byte count and the int count advance independently.
Yes when the effective parameters match. Fifo of byte with an omitted depth is the same type as fifo of byte with depth 8, but the typedef recipe removes the guessing.
One way to write it:
typedef enum bit [1:0] {DATA = 2'd0, CTRL = 2'd1, ERR = 2'd2} kind_e;
class pkt_txn;
kind_e kind;
bit [1:0] dest;
function new(kind_e k, bit [1:0] d);
kind = k; dest = d;
endfunction
endclass
class fifo #(type T = int, int DEPTH = 8);
local T q[$];
function bit try_put(T item);
if (q.size() >= DEPTH) return 0;
q.push_back(item);
return 1;
endfunction
function bit try_get(output T item);
if (q.size() == 0) return 0;
item = q.pop_front();
return 1;
endfunction
endclass
// PacketFIFO's reference model: the same container, holding packets, as deep as the RTL.
typedef fifo #(pkt_txn, 4) pkt_model_t;
module tb_pkt_model;
pkt_model_t model;
pkt_txn p;
int errors = 0;
initial begin
model = new();
for (int i = 0; i < 4; i++) begin
p = new(DATA, 2'(i));
if (!model.try_put(p)) errors++;
end
p = new(CTRL, 0);
if (model.try_put(p)) errors++; // fifth packet: full, rejected
for (int i = 0; i < 4; i++) begin
if (!model.try_get(p) || p.dest != 2'(i)) errors++;
end
if (model.try_get(p)) errors++; // empty again
if (errors == 0) $display("PASS: fifo #(pkt_txn, 4) took 4 packets, refused a fifth, and returned them in order");
else $fatal(1, "FAIL: %0d model checks failed", errors);
$finish;
end
endmodulePASS: fifo #(pkt_txn, 4) took 4 packets, refused a fifth, and returned them in orderRun with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown, with no warnings. These class benches were run on Verilator only.