Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations35
  • Object-Oriented SystemVerilog for Verification35
  • Constraint Randomization — Interview-Critical Deep Dive66
  • SystemVerilog Assertions — Interview-Critical Deep Dive63
  • Functional Coverage in SystemVerilog28
  • Testbench Architecture in SystemVerilog38
  • Advanced Language & Integration30
  • Senior & Interview Preparation34
    • Senior Debug Playbook
    • Reviewing Verification Code
    • Verification Strategy: Spec to Sign-Off
    • SystemVerilog Interview Q&A Bank
      • Q&A: Language Core
      • Q&A: Arrays & Queues
      • Q&A: Testbench Plumbing
      • Q&A: Classes and Interfaces
      • Q&A: OOP
      • Q&A: Constraint and Assertion Fire-Drill
      • Q&A: Interfaces & Virtual Interfaces
      • Q&A: Processes & IPC
      • Q&A: Scenario and Design Questions
      • Q&A: Constraints Quick-Fire
      • Q&A: SVA Quick-Fire
    • Capstone: Verify a Parameterized FIFO
  • Principal-Level Verification28
Loading lesson

‹SystemVerilog Interview Q&A Bank
  1. Topics
  2. SystemVerilog
  3. Senior & Interview Preparation
  4. SystemVerilog Interview Q&A Bank
  5. Q&A: OOP

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.

On this page

    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.

    fifo.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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.

    Previous

    322 / 363

    Next: Q&A: Constraint and Assertion Fire-Drill

    Up next

    Up nextQ&A: Constraint and Assertion Fire-Drill

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Q&A: Language Core
    • Q&A: Arrays & Queues
    • Q&A: Testbench Plumbing