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.
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.
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.
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, alwaysWorked 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.
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=16Worked 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.
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 insteadWorked 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.
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.