Part 2 · OOP for Verification · Intermediate
Polymorphic Containers
Queues of base handles holding mixed derived transactions, iterate-and-dispatch, and factory-style creation functions.
One queue, many types
A container is declared with one element type — but if that type is a base class handle, the container can hold objects of any derived type simultaneously, because each element is just a reference and every derived object IS-A base. A bus_txn q[$] can mix plain bus transactions, AXI bursts, and error-injection transactions in one stream. This is what makes generic testbench plumbing possible: mailboxes, scoreboard queues, and analysis paths are all declared once at the base type and never change as new transaction flavors are added.
POLYMORPHIC QUEUE — handles in, mixed objects behind
bus_txn q[$];
q: [ h0 ][ h1 ][ h2 ][ h3 ] ← all slots typed bus_txn
│ │ │ │
▼ ▼ ▼ ▼
bus_txn axi_txn err_txn axi_txn ← real object types differ
(qos=3) (bad (qos=0)
parity)
q[i].print() — virtual → each object prints ITS OWN format
q[i].qos — COMPILE ERROR: base handle exposes base members
only; derived fields need $cast firstbus_txn q[$];
axi_txn a = new(); a.qos = 3;
err_txn e = new();
bus_txn b = new();
q.push_back(b);
q.push_back(a); // implicit upcast on insertion
q.push_back(e);
foreach (q[i])
q[i].print(); // virtual dispatch: 3 different print bodies runIterating: virtual dispatch first, $cast when you must
Consuming a mixed container, you have two tools. Prefer virtual methods : if the per-type behavior lives inside the objects (print, compare, pack, check), the loop body is one virtual call and never mentions concrete types — adding a new transaction flavor requires zero consumer changes. Reach for $cast only when the consumer genuinely needs type-specific fields the base cannot expose — AXI-only coverage sampling qos, for example. A long if/else-if chain of casts is a design smell: behavior that belongs in a virtual method has leaked into the consumer.
// GOOD: behavior lives in the objects — consumer is type-blind
function void check_all(bus_txn q[$]);
foreach (q[i])
if (!q[i].check()) // virtual — each type's own rules
$error("txn %0d failed self-check", i);
endfunction
// ACCEPTABLE: type-specific side path via function-form $cast
function void sample_axi_cov(bus_txn q[$]);
axi_txn a;
foreach (q[i])
if ($cast(a, q[i])) // only AXI objects taken
axi_cg.sample(a.qos, a.burst); // derived-only fields
// non-AXI types skipped by design — mixed stream is normal here
endfunctionFactory-style creation returning a base handle
The creation-side mirror of a polymorphic container is a factory function : it decides which concrete class to construct — from a mode enum, a weighted random pick, or test configuration — and returns the object through a base handle. Generators built this way emit mixed streams without knowing concrete types beyond the creation point; everything downstream rides on virtual dispatch. This is the hand-rolled version of what the UVM factory automates with type overrides, and building it once by hand makes the UVM mechanism obvious.
typedef enum {PLAIN, AXI, ERR} txn_kind_e;
function bus_txn make_txn(txn_kind_e kind);
case (kind)
PLAIN: begin bus_txn t = new(); return t; end
AXI: begin axi_txn t = new(); return t; end // upcast on return
ERR: begin err_txn t = new(); return t; end
endcase
endfunction
task generator(mailbox #(bus_txn) mb, int n);
txn_kind_e kind;
bus_txn t;
repeat (n) begin
void'(std::randomize(kind) with {
kind dist {PLAIN := 6, AXI := 3, ERR := 1};
});
t = make_txn(kind); // concrete type chosen HERE only
void'(t.randomize()); // derived constraints apply
mb.put(t); // mixed stream, one mailbox
end
endtaskInterview angle
'Can a queue hold different class types?' — yes, via base-class handles; each element references a derived object.
'How do you call derived-only fields from the queue?' — $cast to a derived handle first; base handles expose base members only.
'What breaks if print() isn't virtual?' — every element prints the base format; the mixed stream looks homogeneous in logs.
'Sketch a factory function' — switch on a kind, construct concrete, return base handle; randomize after creation.
Key takeaways
Base-handle containers hold mixed derived objects — declare plumbing once at the base type.
Consume via virtual methods so per-type behavior stays inside the objects; consumers stay type-blind.
Use function-form $cast for genuinely type-specific side paths; cast chains signal misplaced behavior.
Factory functions construct concrete types and return base handles — the manual ancestor of the UVM factory.
Common pitfalls
Accessing derived fields through the base element type — compile error; the handle's type limits visibility.
Non-virtual print/compare in the base — the whole container behaves as the base type, silently.
Storing then re-randomizing the same handle — every queue slot aliases one mutating object (see Shallow vs Deep Copy).
Growing if/else cast ladders in consumers — move the behavior into a virtual method instead.