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.

Note Originally published in 2017; rewritten in 2026. The UVM table in the original listed 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).
~15 min read · Intermediate body, Advanced tail · Part 2 of 2 — builds on Part 1: Polymorphism.

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 virtual methods — 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 TypeSyntaxImplementationOverride Required?
Regularfunction void foo();RequiredNo (and no dynamic dispatch — see Part 1)
Virtualvirtual function void foo();RequiredOptional
Pure Virtualpure virtual function void foo();ForbiddenMandatory

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 ClassActual declarationAbstract?
uvm_objectvirtual class uvm_object extends uvm_voidYes
uvm_componentvirtual class uvm_component extends uvm_report_objectYes
uvm_monitorvirtual class uvm_monitor extends uvm_componentYes
uvm_sequence#(REQ,RSP)virtual class uvm_sequence #(...)Yes
uvm_driver#(REQ,RSP)class uvm_driver #(...) extends uvm_componentNo — concrete
uvm_sequence_itemclass uvm_sequence_item extends uvm_transactionNo — concrete
Key Sit with the asymmetry for a moment: 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 caughtCompile timeRuntime — or never
Subclass burdenMust implement everythingImplement only what's used
Right whenMethod 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

MistakeSymptomFix
Instantiating an abstract classCompile error at new()Instantiate a concrete subclass; keep the abstract handle
Unimplemented pure virtual"Class is abstract" on the subclassImplement every inherited contract
Body on a pure virtualSyntax errorPrototype only — the body is the subclass's job
Skipping super.do_copy() in hooksBase fields silently not copiedAlways chain to super in do_* overrides
Unguarded $cast in do_copyWrong-type call corrupts or nulls fieldsif (!$cast(...)) `uvm_fatal
Data or method bodies in an interface classCompile errorInterface 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.

Q: Is 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.

Q: UVM's 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.

Q: When would you reach for an 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 virtual moves 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_item concrete. 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 class adds 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.
Author
Mayur Kubavat
DV engineer working on SoC verification. Writes here about UVM, PCIe, SystemVerilog, and the everyday craft of getting designs to tape-out.

Comments (0)

Leave a Comment