2. OOP in SystemVerilog - Abstract Classes
Two teams ship the same VIP mistake. Team A's base transaction declares virtual function bit is_valid(); return 1; endfunction — a friendly default. A user extends it, forgets to override, and for a week their environment happily blesses malformed packets; the bug surfaces in a lab, expensively. Team B's base declares pure virtual function bit is_valid();. The same forgetful user gets a compile error in ten seconds: class is abstract, is_valid not implemented. Same omission, wildly different cost — because Team B moved the contract from convention to compiler.
That is the entire value proposition of abstract classes: making illegal states uncompilable. This post covers the mechanics (virtual class, pure virtual), then goes where the 2017 original couldn't: what UVM's own source actually declares abstract (the original got two of five wrong — corrected below against the official code), why UVM mostly chose runtime hooks instead of pure virtual, and what interface class adds that single inheritance can't.
uvm_sequence_item and uvm_driver as abstract — they are concrete classes. The corrected table below was verified against the Accellera uvm-core reference implementation (July 2026).What is an Abstract Class?
An abstract class — declared with virtual class — is a class that:
- Cannot be instantiated —
new()on it is a compile error - Serves as a template for derived classes, carrying shared fields and default behavior
- May declare
pure virtualmethods — contracts every concrete subclass must implement, enforced at compile time
virtual class ClassName; // abstract — cannot new()
pure virtual function bit is_valid(); // contract, no body
endclass
class DerivedClass extends ClassName; // concrete — must implement
virtual function bit is_valid(); return 1; endfunction
endclass
Three Kinds of Methods
flowchart TD
A["Abstract Class Methods"] --> B["Regular Methods"]
A --> C["Virtual Methods"]
A --> D["Pure Virtual Methods"]
B --> B1["Has implementation
Dispatched by handle type"]
C --> C1["Has implementation
Dispatched by object type"]
D --> D1["No implementation
MUST be overridden"]
style A fill:#dbeafe,stroke:#3b82f6
style D fill:#fee2e2,stroke:#ef4444
style D1 fill:#fee2e2,stroke:#ef4444
| Method Type | Syntax | Implementation | Override Required? |
|---|---|---|---|
| Regular | function void foo(); | Required | No (and no dynamic dispatch — see Part 1) |
| Virtual | virtual function void foo(); | Required | Optional |
| Pure Virtual | pure virtual function void foo(); | Forbidden | Mandatory |
Complete Example: an Abstract Transaction
// Abstract base — the contract every bus transaction must honor
virtual class Transaction;
rand bit [31:0] addr;
rand bit [31:0] data;
// Default behavior, overridable
virtual function void print();
$display("Transaction: addr=0x%0h, data=0x%0h", addr, data);
endfunction
// Contracts — no body, compiler enforces implementation
pure virtual function bit is_valid();
pure virtual task execute();
endclass
class WriteTransaction extends Transaction;
rand bit [3:0] byte_en;
virtual function void print();
$display("WRITE: addr=0x%0h, data=0x%0h, be=0x%0h", addr, data, byte_en);
endfunction
virtual function bit is_valid();
return (addr[1:0] == 2'b00); // aligned only
endfunction
virtual task execute();
$display("Executing WRITE to 0x%0h", addr);
#10;
endtask
endclass
class ReadTransaction extends Transaction;
rand int burst_len;
virtual function void print();
$display("READ: addr=0x%0h, burst=%0d", addr, burst_len);
endfunction
virtual function bit is_valid();
return (burst_len inside {[1:16]});
endfunction
virtual task execute();
$display("Executing READ from 0x%0h", addr);
#5;
endtask
endclass
module tb;
initial begin
Transaction t; // abstract handle: legal
WriteTransaction wr = new();
ReadTransaction rd = new();
// t = new(); // COMPILE ERROR — cannot instantiate virtual class
assert(wr.randomize());
assert(rd.randomize());
t = wr; t.print(); t.execute(); // dispatches to WriteTransaction
t = rd; t.print(); // dispatches to ReadTransaction
end
endmodule
The abstract handle t is the payoff: scoreboard queues, driver interfaces, and coverage collectors all traffic in Transaction, while the compiler guarantees that whatever object actually arrives has a real is_valid() and execute(). Contract up front, variety underneath.
What UVM Actually Declares Abstract — Corrected
The 2017 version of this post published a table calling five UVM bases "abstract." Two of the five are wrong, and the truth is more instructive than the folklore. From the Accellera uvm-core source:
| UVM Class | Actual declaration | Abstract? |
|---|---|---|
uvm_object | virtual class uvm_object extends uvm_void | Yes |
uvm_component | virtual class uvm_component extends uvm_report_object | Yes |
uvm_monitor | virtual class uvm_monitor extends uvm_component | Yes |
uvm_sequence#(REQ,RSP) | virtual class uvm_sequence #(...) | Yes |
uvm_driver#(REQ,RSP) | class uvm_driver #(...) extends uvm_component | No — concrete |
uvm_sequence_item | class uvm_sequence_item extends uvm_transaction | No — concrete |
uvm_monitor is abstract, uvm_driver is not — two classes with mirror-image roles, opposite declarations. There is no deep design reason; it is a historical quirk of the library. The lesson generalizes: when a question hinges on what a library declares, read the declaration. Folklore about UVM (including this blog's own 2017 table) drifts; the source doesn't. Concrete uvm_sequence_item is no accident though — you can new() one directly as a trivial payload, which small testbenches legitimately do.The Hook Pattern: Why UVM Mostly Avoids Pure Virtual
Given this post's sales pitch for compile-time contracts, UVM's own choice is worth examining: do_copy, do_compare, do_print are not pure virtual. They're virtual methods with do-nothing default bodies, called by the concrete public methods copy(), compare(), print() — the classic template method pattern (the framework owns the algorithm, you fill in the steps; see the design-patterns series). Even uvm_sequence::body() isn't pure virtual — its default body raises a runtime warning instead.
Why? Because pure virtual is a tax on every subclass. If do_copy were pure, every one-line experiment extending uvm_object would be forced to implement copying it never uses. UVM chose gradual adoption over strict contracts — the framework can't know which of its dozen hooks your class actually needs. The trade-off table is the real content here:
| Pure virtual (contract) | Virtual hook (default body) | |
|---|---|---|
| Missing override caught | Compile time | Runtime — or never |
| Subclass burden | Must implement everything | Implement only what's used |
| Right when | Method is essential to correctness (is_valid, reg2bus) | Method is optional capability (do_print) |
Your VIP should use both deliberately: pure virtual for the methods whose absence is a bug, hooks for the ones whose absence is a shrug. And when you do override the hooks, guard the cast — the 2017 version of this post itself published the unguarded form:
virtual function void do_copy(uvm_object rhs);
my_transaction t;
super.do_copy(rhs); // never skip the chain
if (!$cast(t, rhs))
`uvm_fatal("CPY", "do_copy called with wrong type") // fail loud, not wrong
this.addr = t.addr;
this.data = t.data;
endfunction
Interface Classes: Contracts Without a Family Tree
A virtual class gives you one inheritance slot — extend it and your ancestry is spent. SystemVerilog's interface class (since SV-2012, no relation to signal interfaces) is a pure contract: only pure virtual methods, no data, and a class may implements as many as it likes while still extends-ing one base. The 2017 post had a comparison table here and no code; here is what it looks like in a real environment:
// Capabilities, not ancestors
interface class reportable;
pure virtual function string to_report();
endclass
interface class replayable;
pure virtual function void replay_to(int fd);
endclass
// One inheritance slot spent on uvm_sequence_item, capabilities added freely
class apb_txn extends uvm_sequence_item implements reportable, replayable;
`uvm_object_utils(apb_txn)
rand bit [31:0] addr, data;
function new(string name = "apb_txn"); super.new(name); endfunction
virtual function string to_report();
return $sformatf("APB @0x%0h = 0x%0h", addr, data);
endfunction
virtual function void replay_to(int fd);
$fdisplay(fd, "apb_write(0x%0h, 0x%0h);", addr, data);
endfunction
endclass
// Consumers depend on the capability, not the class family:
function void log_all(reportable items[$]);
foreach (items[i]) $display(items[i].to_report());
endfunction
log_all accepts APB, AHB, and PCIe transactions from unrelated hierarchies, as long as each implements reportable. That's the choice heuristic in one line: abstract class for shared implementation within a family; interface class for shared capability across families.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Instantiating an abstract class | Compile error at new() | Instantiate a concrete subclass; keep the abstract handle |
| Unimplemented pure virtual | "Class is abstract" on the subclass | Implement every inherited contract |
| Body on a pure virtual | Syntax error | Prototype only — the body is the subclass's job |
Skipping super.do_copy() in hooks | Base fields silently not copied | Always chain to super in do_* overrides |
Unguarded $cast in do_copy | Wrong-type call corrupts or nulls fields | if (!$cast(...)) `uvm_fatal |
Data or method bodies in an interface class | Compile error | Interface classes are pure contract — move shared code to an abstract class |
Interview Corner
Q: Can you create an object of an abstract class? Can you still use one?A: You can't new() one — that's compile-time enforced. But abstract-class handles are the workhorse of every polymorphic design: they point at concrete-subclass objects and dispatch virtual methods to them. "Can't instantiate" and "can't use" are opposite answers.
uvm_sequence_item an abstract class?
A: No — it's declared as a plain class and can be instantiated directly as a trivial payload. uvm_object, uvm_component, uvm_monitor, and uvm_sequence#() are the virtual classes; uvm_driver, perhaps surprisingly, is concrete. Interviewers love this one precisely because most candidates repeat the "all UVM bases are abstract" folklore.
do_copy isn't pure virtual. Was that the right call?
A: It's a deliberate trade: pure virtual gives compile-time safety but taxes every subclass with implementations it may not need; virtual hooks with defaults allow gradual adoption but let a forgotten override fail silently at runtime. A framework serving thousands of teams reasonably picks hooks; your in-house VIP base class, where is_valid() being missing is always a bug, should pick pure virtual. Strong answers name the trade-off, not a winner.
interface class over a virtual class?
A: When the contract must cross family lines. A class already extends its one base; implements lets it additionally promise capabilities — reportable, replayable — that consumers can depend on without caring about ancestry. Abstract class = shared implementation within a hierarchy; interface class = shared promises across hierarchies.
Beyond the Basics: Advanced → Expert
Level 1 — Designing the contract boundary in a VIP
The practical skill is deciding, method by method, which enforcement each deserves. A rubric that holds up: anything the driver or checker calls unconditionally is pure virtual (is_valid, pack_bytes, get_delay); anything that's a reporting or debug nicety is a hook with a sane default; anything truly invariant is a non-virtual method the subclass can't break. A base class where everything is pure virtual is as much a design failure as one where nothing is — it means the base captured no shared behavior at all.
Level 2 — Parameterized abstract classes and the type-erasure ladder
uvm_sequence#(REQ,RSP) is abstract and parameterized, which introduces a subtlety that bites at scale: every distinct parameterization is a distinct, unrelated type. A uvm_sequence#(apb_txn) handle and a uvm_sequence#(ahb_txn) handle share no assignable base at the parameterized level. UVM's answer is a two-story architecture — the unparameterized uvm_sequence_base above the parameterized abstract class — so heterogeneous machinery (sequencers, arbitration queues) traffics in the base while typed code enjoys the parameter. When you build parameterized abstract VIP classes, copy this ladder: an unparameterized abstract anchor for storage and control, a parameterized layer for type safety. Skipping the anchor is why "how do I put different sequences in one queue" is a perennial forum question.
Level 3 — Capability-based architecture with interface classes
At environment scale, interface classes invert dependency direction. A scoreboard that needs to compare items shouldn't demand they extend my_vip_base_txn (which forces every agent in the SoC bench onto one family tree); it should demand implements comparable and let each VIP keep its own ancestry. The idiom pairs with a guarded capability probe: if ($cast(cmp, item)) use it; else fall back — dynamic capability discovery, the same move as querying an interface in COM or Go. Environments built this way absorb third-party VIP without wrapper classes, because the integration surface is a set of small promises rather than a base class nobody else inherits from.
Level 4 — The abstract BFM: bridging the class and module kingdoms
The expert-tier use of abstract classes in DV solves a problem OOP textbooks never mention: classes live in the dynamic world, but synthesizable BFMs for emulation must live inside modules/interfaces, and virtual interfaces don't cross that boundary well on accelerators. The pattern — write an abstract API class in a package (virtual class apb_bfm_api; pure virtual task drive(apb_txn t); endclass), then declare a concrete class extending it inside the interface itself, where it can touch signals directly. The interface registers an instance of that class; the UVM driver retrieves it as an apb_bfm_api handle and calls drive() — never touching a virtual interface. The abstract class is the doorway between kingdoms: the testbench sees a contract, the interface supplies signal-level implementation, and the same environment retargets from simulation to emulation by swapping which concrete class registers itself. This is the architecture behind most dual-domain (simulation/emulation) testbenches in production.
Level 5 — Abstract classes meet the factory
One last collision course: the UVM factory must be able to new() what it creates, and abstract classes can't be newed — so `uvm_object_utils on a virtual class doesn't compile. UVM provides the little-known escape hatches `uvm_abstract_object_utils / `uvm_abstract_component_utils: they register the abstract type with the factory without a constructor, existing purely so it can serve as an override target — set_type_override_by_type(vip_base_txn::get_type(), vendor_txn::get_type()). The pattern: publish an abstract type as the factory-visible seam of your VIP, ship concrete implementations separately, and let integrators re-point the seam without ever instantiating the abstraction. It's the factory-flavored version of this post's opening moral — the base class exists to be a contract, and the tooling finally lets it stay one.
Key Takeaways
virtual class+pure virtualmoves contract violations from the lab to the compiler — use it for every method whose absence is a bug.- UVM's real declarations:
uvm_object/uvm_component/uvm_monitor/uvm_sequence#()abstract;uvm_driver/uvm_sequence_itemconcrete. When it matters, read the source. - UVM's
do_*hooks show the other side of the trade: virtual-with-default for optional capabilities, pure virtual for essential ones. Choose per method, not per ideology. interface classadds cross-family contracts with multiple inheritance — capabilities (reportable) rather than ancestors.- The expert patterns — type-erasure ladders, capability probes, abstract BFMs,
`uvm_abstract_object_utils— are all the same idea at larger radius: publish contracts, hide families.
Comments (0)
Leave a Comment