Parameterized classes and static members
Share state across all objects of a class with static members, and know exactly which objects share it when classes are extended or parameterized.
- Lesson
- 4.6
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A transaction class counts its objects in static int created, and the test checks at the end that created == 1000, one per planned read. The check fails with 1,600. The count is right; the assumption is wrong. Six hundred error transactions were also built, and err_txn extends the read class, so its constructor runs the base constructor and increments the same static counter.
A static property declared in the base class is shared by every derived class.
Static properties belong to the class
A property declared static exists once for the class, not once per object. static int created = 0; is a single counter shared by every axi_txn object, and it exists even before any object is created.
You reach it through the class name with the scope operator, axi_txn::created, or through any handle of the class. All of those names lead to the same variable.
A static property is the right place for things that really are shared: a count of objects, the next free ID, or a table of settings that every object reads.
Shared also means global. If two environments in one simulation use the same class, they share its static settings, and one can change the other's behaviour without warning.
Static methods
A static method also belongs to the class, so you call it without an object: axi_txn::count(). Because there is no object, a static method has no this and cannot use ordinary properties; it can use only static ones.
A static method that hands out IDs, such as id_pool::alloc(), keeps the counter and the rule for advancing it in one place. No other code needs to touch the counter directly.
Static values last for the whole simulation. If a test applies reset or runs several phases, give the class a static reset() method that puts every counter back, and call it wherever the scoreboard is cleared.
Derived classes share the base static
Extending a class does not copy its static properties. err_txn uses the very same created as axi_txn, because the counter is declared, and incremented, in the base constructor.
That is why the count in the opening story included error transactions. If you need a count per kind, declare a separate static counter in each derived class and increment it in that class's constructor.
Parameterized classes
A parameterized class takes parameters, like a module. A value parameter sets a number, as in class id_pool #(int WIDTH = 4);. A type parameter sets a type, as in class typed_q #(type T = axi_txn);.
Each distinct set of parameter values creates a specialization, which is a separate class type. id_pool #(4) and id_pool #(8) are two different classes that happen to share source code.
Type parameters let you write one container or checker and use it for any transaction type, with full type checking and no casts. A typed_q #(err_txn) hands back err_txn handles directly.
Upcasting still works through a type parameter. A typed_q #(axi_txn) accepts err_txn handles, because every error transaction is an axi_txn, but a typed_q #(err_txn) rejects plain reads at compile time.
Parameter values must be known when the code is compiled. You choose id_pool #(8) in the source, not with a variable while the test runs, so a run-time choice between widths needs code that names both specializations.
Statics in constructors and copies
A constructor often takes the next ID from a static pool, as in id = id_pool #(4)::alloc();. Every object built with new then gets a fresh ID without the caller having to remember one.
A shallow copy made with new tr skips the constructor, as lesson 4.2 showed. The copy therefore keeps the original's ID and does not increment created, which is right for a stored expectation but wrong if you meant to build a new transaction.
Each specialization has its own statics
Static properties of a parameterized class exist once per specialization. id_pool #(4)::next_id and id_pool #(8)::next_id are two separate counters, so a 4-bit port and an 8-bit port can draw IDs without interfering.
| Declaration | Counter it uses |
|---|---|
id_pool #(4) | the 4-bit counter |
id_pool #(8) | the 8-bit counter, separate from the 4-bit one |
id_pool #() or id_pool | the default specialization, here the same as id_pool #(4) |
a class that extends id_base | one counter shared by every class that extends id_base |
Which static counter a declaration shares.
Counter in a shared base class
class id_base;
static int next_id;
endclass
class id_pool #(int WIDTH = 4)
extends id_base;
...
endclass
Every width draws from one counter, so each port sees gaps in its IDs.
Counter in the parameterized class
class id_pool #(int WIDTH = 4);
static int next_id;
...
endclass
Each width has its own counter and wraps at its own limit.
Make the choice on purpose. Put the static in the parameterized class for one copy per specialization, or in a non-parameterized base class for one copy across all of them.
Try it
Three reads and two error transactions made one counter reach 5. The 4-bit pool handed out 17 IDs in total and wrapped from 15 back to 0, while the 8-bit pool, a separate specialization, handed out only 2.
Add static int made; to err_txn and increment it in its constructor, then print both counts. Predict the two numbers first.
objects created: 5 (base and derived share one counter) 4-bit pool next=1, 8-bit pool next=2 default specialization is the 4-bit pool: next=1Check yourself
Remember
- A static property exists once per class and is shared by every object; reach it with
class::name. - A static method has no
thisand can use only static members. - Derived classes share the static properties of their base class.
- Each set of parameter values creates a separate class, with its own static properties.
- Put a static in the parameterized class for one copy per specialization, or in a common base class for one shared copy.
Practice
- Typed value box Implement · Easy