Skip to content

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

Abstract Class — Complete Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── '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 class

Three 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.

Implementing pure virtual — Concrete Subclasses
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Multiple 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.

Multiple Pure Virtual — Coverage Collector Interface
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Abstract 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 — UVM-Style Pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endclass

Quick Reference

FeatureSyntaxKey Rule
Abstract classvirtual class Name;Cannot be instantiated directly — compile error if you try
Pure virtual methodpure virtual function type name(args);No body; every concrete subclass must implement it
Regular virtual methodvirtual function type name(args);Has default body; child can override — polymorphism applies
Non-virtual methodfunction type name(args);Cannot be overridden — always runs base version
Concrete classNo virtual class keyword; implements all pure virtualsCan be instantiated with new()
Polymorphic useBaseClass 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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.

✅ Concrete class — instantiable
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
❌ Direct construction of abstract
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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

1

"Compile error: cannot instantiate virtual class"

DEBUG
Symptom

The line shape s = new(); rejects compilation with "cannot instantiate virtual class 'shape'".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

virtual class marks shape as abstract — only concrete subclasses can be instantiated. shape exists only to define the contract every subclass must follow.

Fix

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();.

2

"Compile error: must implement pure virtual method 'area'"

DEBUG
Symptom

A 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'".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

3

"Subclass appears to compile but stays abstract due to signature mismatch"

DEBUG
Symptom

A subclass clearly implements the same-named method, yet new() on it fails with "must implement pure virtual".

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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.

4

"Pure virtual gives a body — compile rejects it"

DEBUG
Symptom

The simulator rejects pure virtual function void log(); $display("base"); endfunction with "pure virtual cannot have a body."

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<code>virtual class agent_base;
  pure virtual function void log();
    $display("base default log");   // BUG: pure virtual cannot have a body
  endfunction
endclass</code>
Root Cause

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.

Fix

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.

5

"Adding a pure virtual to a shipped base class broke every customer subclass"

DEBUG
Symptom

A 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.

Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<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>
Root Cause

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.

Fix

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

  1. 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.
  2. Prefer pure virtual over empty-body virtual for mandatory hooks. Compile-time enforcement always beats runtime silent failure.
  3. Never give a pure virtual method a body. The intent of pure virtual is "no default"; if you want a default, use plain virtual.
  4. 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.
  5. 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.
  6. Use abstract-typed handles for polymorphic containers. A queue of bus_driver can hold AXI, AHB, and APB drivers; the abstract type guarantees the queue is type-safe at the contract level.
  7. Never add a new pure virtual to a shipped base class. Use plain virtual with a default body to preserve backward compatibility; reserve pure virtual for the initial design.
  8. 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.
  9. 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.
  10. 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.