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.

Expected and actual responses
0123objectaxi_txnerr_txnaxi_txnerr_txnrresp (DUT)OKAYSLVERROKAYSLVERRexpected (non-virtual)OKAYexpected (virtual)OKAYSLVERROKAYSLVERRfalse failurefalse failure

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 typeObject typeNon-virtual method runsVirtual method runs
axi_txnaxi_txnaxi_txn versionaxi_txn version
axi_txnerr_txnaxi_txn versionerr_txn version
err_txnerr_txnerr_txn versionerr_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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: 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 SLVERR

Check yourself

Predict the outputPredict the exact output.
class t;
  virtual function string resp();
    return "OKAY";
  endfunction
  function string name();
    return "t";
  endfunction
endclass

class e extends t;
  function string resp();
    return "SLVERR";
  endfunction
  function string name();
    return "e";
  endfunction
endclass

module tb;
  t h;
  e x;
  initial begin
    x = new();
    h = x;
    $display("%s %s %s", h.resp(), h.name(), x.name());
  end
endmodule
ConceptWhich decides the version of a virtual method that runs when you call it through a handle?
ConceptThe base method is virtual; the override in the derived class leaves out the keyword. Is the override virtual?
Read the waveformThe scoreboard compares each response with t.expected_resp() through an axi_txn handle, but the base method is not virtual. In which cycles does it report a false failure?
Responses checked through base handles
0123objecterr_txnaxi_txnerr_txnrresp (DUT)SLVERROKAYSLVERR

An err_txn should get SLVERR; an axi_txn should get OKAY.

Spot the bugEach option declares expected_resp in the base class axi_txn. The scoreboard calls it through an axi_txn handle that refers to an err_txn, which overrides it to return SLVERR. Which option breaks the scoreboard?

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