Part 2 · OOP for Verification · Intermediate

Inheritance Interview Traps

Five classic puzzles with worked answers: non-virtual override, property hiding, constructor ordering, virtual calls in constructors, sibling $cast.

Trap 1 — the non-virtual override surprise

The most common opener: an override exists, the object is the derived type, yet the base method runs. The missing keyword is virtual — without it, the handle's declared type picks the body at compile time, and the derived 'override' is really a separate method that merely hides the base one.

systemverilog
class animal;
  function void speak();              // NOT virtual
    $display("...");
  endfunction
endclass

class dog extends animal;
  function void speak();              // hides, does not override
    $display("woof");
  endfunction
endclass

animal a = dog::new_dog();   // assume returns a dog
dog    d = new();
a = d;

a.speak();   // Q: what prints?
d.speak();   // Q: what prints?

Worked answer

  • a.speak() prints "..." — non-virtual, so the declared type (animal) binds the call at compile time; the object being a dog is irrelevant.

  • d.speak() prints "woof" — declared type is dog, so dog's version binds.

  • One-line fix: virtual function void speak(); in animal — then a.speak() prints "woof".

  • The sentence to say: "non-virtual resolves on handle type; virtual resolves on object type."


Trap 2 — property hiding (there is no virtual for data)

Methods can dispatch dynamically; properties never do . If a derived class declares a property with the same name as a base property, the object carries BOTH, and the handle's declared type decides which one any access touches. There is no 'virtual data'. Mixing the two — base code writing the base copy, derived code reading the derived copy — produces values that seem to flicker depending on which handle you look through.

systemverilog
class base;
  int mode = 1;
  function void set_mode(int m);
    mode = m;                  // ALWAYS base::mode — compiled in base
  endfunction
endclass

class child extends base;
  int mode = 2;                // second, separate 'mode' — hides base's
endclass

child c = new();
base  b = c;                   // same single object, two views

c.set_mode(7);                 // inherited method writes base::mode

$display(b.mode);   // 7  ← base view sees base::mode
$display(c.mode);   // 2  ← child view sees child::mode (untouched!)

Worked answer

  • The object holds two distinct integers both named mode; b.mode and c.mode are different storage.

  • set_mode lives in base, so its bare 'mode' is resolved at compile time to base::mode — writing 7 there.

  • Output: 7 then 2. There is no dispatch for data; only the declared type of the accessing handle/method matters.

  • Real-world rule: never shadow base properties; if behavior must vary, wrap access in virtual get/set methods.


Trap 3 — constructor ordering

A favorite 'predict the output' puzzle. The rule: construction runs base-to-derived, with super.new() (explicit or compiler-inserted) executing before the rest of the derived constructor body.

systemverilog
class A;
  function new();
    $display("A::new");
  endfunction
endclass

class B extends A;
  function new();
    super.new();
    $display("B::new");
  endfunction
endclass

class C extends B;
  function new();
    // no explicit super.new() — compiler inserts super.new()
    $display("C::new");
  endfunction
endclass

C c = new();
// Output:
//   A::new
//   B::new
//   C::new          ← base-most first, always

Worked answer

  • Order is A, B, C — each constructor's first action is (implicitly or explicitly) running its parent's constructor.

  • C omits super.new(); the compiler inserts a zero-argument call — legal here because B::new takes no arguments.

  • Follow-up they ask: "what if B::new required an argument?" — C would fail to compile until it called super.new(arg) explicitly.


Trap 4 — calling a virtual method from a constructor

Virtual dispatch works inside constructors — and that is exactly the problem. During super.new(), the object already IS its final derived type, so a virtual call from the base constructor lands in the derived override — which then runs before the derived constructor body has initialized the fields it depends on. The override executes against default-valued state.

systemverilog
class base;
  function new();
    init();                      // virtual call from constructor
  endfunction
  virtual function void init();
    $display("base init");
  endfunction
endclass

class child extends base;
  int unsigned depth = 16;       // initializer runs with child's ctor

  function new();
    super.new();                 // base::new → init() → CHILD's init,
                                 // but depth is still 0 here!
    $display("after super, depth=%0d", depth);
  endfunction

  virtual function void init();
    $display("child init, depth=%0d", depth);   // prints depth=0
  endfunction
endclass

child c = new();
// child init, depth=0        ← derived override, uninitialized state
// after super, depth=16

Worked answer

  • The virtual call dispatches to child::init (object type is child even during base construction).

  • child::init reads depth before the derived initialization has run — it sees 0, not 16.

  • Rule: constructors should only initialize their own level; defer virtual setup to an explicit post-construction call (UVM solves this with build_phase).


Trap 5 — $cast between siblings

Two classes extend the same base. A base handle points at one sibling; the candidate tries to $cast it to the other. Sharing a parent creates no relationship between siblings — casts succeed only when the actual object's type is the destination type or a descendant of it.

systemverilog
class bus_txn;          endclass
class axi_txn extends bus_txn;  endclass
class ahb_txn extends bus_txn;  endclass

bus_txn b;
axi_txn a = new();
ahb_txn h;

b = a;                       // base handle → axi object

if ($cast(h, b))             // axi object into ahb handle?
  $display("cast OK");
else
  $display("cast FAILED");   // ← this prints

$cast(h, b);                 // task form: runtime ERROR instead

Worked answer

  • The object behind b is an axi_txn. An axi_txn is NOT an ahb_txn — siblings share a parent, nothing more.

  • Function form returns 0 (h unchanged, still null); task form raises a runtime error.

  • The check $cast performs: is the object's actual class the destination class or derived from it? Sibling: no.

  • Bonus point: $cast(b2, b) to the base always succeeds — every object IS its own base.

diagram
THE FIVE TRAPS — one-line cheat sheet

  1. non-virtual override   handle type picks body; add 'virtual'
  2. property hiding        data never dispatches; two fields exist
  3. constructor order      base-most new() first, always
  4. virtual in ctor        derived override runs on uninit state
  5. sibling $cast          fails; only dest-or-descendant succeeds

  Master sentence: "declared type controls non-virtual calls and
  ALL property access; object type controls virtual calls and $cast."

Key takeaways

  • Non-virtual methods and all properties resolve on the handle's declared type; virtual methods and $cast resolve on the object's runtime type.

  • Property hiding creates two coexisting fields — never shadow base data; use virtual accessors when behavior must vary.

  • Constructors run base-to-derived; virtual calls inside them reach derived overrides before derived state exists.

  • $cast succeeds only to the object's own class or an ancestor of it — sibling casts always fail.

Common pitfalls

  • Answering trap 1 with only 'add virtual' — interviewers want the resolution rule stated, not just the fix.

  • Claiming derived properties 'override' base ones — they hide them; both copies live in the object.

  • Forgetting the compiler inserts super.new() — changes the constructor-order answer when parents need arguments.

  • Using task-form $cast in trap-5 style code — turns an expected mismatch into a simulation-killing runtime error.