SystemVerilog · Module 9
Abstract Classes & Pure Virtual Methods
Contracts every subclass must implement, compile-time enforcement, the protocol-agnostic driver pattern, and the backward-compatibility hazards of evolving a shipped abstract base.
Module 9 · Page 9.11
The Problem Abstract Classes Solve
You are building a generic scoreboard framework. You want every team that uses it to plug in their own comparison logic. You create a base BaseChecker class with a check() method.
The problem: what should the base check() do? It cannot know — comparison logic is protocol-specific. If you leave it empty, every team can forget to override it and the checker silently does nothing. If you put a $fatal in it, it is messy and the error only happens at runtime.
Abstract classes solve this at compile time. Declare check() as pure virtual — now any class that extends your checker but does not implement check() simply cannot be compiled. The contract is enforced before a single simulation runs.
Syntax — virtual class and pure virtual
// ── 'virtual class' = abstract ────────────────────────────────
// Cannot be instantiated directly
virtual class BaseChecker;
// Regular property — works normally
string checker_name;
int pass_count;
int fail_count;
// Regular constructor — runs when concrete child is created
function new(string name = "checker");
checker_name = name;
pass_count = 0;
fail_count = 0;
endfunction
// ── PURE VIRTUAL ── no body, MUST be implemented by child ──
pure virtual function bit check(BaseTxn exp, BaseTxn got);
// ── Regular virtual — has default body, child CAN override ─
virtual function void report();
$display("[%s] PASS=%0d FAIL=%0d",
checker_name, pass_count, fail_count);
endfunction
// ── Non-virtual — always runs this version, no override ────
function void run_check(BaseTxn exp, BaseTxn got);
if (check(exp, got)) // calls child's implementation
pass_count++;
else
fail_count++;
endfunction
endclass
// BaseChecker bc = new(); ← COMPILE ERROR: cannot instantiate abstract classThree types of methods live inside an abstract class. Understanding the difference is the whole topic: pure virtualMUST override No body — just a declaration. Every concrete subclass must implement this or it stays abstract and cannot be instantiated.
pure virtual function bit check(...);
virtualCAN override
Has a default body. Child may override but is not required. Polymorphic dispatch applies when accessed through parent handle.
virtual function void report();
non-virtualFIXED
No override possible. Always runs the base class version. Use for utility logic that is the same for all subclasses.
function void run_check(...);
Abstract vs Concrete — What Makes a Class Concrete
A class is abstract if it has even one unimplemented pure virtual method — either its own or one it inherited but did not implement. A class is concrete when every pure virtual method in the entire hierarchy has been given a body. Only concrete classes can be instantiated.
Abstract vs Concrete — Instantiation Rules virtual class BaseChecker cannot new()pure virtual function bit check(...) ← no bodyvirtual function void report()function void run_check(...)→ virtual class PartialChecker extends BaseChecker cannot new()pure virtual function bit check(...) ← still no bodyvirtual function void report() ← overridden↓ class DataChecker extends BaseChecker can new() ✓function bit check(...) ← implemented — class is now concrete class AddrChecker extends BaseChecker can new() ✓function bit check(...) ← implemented — class is now concrete
Implementing Pure Virtual Methods
When a concrete child implements a pure virtual method, the syntax is identical to overriding a regular virtual method. There is no special keyword. You just write the function with the same name and matching signature.
// ── Abstract base ─────────────────────────────────────────────
virtual class BaseChecker;
string name;
int fail_count;
function new(string n = "checker");
name = n;
fail_count = 0;
endfunction
pure virtual function bit check(BaseTxn exp, BaseTxn got);
function void run_check(BaseTxn exp, BaseTxn got);
if (!check(exp, got)) begin
fail_count++;
$error("[%s] Check FAILED", name);
end
endfunction
endclass
// ── Concrete: checks data field ───────────────────────────────
class DataChecker extends BaseChecker;
function new();
super.new("DataChecker");
endfunction
// Mandatory implementation — same signature as pure virtual
virtual function bit check(BaseTxn exp, BaseTxn got);
return (exp.data === got.data);
endfunction
endclass
// ── Concrete: checks address field ────────────────────────────
class AddrChecker extends BaseChecker;
function new();
super.new("AddrChecker");
endfunction
virtual function bit check(BaseTxn exp, BaseTxn got);
return (exp.addr === got.addr);
endfunction
endclass
// ── Concrete: checks both fields ──────────────────────────────
class FullChecker extends BaseChecker;
function new();
super.new("FullChecker");
endfunction
virtual function bit check(BaseTxn exp, BaseTxn got);
bit ok = 1;
if (exp.addr !== got.addr) begin
$error("addr mismatch: exp=0x%08h got=0x%08h",
exp.addr, got.addr);
ok = 0;
end
if (exp.data !== got.data) begin
$error("data mismatch: exp=0x%08h got=0x%08h",
exp.data, got.data);
ok = 0;
end
return ok;
endfunction
endclass
// ── Scoreboard using any checker polymorphically ───────────────
module tb;
BaseChecker checkers[3]; // abstract handle array
BaseTxn exp_t, got_t;
initial begin
checkers[0] = new DataChecker();
checkers[1] = new AddrChecker();
checkers[2] = new FullChecker();
exp_t = new(); exp_t.addr = 32'h4000_0000; exp_t.data = 32'hA5A5_A5A5;
got_t = new(); got_t.addr = 32'h4000_0000; got_t.data = 32'hA5A5_A5A6;
// One loop — each checker calls its own check() implementation
foreach (checkers[i])
checkers[i].run_check(exp_t, got_t);
end
endmoduleMultiple Pure Virtual Methods
An abstract class can declare as many pure virtual methods as needed. The child must implement all of them to become concrete. This is how you define a complete interface contract — a set of capabilities every implementor must provide.
// ── Abstract coverage interface ───────────────────────────────
virtual class BaseCoverage;
string cov_name;
function new(string n); cov_name = n; endfunction
// Every coverage class MUST implement all three
pure virtual function void sample(BaseTxn t);
pure virtual function real get_coverage();
pure virtual function void reset();
// Non-virtual: shared report format for all coverage types
function void report();
$display("[Coverage: %s] %.1f%%",
cov_name, get_coverage());
endfunction
endclass
// ── Concrete: address range coverage ─────────────────────────
class AddrCoverage extends BaseCoverage;
bit addr_hit[256]; // track which 256 address buckets were hit
function new(); super.new("AddrCoverage"); endfunction
virtual function void sample(BaseTxn t);
addr_hit[t.addr[7:0]] = 1; // bucket by lower byte
endfunction
virtual function real get_coverage();
int hits = 0;
foreach (addr_hit[i]) if (addr_hit[i]) hits++;
return (hits * 100.0) / 256.0;
endfunction
virtual function void reset();
foreach (addr_hit[i]) addr_hit[i] = 0;
endfunction
endclass
// ── Concrete: write/read direction coverage ───────────────────
class DirCoverage extends BaseCoverage;
int write_count, read_count;
function new(); super.new("DirCoverage"); endfunction
virtual function void sample(BaseTxn t);
if (t.write) write_count++;
else read_count++;
endfunction
virtual function real get_coverage();
return (write_count > 0 && read_count > 0) ? 100.0 : 50.0;
endfunction
virtual function void reset();
write_count = 0; read_count = 0;
endfunction
endclass
// ── Environment drives all coverage collectors generically ────
module tb;
BaseCoverage cov_list[2];
BaseTxn t;
initial begin
cov_list[0] = new AddrCoverage();
cov_list[1] = new DirCoverage();
repeat(20) begin
t = new(); void'(t.randomize());
foreach (cov_list[c])
cov_list[c].sample(t); // polymorphic — right sample() runs
end
// Report all — each calls its own get_coverage()
foreach (cov_list[c])
cov_list[c].report();
end
endmoduleAbstract Classes in UVM
The UVM methodology uses both regular virtual classes and patterns that behave like abstract classes. When you write a UVM sequence and override body(), that method is declared virtual in uvm_sequence_base. Your override is what actually drives transactions — if you forget to override body(), nothing happens at all.
The pattern is identical to pure virtual in intent, even though UVM uses a default empty body rather than pure virtual. When you design your own reusable frameworks — base sequences, base scoreboards, base coverage collectors — use pure virtual to make the contract explicit and the omission a compile error.
// Abstract base sequence — enforces common structure
virtual class BaseRegTest;
string test_name;
int pass_count;
function new(string n); test_name = n; pass_count = 0; endfunction
// Every register test must define its own scenario
pure virtual task run();
// Every register test must be able to check results
pure virtual function bit check_results();
// Framework provides the full execution flow
task execute();
$display("[%s] Starting...", test_name);
run(); // child's scenario runs here
if (check_results()) // child's check logic
$display("[%s] PASS", test_name);
else
$error("[%s] FAIL", test_name);
endtask
endclass
// Concrete test — provides run() and check_results()
class ResetValueTest extends BaseRegTest;
function new(); super.new("ResetValueTest"); endfunction
virtual task run();
// Apply reset, read registers
$display(" Applying reset and reading registers...");
endtask
virtual function bit check_results();
// Verify all registers at reset values
return 1; // simplified
endfunction
endclassQuick Reference
| Feature | Syntax | Key Rule |
|---|---|---|
| Abstract class | virtual class Name; | Cannot be instantiated directly — compile error if you try |
| Pure virtual method | pure virtual function type name(args); | No body; every concrete subclass must implement it |
| Regular virtual method | virtual function type name(args); | Has default body; child can override — polymorphism applies |
| Non-virtual method | function type name(args); | Cannot be overridden — always runs base version |
| Concrete class | No virtual class keyword; implements all pure virtuals | Can be instantiated with new() |
| Polymorphic use | BaseClass h = new ConcreteChild(); | Abstract handles can hold concrete objects — polymorphism works normally |
Verification Usage — Where Abstract Classes Earn Their Keep
UVM ships with abstract classes at every layer: uvm_object, uvm_report_object, uvm_component, uvm_driver, uvm_monitor — every one of them declares hooks that descendants must implement. Three patterns dominate the day-to-day use of abstract classes in production testbenches.
The protocol-agnostic driver interface
A verification environment that must support multiple bus protocols (AXI, AHB, APB, OCP) defines an abstract bus_driver with pure virtual drive(), reset(), and collect_coverage(). Each protocol subclass implements those hooks. The test layer talks only to the abstract handle — switching protocols is one line in build_phase.
<code>virtual class bus_driver extends uvm_driver;
pure virtual task drive(uvm_sequence_item t);
pure virtual task reset();
pure virtual function void collect_coverage(uvm_sequence_item t);
endclass
class axi_driver extends bus_driver;
virtual task drive(uvm_sequence_item t); /* AXI handshake */ endtask
virtual task reset(); /* AXI reset */ endtask
virtual function void collect_coverage(uvm_sequence_item t); /* AXI cg */ endfunction
endclass
class apb_driver extends bus_driver;
virtual task drive(uvm_sequence_item t); /* APB handshake */ endtask
virtual task reset(); /* APB reset */ endtask
virtual function void collect_coverage(uvm_sequence_item t); /* APB cg */ endfunction
endclass</code>The abstract scoreboard reference model
A scoreboard talks to a reference model through an abstract ref_model handle. The test plugs in either a SystemVerilog reference, a SystemC reference via DPI, or a C-model — all conforming to the same pure-virtual predict() contract. No scoreboard change required.
Pluggable error-injection strategies
An abstract error_injector with a single pure virtual inject(packet p) method lets each test plug in a different error policy — CRC corruption, timeout injection, parity flip — without touching the driver or monitor. The abstract class enforces that every error strategy implements at least the basic inject() hook.
Simulation Behavior — How Abstract Classes Affect Elaboration & Runtime
Elaboration-time enforcement
The check for "every pure virtual is implemented" happens at compile/elaboration time, not at runtime. The simulator examines each concrete (non-abstract) class's vtable and verifies that every slot has a non-null function pointer. If any slot is empty, compilation fails with a message naming the specific method and the class missing the implementation. You can never reach simulation with an abstract class instantiated.
Vtable layout for abstract classes
Abstract classes still get vtables — they have to, because their concrete descendants inherit and extend that vtable. The pure virtual slots are simply marked as "must-implement-before-instantiation." A descendant's vtable copies the parent's slot layout and either inherits a non-pure parent slot, fills in a pure-virtual slot with its own implementation, or leaves a pure slot empty (and thereby remains abstract itself).
Calling a pure virtual through a parent handle is impossible at runtime
Because you can never construct an abstract class, you can never have an object whose vtable has empty slots — the runtime simply cannot reach a "call into a null function pointer" state through normal class usage. This is the type-system guarantee that makes pure virtuals safe.
<code>virtual class shape;
pure virtual function int area();
endclass
class square extends shape; // implements area
int side;
function int area();
return side * side;
endfunction
endclass
square s = new(); // OK
shape h = s; // OK, polymorphic</code><code>virtual class shape;
pure virtual function int area();
endclass
shape h = new();
// ^^^
// Error: cannot instantiate
// virtual class 'shape'
//
// Forces you to construct a
// concrete subclass instead.</code>Waveform Analysis — Tracing Abstract-Class Hierarchies in Debug
Abstract classes are simulation-only structural scaffolding — they leave no direct waveform footprint. What does show up is the chain of overridden methods that abstract dispatch reaches. Instrumenting each concrete implementer with a clear log line is the practical way to confirm that the right subclass was actually invoked.
The trace pattern for protocol dispatch
<code>virtual class bus_driver;
pure virtual task drive(uvm_sequence_item t);
endclass
class axi_driver extends bus_driver;
virtual task drive(uvm_sequence_item t);
$display("[%0t] AXI driver firing this=%s id=%0d",
$time, $typename(this), t.get_transaction_id());
// ... AXI handshake ...
endtask
endclass
class apb_driver extends bus_driver;
virtual task drive(uvm_sequence_item t);
$display("[%0t] APB driver firing this=%s id=%0d",
$time, $typename(this), t.get_transaction_id());
// ... APB handshake ...
endtask
endclass</code>ASCII view — a mixed-protocol environment
<code> 100ns AXI driver firing this=axi_driver id=1
250ns APB driver firing this=apb_driver id=2
400ns AXI driver firing this=axi_driver id=3
550ns APB driver firing this=apb_driver id=4
↑ ↑
concrete impl runtime type proves pure-virtual
that ran dispatch reached correct subclass</code>Industry Insights — Hard-Won Lessons About Abstract Classes
Debugging Academy — Five Real Bugs From Abstract-Class Usage
"Compile error: cannot instantiate virtual class"
DEBUGThe line shape s = new(); rejects compilation with "cannot instantiate virtual class 'shape'".
<code>virtual class shape;
pure virtual function int area();
endclass
module tb;
initial begin
shape s = new(); // BUG: abstract class — cannot construct
$display("area=%0d", s.area());
end
endmodule</code>virtual class marks shape as abstract — only concrete subclasses can be instantiated. shape exists only to define the contract every subclass must follow.
Construct a concrete subclass and (optionally) store it in an abstract-typed handle for polymorphism: class square extends shape; int side; function int area(); return side*side; endfunction endclass then shape s = square::new();.
"Compile error: must implement pure virtual method 'area'"
DEBUGA subclass of an abstract shape compiles fine on its own but errors out at the first new() attempt with "class 'triangle' must implement pure virtual method 'area'".
<code>virtual class shape;
pure virtual function int area();
endclass
class triangle extends shape;
int base, height;
// BUG: forgot to implement area()
endclass
triangle t = new(); // compile error here</code>A subclass remains abstract until it implements every pure virtual method inherited from any ancestor. triangle never implements area(), so it is still abstract — and abstract classes cannot be instantiated.
Implement the missing method: function int area(); return (base * height) / 2; endfunction. The compiler error always names exactly which method is missing — use that as your checklist.
"Subclass appears to compile but stays abstract due to signature mismatch"
DEBUGA subclass clearly implements the same-named method, yet new() on it fails with "must implement pure virtual".
<code>virtual class predictor;
pure virtual function void predict(packet p);
endclass
class axi_predictor extends predictor;
// BUG: signature drift — takes a different argument type
virtual function void predict(axi_packet p);
// ...
endfunction
endclass
axi_predictor ap = new(); // compile error</code>An override must have the exact same signature as the parent declaration — same name, same return type, same argument types in the same order. predict(packet) and predict(axi_packet) are two different methods; the second one does not override the first, so the pure virtual remains unfilled.
Match the signature: virtual function void predict(packet p); axi_packet ap; if (!$cast(ap, p)) return; /* AXI-specific logic */ endfunction. Use $cast inside to recover the specific type.
"Pure virtual gives a body — compile rejects it"
DEBUGThe simulator rejects pure virtual function void log(); $display("base"); endfunction with "pure virtual cannot have a body."
<code>virtual class agent_base;
pure virtual function void log();
$display("base default log"); // BUG: pure virtual cannot have a body
endfunction
endclass</code>pure virtual declares the slot only — it is a contract, not a default. If you want a default implementation that subclasses can choose to override, drop pure and keep just virtual.
Pick one of two intents: (a) pure virtual function void log(); (mandatory override, no body); or (b) virtual function void log(); $display("base default"); endfunction (default body, override optional). The two are fundamentally different design choices.
"Adding a pure virtual to a shipped base class broke every customer subclass"
DEBUGA VIP release adds pure virtual function void collect_coverage(packet p); to driver_base. Every downstream project's compile breaks with "must implement pure virtual collect_coverage" on dozens of subclasses.
<code>// VIP v2 — base class
virtual class driver_base;
pure virtual task drive(packet p);
pure virtual function void collect_coverage(packet p); // NEW in v2 — breaks all old subclasses
endclass
// Customer's existing subclass — written against VIP v1
class axi_driver extends driver_base;
virtual task drive(packet p); /* ... */ endtask
// no collect_coverage — becomes abstract under v2
endclass</code>Adding a pure virtual to a shipped base class breaks backward compatibility — every existing subclass instantly becomes abstract. There is no "default behaviour" with pure virtual, so customers must add implementations.
When evolving a shipped base class, either (a) add the new method as plain virtual with an empty (or sensible default) body, so existing subclasses keep compiling; or (b) document the new pure virtual as a major-version breaking change and ship migration notes. Reserve pure virtual for the initial design of a base class.
Interview Q&A — Twelve Questions You Will Be Asked
A class declared with the virtual keyword in front of class — i.e., virtual class my_base;. Abstract classes cannot be instantiated directly; they exist only to be extended by concrete subclasses. They typically contain one or more pure virtual methods that subclasses must implement before they themselves can be instantiated.
Best Practices — Ten Rules for Using Abstract Classes Well
- Use abstract classes whenever you have three or more parallel implementations of the same role. Drivers across protocols, predictors across modes, error injectors across policies — exactly the cases pure virtuals were invented for.
- Prefer
pure virtualover empty-bodyvirtualfor mandatory hooks. Compile-time enforcement always beats runtime silent failure. - Never give a
pure virtualmethod a body. The intent of pure virtual is "no default"; if you want a default, use plainvirtual. - Match override signatures exactly. Name, return type, argument types in order — a single argument type mismatch turns "override" into "new method" and leaves the pure virtual unfilled.
- Mark intermediate abstract classes explicitly
virtual class. If a subclass legitimately leaves pure virtuals unimplemented, the compiler's "must implement" error is confusing; making the intent explicit documents the design. - Use abstract-typed handles for polymorphic containers. A queue of
bus_drivercan hold AXI, AHB, and APB drivers; the abstract type guarantees the queue is type-safe at the contract level. - Never add a new
pure virtualto a shipped base class. Use plainvirtualwith a default body to preserve backward compatibility; reserve pure virtual for the initial design. - Document the abstract-class contract at the top of the file. What are the pure virtuals? What should each one do? What invariants must every implementation preserve? An abstract class is a public API contract — treat its docstring with the same care.
- Print
$typename(this)in every pure-virtual implementation during bring-up. The dispatch path through abstract classes is invisible without the runtime type at the implementation call site. - Don't try to instantiate abstract classes. The compiler will stop you, but the cleanest signal of intent is constructing a concrete subclass directly and only using the abstract type as a handle declaration.