Part 8 · Senior & Interview Prep · Intermediate
Q&A: OOP
Class vs module, shallow vs deep copy, virtual methods and drivers, $cast purposes, static members, parameterized classes.
Q: Class vs module — why are testbenches built from classes?
A module is a static hardware container — instantiated at elaboration, fixed for the simulation, no inheritance. A class is a dynamic software object — created at runtime with new(), garbage-collected, supporting inheritance, polymorphism, and randomization. Testbenches are classes because verification needs runtime flexibility: thousands of transaction objects created and destroyed, behavior swapped by extension (an error-injecting driver) without touching the original code. DUTs are modules because hardware is static.
module fifo (...); // static: exists from elab to end
endmodule
class fifo_txn; // dynamic: thousands created at runtime
rand bit [7:0] data;
rand op_e op;
endclass
class err_txn extends fifo_txn; // extension — impossible for modules
constraint c_err { op == PUSH_WHEN_FULL; }
endclassFollow-up: "If classes are so flexible, why is the DUT not a class?" — Hardware is static structure with timing; modules synthesize, elaborate fixed hierarchy, and connect through ports. Classes have no pins and no synthesis semantics — they model behavior, not structure.
Junior vs senior: a junior lists feature differences. A senior connects each feature to the verification need: dynamic creation for transactions, inheritance for test variation, polymorphism for swapping components.
Q: Shallow copy vs deep copy — what does new obj actually copy?
t2 = new t1 is a shallow copy : every field is copied bit-for-bit — including handle fields, which means both objects end up pointing at the same nested sub-objects. A deep copy requires a hand-written copy() method that recursively copies nested objects. The classic bug: shallow-copying a transaction that contains a payload object, then modifying the copy's payload — and silently corrupting the original.
class packet;
bit [7:0] hdr;
payload pl; // handle to nested object
function packet copy(); // deep copy: write it yourself
packet c = new();
c.hdr = hdr;
c.pl = (pl == null) ? null : pl.copy(); // recurse!
return c;
endfunction
endclass
p2 = new p1; // SHALLOW: p2.pl == p1.pl (same object!)
p2.pl.len = 99; // ...just corrupted p1's payload too
p3 = p1.copy(); // DEEP: independent objectsFollow-up: "And t2 = t1 with no new?" — Not a copy at all: handle assignment. Both names refer to one object. Three levels: assignment (one object), shallow copy (two objects sharing nested ones), deep copy (fully independent).
Junior vs senior: a junior defines the terms. A senior gives all three levels, the corrupted-original bug, and notes that monitors must deep-copy (or allocate fresh) before publishing a transaction they will reuse.
Q: What do virtual methods do, and why do drivers need them?
A virtual method dispatches by the object's actual type rather than the handle's declared type — polymorphism. The testbench payoff: components hold base-class handles and call base-class methods, while tests inject derived objects with overridden behavior. A driver calling txn.pack() on a base handle automatically runs the derived error-injecting version when a test sends one — the driver is never edited.
class base_txn;
virtual function void apply(virtual fifo_if vif);
vif.cb.data <= data; // normal drive
endfunction
endclass
class corrupt_txn extends base_txn;
function void apply(virtual fifo_if vif); // override
vif.cb.data <= data ^ 8'hFF; // inject corruption
endfunction
endclass
// driver code — never changes:
base_txn t;
gen2drv.get(t); // may actually hold a corrupt_txn
t.apply(vif); // virtual → runs the overrideFollow-up: "What happens without virtual?" — Dispatch follows the handle's declared type: the base apply() runs even though the object is a corrupt_txn. The override is silently ignored — no error, just wrong stimulus, which is the worst kind of bug.
Junior vs senior: a junior defines polymorphism. A senior shows the driver-never-edited pattern and stresses that a forgotten virtual fails silently — declared type wins, no warning.
Q: What is $cast for — both purposes?
$cast performs a checked downcast : assigning a base-class handle to a derived-class handle, verified at runtime against the object's actual type. Called as a function it returns 0 on failure (test and branch); called as a task it errors out on failure (assert the impossible). The second use: converting an integral value to an enum, where direct assignment is a compile error.
base_txn b;
err_txn e;
// function form: legitimate "is it this type?" test
if ($cast(e, b))
$display("error txn, code=%0d", e.err_code); // safe: e valid here
else
; // normal txn path
// task form: failure = testbench bug, want loud death
$cast(e, b); // runtime fatal if b isn't an err_txn
// enum conversion
op_e op;
$cast(op, rdata[1:0]); // int → enum needs a cast; direct assign illegalFollow-up: "Why is an explicit upcast never needed?" — Every derived object is-a base object, so base_handle = derived_handle is always type-safe and implicit. Downcasting needs the runtime check because a base handle might point at a base object, a sibling type, or null.
Junior vs senior: a junior says "casting between classes." A senior distinguishes function vs task forms by intent — branch on type vs assert the impossible — and remembers the enum-conversion use.
Q: What are static members, and where do they belong in a testbench?
A static property has one copy shared by all instances of the class, existing from time zero without any object. The canonical testbench uses: a transaction ID counter (each new object grabs the next ID), and shared configuration or counters accessible without plumbing a handle everywhere. Static methods can be called without an instance but can only touch static members.
class fifo_txn;
static int next_id = 0; // one copy, all instances
int id; // per-instance
function new();
id = next_id++; // unique ID per transaction — free debugging
endfunction
static function int created_so_far();
return next_id; // static method: no instance needed
endfunction
endclass
// fifo_txn::created_so_far() — class-scope call, no objectFollow-up: "What is the danger of static state?" — It is global state in disguise: invisible coupling between tests, stale values across a soft-restart, and clashing IDs if two environments share the class. Use for IDs and debug counters; avoid as a configuration back-channel.
Junior vs senior: a junior defines static. A senior shows the ID-stamping idiom — the single best debugging investment in a testbench — and warns about static as hidden global state.
Q: What are parameterized classes for?
A class with #(type T = ...) or value parameters is a template: one implementation, many specializations. This is how generic testbench infrastructure exists — a scoreboard, mailbox wrapper, or coverage container written once and specialized per transaction type. Each distinct parameter set is a distinct type: a scoreboard of fifo_txn and a scoreboard of bus_txn are unrelated classes.
class scoreboard #(type T = base_txn);
T exp_q[$];
function void add_expected(T t); exp_q.push_back(t); endfunction
function void check(T actual);
T exp = exp_q.pop_front();
if (!actual.compare(exp)) $error("mismatch");
endfunction
endclass
scoreboard #(fifo_txn) fifo_sb; // specialization 1
scoreboard #(bus_txn) bus_sb; // distinct, incompatible type
class big_fifo #(int DEPTH = 16); // value parameter version
bit [7:0] mem [DEPTH];
endclassFollow-up: "Can scoreboard#(fifo_txn) and scoreboard#(bus_txn) share a base handle?" — Not directly — different specializations are unrelated types. The standard trick is a non-parameterized base class that the parameterized class extends, giving you a common handle type for storage and iteration.
Junior vs senior: a junior says "like generics." A senior knows each specialization is a separate type, the shared-base-class workaround, and points at mailbox #(T) as the built-in example everyone already uses.
Key takeaways
Classes for dynamic behavior (testbench); modules for static structure (DUT).
Three copy levels: handle assignment, shallow (new obj), deep (hand-written copy()).
virtual = dispatch by object type; forgetting it fails silently with declared-type dispatch.
$cast: function form to branch on type, task form to assert; also int→enum conversion.
Static ID stamping is cheap and invaluable; parameterized classes are the generic-infrastructure tool.
Common pitfalls
new obj on transactions with nested objects — shared payloads corrupt silently.
Missing virtual on an overridden method — base version runs, no warning.
Direct integral-to-enum assignment — compile error; needs $cast or static cast.
Treating different parameterizations as compatible types — they are strangers.