Inheritance and super.new
Extend a transaction class for error injection, pass the base class everything it needs through super.new, and keep base-class state owned by the base class.
- Lesson
- 4.3
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
A new error-injecting transaction extends the read transaction and adds the beat to corrupt. Its constructor sets err_beat but never calls super.new(id, addr). Every base argument has a default, so the compiler quietly inserts super.new() with no arguments, and every error transaction is built with ID 0. In the scoreboard they collide with the real ID 0 reads, and the duplicate-ID check from Module 2 fires on a correct DUT.
The derived constructor accepted the ID but never passed it to the base class.
A derived class extends a base class
class err_txn extends axi_txn; declares a derived class, also called a subclass. It inherits every property and method of the base class axi_txn, and it can add its own, such as err_beat.
An err_txn object therefore has an ID, an address and an error beat. Code written for axi_txn can work on it unchanged, because everything that code uses is still there.
A base-class handle may refer to a derived object: axi_txn t = e; is always legal. Through t you can use only the members that axi_txn declares, which is what lets one scoreboard queue hold every kind of transaction.
Construction runs from the base class outward
When you construct a derived object, the base part is built first. The derived constructor calls super.new(...) to run the base constructor, and that call must be the first statement in the derived constructor.
e = new(5, 'h80, 2);
The derived constructor starts and immediately calls the base constructor.
super.new(5, 'h80);
The base constructor sets the properties it owns.
id 5addr 80this.err_beat = 2;
Back in the derived constructor, the new property is set.
err_beat 2
If a derived constructor does not call super.new, the compiler inserts super.new() with no arguments. That compiles only when every base argument has a default, and then the base part silently gets the default values, which is the trap in the opening story.
The super.new trap
Arguments never reach the base class
function new(int id = 0, int addr = 0,
int err_beat = 0);
// super.new() is inserted here,
// with no arguments
this.err_beat = err_beat;
endfunction
Every error transaction gets ID 0 and address 0.
Pass them on first
function new(int id = 0, int addr = 0,
int err_beat = 0);
super.new(id, addr);
this.err_beat = err_beat;
endfunction
ID and address are set by the base constructor, as for every other transaction.
Removing the defaults from the base constructor turns this silent bug into a compile error. With function new(int id, int addr); in the base class, a derived class that forgets super.new(id, addr) no longer compiles.
Let the base class own its state
The derived constructor could set this.id = id; itself, and the object would look right. But then two classes set the ID, and when the base constructor gains a check or a new property, the derived class silently skips it.
Keep each property set in one place, the class that declares it. The derived class passes values up through super.new and sets only what it adds.
The keywords local and protected enforce this. A local property can be used only inside its own class, and a protected one also inside derived classes, so code outside cannot change either directly.
Overriding a method and calling the original
A derived class can override a method by declaring one with the same name and arguments. Inside the override, super.describe() calls the base version, so the derived method can extend the base behaviour instead of repeating it.
For an error transaction, describe can return the base description followed by the error beat. If the base description later gains a field, the error transaction shows it too, with no change to the derived class. An override that forgets to call super.describe() drops the base fields from every log line for that class, which is easy to miss.
Testing a derived class
Construct the derived class through each of its constructors, then check the base fields as well as the new ones. The forgotten super.new in the opening story shows up at once as an ID of 0.
Then use the object through a base handle, the way the scoreboard will. Push it onto a queue of axi_txn handles and read the ID and address back from there.
When to extend and when to contain
Extend a class when the new thing really is a kind of the old one: an error transaction is a transaction. Every place that accepts an axi_txn can then accept an err_txn.
When one thing merely uses another, hold a handle to it instead. A scoreboard has transactions but is not one, so it contains a queue of axi_txn handles rather than extending axi_txn.
Try it
The correct constructor passed the ID and address to super.new, and describe built on the base description. The forgetful one set its own property but left the base part at its defaults.
Remove the defaults from the base constructor, so it reads function new(int id, int addr);, and run it again. The forgetful class now fails to compile, which is the result you want.
err_txn: id=5 addr=80 err_beat=2 forgot super: id=0 addr=0 err_beat=2 base handle on an err_txn: id=5 addr=80Check yourself
Remember
- A derived class inherits every member of its base class and adds its own; a base handle may refer to a derived object.
- A derived constructor must call
super.new(...)first; if it does not,super.new()with no arguments is inserted. - Base constructors without default arguments turn a forgotten
super.newinto a compile error. - Let each class set its own properties, and call
super.method()to build on a base method. - Extend for "is a kind of"; hold a handle for "uses".
Practice
- Derived read transaction Implement · Easy