Virtual methods and dynamic dispatch
Predict which method runs when you call it through a base handle, and make the scoreboard ask each transaction what to expect.
- Lesson
- 4.4
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
The scoreboard keeps every request in a queue of axi_txn handles and asks t.expected_resp() which response code to expect. The error transaction overrides expected_resp() to return SLVERR, but the base method was not declared virtual. Every call through the base handle runs the base version, so the scoreboard expects OKAY for injected errors and fails each correct error response from the DUT.
With a non-virtual method, the handle type decides; with a virtual method, the object decides.
Every call involves two types
A call such as t.expected_resp() involves two types. The handle type is how t was declared, here axi_txn. The object type is the class of the object t refers to right now, which may be err_txn.
The compiler knows only the handle type. The object type is known only while the simulation runs, and it can change every time t is assigned. Assigning a different object to t therefore changes which virtual method runs, without any change to the calling code.
Non-virtual and virtual calls
For a non-virtual method, the handle type picks the method, and the choice is fixed when the code is compiled. For a virtual method, the object type picks it while the simulation runs. This run-time choice is called dynamic dispatch.
| Handle type | Object type | Non-virtual method runs | Virtual method runs |
|---|---|---|---|
axi_txn | axi_txn | axi_txn version | axi_txn version |
axi_txn | err_txn | axi_txn version | err_txn version |
err_txn | err_txn | err_txn version | err_txn version |
Only the middle row differs, and it is the one a scoreboard queue of base handles uses.
A scoreboard holds base handles on purpose, so that one queue can carry every kind of transaction. Every method it calls through those handles must therefore be virtual, or the derived classes are ignored.
The missing virtual keyword
Non-virtual base method
class axi_txn;
function string expected_resp();
return "OKAY";
endfunction
endclass
Through an axi_txn handle, every transaction expects OKAY, including error transactions.
Virtual base method
class axi_txn;
virtual function string expected_resp();
return "OKAY";
endfunction
endclass
Each object answers for itself: an err_txn expects SLVERR.
The derived class compiles and looks correct in both cases, because overriding a non-virtual method is legal. The difference appears only when the call goes through a base handle, so test that path directly.
Rules for overriding
An override must match the virtual method it replaces: the same name, the same argument types, names and directions, and a compatible return type. SystemVerilog has no overloading, so the standard treats a mismatched override as an error.
Once a method is virtual, it stays virtual in every derived class, whether or not the override repeats the keyword. Repeating virtual in overrides is still good practice, because readers then see the rule where it applies.
Abstract classes and pure virtual methods
A pure virtual method declares the method with no body: pure virtual function string expected_resp();. A class containing one must be declared virtual class, which makes it an abstract class.
You cannot create an object of an abstract class; new on it is a compile error. Every class that extends it must implement each pure virtual method before objects of it can be created.
An abstract base class is a way to say what every transaction must be able to do, while leaving the details to each kind.
Building on the base version
An override can call the version it replaces with super.expected_resp(). A burst error transaction might return the base answer for every beat except the corrupted one, without repeating the base logic.
Not every method should be virtual. A small helper that formats the ID, for example, must behave the same for every kind of transaction. Leaving it non-virtual tells readers it is not meant to be overridden.
Testing dispatch
A dispatch test must go through a base handle, because that is the only path where virtual and non-virtual methods differ. Calling the method through a derived handle proves nothing.
Put one object of each derived class into a queue of base handles, call the method on every entry, and compare each answer with the expected one. The example below does exactly that.
Let the object answer
With virtual methods, the scoreboard does not need to know which kinds of transaction exist. It calls t.expected_resp() and t.compare(rsp), and each object answers for itself.
Adding a new kind, such as a transaction that expects DECERR, then means writing one derived class. The scoreboard does not change at all, which is the main reason to build it this way.
Try it
Through the queue's axi_txn handles, the non-virtual method always runs the base version, while the virtual method follows each object. Through an err_txn handle, even the non-virtual method runs the derived version.
Add a third class, dec_txn, whose expected_resp returns DECERR, push one onto the queue, and predict the new line. Notice that the loop needs no change.
id 1: non-virtual says OKAY, virtual says OKAY id 2: non-virtual says OKAY, virtual says SLVERR id 3: non-virtual says OKAY, virtual says OKAY through an err_txn handle, non-virtual says SLVERRCheck yourself
Remember
- A call through a handle involves the handle type and the object type.
- A non-virtual method is chosen by the handle type; a virtual method by the object, at run time.
- Declare every method a scoreboard calls through base handles as virtual.
- Once virtual, a method stays virtual in every derived class.
- A pure virtual method makes its class abstract: it cannot be constructed, and derived classes must implement the method.
Practice
- Virtual response length Implement · Easy