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.

systemverilog
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; }
endclass

Follow-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.

systemverilog
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 objects

Follow-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.

systemverilog
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 override

Follow-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.

systemverilog
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 illegal

Follow-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.

systemverilog
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 object

Follow-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.

systemverilog
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];
endclass

Follow-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.