SystemVerilog · Module 9
Nested Classes
Class-scoped helper types, qualified-name discipline, no implicit enclosing-instance access, and the namespace-hygiene wins of nesting class-local state enums.
Module 9 · Page 9.13
What Nested Classes Are
A nested class is simply a class declared inside another class. The inner class becomes scoped to the outer class — it is only visible through the outer class's name. Nothing else about it is special. It still has its own properties, methods, constructors, and lifecycle.
The main reason to nest a class is organisation. When a small helper class is tightly related to one outer class and makes no sense independently, putting it inside keeps the namespace clean and signals the relationship clearly. Nested Class Scope — How Visibility Worksclass ApbDriver rand bit [31:0] addr; Config cfg; class Config (nested inside ApbDriver) int clk_mhz = 100; int timeout = 1000; bit verbose = 0; Config is only accessible as ApbDriver::Config from outside. Inside ApbDriver's own methods, use it directly as Config.
Basic Syntax
// ── Outer class ───────────────────────────────────────────────
class ApbDriver;
// ── Nested (inner) class ──────────────────────────────────
class Config;
int clk_mhz = 100;
int timeout = 1000;
bit verbose = 0;
function void display();
$display("Config: clk=%0d timeout=%0d verbose=%0b",
clk_mhz, timeout, verbose);
endfunction
endclass // ← inner class ends here, inside outer
// Outer class properties — use inner class name directly
Config cfg; // short name works inside the outer class
string name;
function new(string n = "drv");
this.name = n;
this.cfg = new(); // create the nested Config object
endfunction
function void display();
$display("[%s]", name);
cfg.display(); // call inner object's method
endfunction
endclass
// ── Accessing the inner class from OUTSIDE the outer class ────
module tb;
ApbDriver drv; // declare handle at module level
initial begin
// Use :: to access inner class through outer class name
ApbDriver::Config my_cfg = new();
my_cfg.clk_mhz = 200;
my_cfg.verbose = 1;
my_cfg.display();
// Config: clk=200 timeout=1000 verbose=1
// Create via the outer object
drv = new("apb_drv");
drv.cfg.clk_mhz = 50; // ← can modify cfg directly
drv.display(); // ← prints driver + config info
end
endmoduleAccess Rules — What Can Touch What
This is the part that surprises most engineers. The inner class does not automatically have access to the outer class's instance properties. There is no implicit back-reference. If the inner class needs data from the outer class, you pass it explicitly.
class Outer;
int instance_var = 42; // instance property
static int shared_count = 0; // static property
local int secret = 99; // local — private to Outer
class Inner;
int inner_val;
function void try_access();
// instance_var ← ERROR: no outer-class object context
// secret ← ERROR: local to Outer, Inner cannot see it
// Static members ARE accessible — they belong to the class, not an object
shared_count++; // OK: Outer::shared_count
$display("shared=%0d", shared_count);
endfunction
endclass
// Outer class can pass its own data to Inner explicitly
function void give_data_to_inner(Inner i);
i.inner_val = instance_var; // outer passes its own data
endfunction
endclass| What the inner class tries to access | Result |
|---|---|
Outer class instance property (instance_var) | ❌ Compile error — no implicit outer-object reference |
Outer class local property (secret) | ❌ Compile error — local is private to the outer class only |
Outer class static property (shared_count) | ✅ Allowed — static belongs to the class, not an object |
| Its own properties and methods | ✅ Normal — inner class is a full class |
| Outer data passed as a method argument | ✅ Allowed — explicit parameter, no scoping issue |
typedef Inside a Class — A Cleaner Pattern
Instead of nesting a full class, you can define a typedef inside a class. This gives the outer class a locally-scoped type alias without the weight of a full nested class definition. It is lighter and more common for simple type aliases.
class AhbTransaction;
// typedef scoped to this class — cleaner than a nested class
typedef enum bit [2:0] {
SINGLE = 3'b000,
INCR = 3'b001,
WRAP4 = 3'b010,
INCR4 = 3'b011,
WRAP8 = 3'b100,
INCR8 = 3'b101
} burst_e;
typedef enum bit [1:0] {
OKAY = 2'b00,
ERROR = 2'b01,
RETRY = 2'b10,
SPLIT = 2'b11
} resp_e;
rand burst_e hburst;
rand bit [31:0] haddr;
resp_e hresp;
function void display();
$display("[AHB] burst=%s addr=0x%08h resp=%s",
hburst.name(), haddr, hresp.name());
endfunction
endclass
// Access typedef from outside via :: operator
AhbTransaction::burst_e b = AhbTransaction::INCR4;
$display("burst = %s", b.name()); // burst = INCR4Practical Verification Use Cases
Use Case 1 — Driver with Nested Config
Group all configuration knobs for a driver into a nested Config class. All the settings live in one place, easy to read and extend. The outer driver creates a Config object in its constructor and uses it through the property.
class SpiDriver;
// ── Nested class — defined inside SpiDriver's scope ───────
class Config;
int clk_mhz = 10;
bit [1:0] spi_mode = 2'b00; // CPOL=0 CPHA=0
int cs_delay = 2;
bit lsb_first = 0;
int word_size = 8;
function void display();
$display("[SPI Config] clk=%0dMHz mode=%02b word=%0db %s",
clk_mhz, spi_mode, word_size,
lsb_first ? "LSB" : "MSB");
endfunction
endclass // ← Config ends here, still inside SpiDriver
// ── Outer class properties ────────────────────────────────
Config cfg; // use short name inside the outer class
string name;
// ── Constructor: create the inner Config object here ──────
function new(string n = "spi_drv");
name = n;
cfg = new(); // creates the nested Config object
endfunction
function void display();
$display("Driver: %s", name);
cfg.display(); // call the inner object's method
endfunction
endclass
module tb;
SpiDriver drv1, drv2;
SpiDriver::Config standalone_cfg;
initial begin
// ── Simple usage — default config ─────────────────────
drv1 = new();
drv1.display();
// Driver: spi_drv
// [SPI Config] clk=10MHz mode=00 word=8b MSB
// ── Modify config via the outer object's cfg property ─
drv2 = new("fast_drv");
drv2.cfg.clk_mhz = 50;
drv2.cfg.lsb_first = 1;
drv2.display();
// Driver: fast_drv
// [SPI Config] clk=50MHz mode=00 word=8b LSB
// ── Create a standalone Config via outer class scope ──
standalone_cfg = new();
standalone_cfg.clk_mhz = 100;
standalone_cfg.display();
// [SPI Config] clk=100MHz mode=00 word=8b MSB
end
endmoduleUse Case 2 — Transaction with Nested Status
A transaction class often needs a small status sub-object — populated after the DUT responds. Nesting it keeps the response fields logically separate from the stimulus fields while keeping everything in one file.
class I2cTransaction;
// ── Nested status object — filled by the monitor after drive ─
class Status;
bit ack_received;
bit arb_lost;
bit timeout;
int cycles_taken;
function bit is_success();
return (ack_received && !arb_lost && !timeout);
endfunction
function void display();
$display(" Status: ack=%0b arb=%0b tout=%0b cycles=%0d [%s]",
ack_received, arb_lost, timeout,
cycles_taken,
is_success() ? "OK" : "FAIL");
endfunction
endclass
// ── Stimulus fields ───────────────────────────────────────
rand bit [6:0] slave_addr;
rand bit [7:0] reg_addr;
rand bit [7:0] data;
bit read_write; // 0=write 1=read
int txn_id;
// ── Response — created automatically in constructor ────────
Status status;
function new(int id = 0);
txn_id = id;
status = new(); // always create status object
endfunction
function void display();
$display("[I2C #%0d] %s slave=0x%02h reg=0x%02h data=0x%02h",
txn_id,
read_write ? "RD" : "WR",
slave_addr, reg_addr, data);
status.display();
endfunction
endclass
module tb;
I2cTransaction t = new(1);
initial begin
void'(t.randomize());
// Simulate DUT filling in status
t.status.ack_received = 1;
t.status.cycles_taken = 18;
t.display();
end
endmoduleNested vs Separate Class — Which One to Use
Use nested class when
- The inner class only makes sense in the context of the outer class
- The inner class is never used anywhere outside the outer class
- It is a small helper (Config, Status, Stats) with no independent life
- You want to keep the global namespace clean Use separate class when
- The class is used by multiple other classes
- It is large enough to deserve its own file
- It participates in an inheritance hierarchy
- Other teams need to extend or import it independently
Quick Reference
| Operation | Syntax | Notes |
|---|---|---|
| Declare nested class | class Inner; ... endclass inside outer class body | Inner class is scoped to outer class |
| Use inner class inside outer | Inner obj = new(); | Short name works inside the outer class |
| Access inner class from outside | OuterClass::InnerClass obj = new(); | Scope resolution :: required |
| Inner accesses outer static | OuterClass::static_prop or just static_prop | ✅ Allowed — static has no object context |
| Inner accesses outer instance property | Not possible without an explicit outer handle | ❌ No implicit outer-object reference |
| Inner accesses outer local property | Not possible | ❌ local blocks even nested classes |
| typedef inside a class | typedef enum {...} my_e; inside class body | Lighter alternative to nested class for type aliases |
Verification Usage — Where Nested Classes Earn Their Place in Real Testbenches
Nested classes are a sharp tool that solves a specific problem: keeping the global namespace clean and the relationship between a helper and its owner unambiguous. Three patterns recur across production UVM environments.
State enums and bookkeeping types scoped to one class
A driver's internal state machine, a sequence's phase enum, a monitor's error codes — all of these have meaning only inside their owning class. Defining them as nested typedef enum makes the relationship explicit and prevents pollution of the global namespace.
<code>class apb_driver extends uvm_driver #(apb_xact);
typedef enum {S_IDLE, S_SETUP, S_ACCESS, S_DONE} state_e;
typedef enum {E_NONE, E_TIMEOUT, E_PSLVERR} error_e;
state_e state;
error_e last_error;
endclass
// External code refers via the qualified name:
apb_driver::state_e s = apb_driver::S_IDLE;</code>Private helper classes used by exactly one outer class
A scoreboard might need a small txn_record helper class to store one expected transaction plus metadata (timestamp, source agent, prediction confidence). If nothing else in the testbench ever uses txn_record, nesting it inside the scoreboard prevents misuse and makes the ownership clear at a glance.
<code>class my_scoreboard extends uvm_scoreboard;
// Private bookkeeping type, used by no one else
class txn_record;
apb_xact xact;
time received_at;
int source_agent_id;
function new(apb_xact x, int aid);
xact = x;
received_at = $time;
source_agent_id = aid;
endfunction
endclass
txn_record expected[$];
endclass</code>Coverage-collection helpers that mirror one transactor's view
A monitor that owns a covergroup often also owns a small helper class that pre-processes sampled values before calling cg.sample(). Nesting the helper inside the monitor keeps the per-bus-protocol coverage logic encapsulated — different protocols can each have their own scoped helper without any naming collisions.
Simulation Behavior — What the Simulator Does With Nested Classes
Nesting is purely a compile-time naming convention
The simulator does not store nested classes any differently from top-level classes — at runtime they are ordinary objects with ordinary vtables. The only difference is at compile time: the nested class's fully-qualified name is outer::inner, and all access-control rules are checked relative to that scope.
No implicit pointer to an enclosing instance
A nested class in SystemVerilog is more like a C++ nested class than a Java inner class — it has no hidden field pointing to "the outer object that created me." If the nested class needs to access outer-instance state, the outer must pass this explicitly to the nested constructor and the nested must store it as an ordinary field.
Static members of nested classes are normal static
A static int declared inside a nested class has exactly one storage cell, addressed as outer::inner::field_name. The nesting affects the qualified name only — the underlying lifetime and sharing semantics are identical to a top-level class's static fields.
Construction follows ordinary scoping rules
You can construct an instance of a nested class anywhere the qualified name is visible: outer::inner h = new();. Inside the outer class's own methods you may write just inner h = new();. The qualified form is required from any other scope.
<code>class scoreboard;
class record;
apb_xact x;
time t;
endclass
record entries[$];
function void log(apb_xact xact);
record r = new();
r.x = xact; r.t = $time;
entries.push_back(r);
endfunction
endclass
// External: scoreboard::record is the qualified name
// Top-level namespace is uncluttered</code><code>class record; // floating helper
apb_xact x;
time t;
endclass
class scoreboard;
record entries[$];
function void log(apb_xact xact);
record r = new();
r.x = xact; r.t = $time;
entries.push_back(r);
endfunction
endclass
// 'record' lives in the global namespace —
// likely to collide with another component's
// 'record' helper.</code>Waveform Analysis — Tracing Nested Class Activity in Debug
Nested classes leave no special waveform signature — they're ordinary classes. The only debug-time consideration is that $typename() on a nested-class handle returns the qualified name, which makes ownership immediately readable.
Type identification by qualified name
<code>class scoreboard;
class record;
apb_xact x;
time t;
endclass
record entries[$];
function void show();
foreach (entries[i])
$display("[%0t] entry %s @ %0t xact=%s",
$time, $typename(entries[i]), entries[i].t,
$typename(entries[i].x));
endfunction
endclass
// Log output:
// 100ns entry scoreboard::record @ 50 xact=apb_xact
// 150ns entry scoreboard::record @ 95 xact=apb_xact
// 200ns entry scoreboard::record @ 145 xact=apb_xact</code>ASCII view — qualified types in mixed-component log
<code>100ns scoreboard::record created @ 50 xact=apb_xact id=1
150ns monitor::sample fired @ 95 xact=apb_xact id=2
200ns scoreboard::record created @ 145 xact=apb_xact id=2
↑ ↑
nested helper method on outer
instantly identifies its parent class</code>Industry Insights — Hard-Won Lessons About Nested Classes
Debugging Academy — Five Real Bugs From Nested Class Usage
"Nested class can't see outer instance fields"
DEBUGCode inside a nested class references an outer instance field and the compiler rejects it with "undeclared identifier 'name'".
<code>class scoreboard;
string name = "sb";
class record;
function void show();
$display("from %s", name); // BUG: no implicit outer-instance access
endfunction
endclass
endclass</code>SystemVerilog nested classes have no implicit pointer to an enclosing instance. The nested class can see the outer class's type declarations and static members, but it cannot reach instance fields of the outer without an explicit handle.
Pass a handle to the outer through the nested constructor: class record; scoreboard m_parent; function new(scoreboard p); m_parent = p; endfunction function void show(); $display("from %s", m_parent.name); endfunction endclass.
"External code can't find the nested type — 'record' is not declared"
DEBUGCode outside the outer class references the nested type by its short name and gets "undeclared identifier 'record'".
<code>class scoreboard;
class record;
apb_xact x;
endclass
endclass
module tb;
initial begin
record r = new(); // BUG: needs qualified name
end
endmodule</code>Outside the enclosing class, the nested type is only visible by its fully-qualified name. The bare name record is meaningless in the module scope.
Always use the qualified name from external scopes: scoreboard::record r = new();. If you find yourself reaching for the type often from outside, that's a signal the type should be promoted to top-level.
"Nested class can't access local fields of its outer"
DEBUGA nested class is given an outer handle and tries to read an outer field declared local — compile error: "cannot access local member".
<code>class scoreboard;
local int secret_count = 0; // outer-private
class record;
scoreboard m_parent;
function new(scoreboard p); m_parent = p; endfunction
function void show();
$display("count=%0d", m_parent.secret_count); // ILLEGAL
endfunction
endclass
endclass</code>A nested class is not a friend of its outer in SystemVerilog. It has no special access to local members of the outer — only to public ones (and to outer protected members through inheritance, which is not what nesting provides).
Either (a) widen the outer field's access to protected or expose a getter, or (b) provide an accessor method on the outer that the nested class calls: function int get_secret_count(); return secret_count; endfunction.
"Forward reference fails — nested A needs nested B which is declared later"
DEBUGA nested class A holds a handle to nested class B, both inside the same outer. Compile error: "undeclared type 'B'".
<code>class scoreboard;
class A;
B b; // BUG: B not yet declared
endclass
class B;
int val;
endclass
endclass</code>Declaration order matters within an outer class — at the point where A references B, B has not yet been parsed.
Use a forward declaration with typedef class at the top of the outer: typedef class B; class A; B b; endclass class B; int val; endclass. The forward declaration tells the compiler that B is a class type that will be fully defined later.
"Nested static counter behaves per-outer-instance, not class-wide as expected"
DEBUGA nested class's static int count is expected to be a single shared counter, but each outer instance appears to have its own counter starting from zero.
<code>class outer;
class inner;
static int count = 0; // expected to be class-wide
function new();
count++;
endfunction
endclass
endclass
// In test: two outer instances each create an inner.
// Expectation: outer::inner::count == 2
// Reality: outer::inner::count == 2 ← actually correct!
// But user observation: "each outer's view shows 1"</code>Actually the static is class-wide — there is exactly one outer::inner::count for the whole simulation. The misobservation typically comes from looking at the counter through different outer instances and confusing oneself. The static is correctly singular.
Always reference statics through the qualified name (outer::inner::count) rather than through a particular instance, even though both forms work. The qualified form makes the "one shared cell" semantics visually obvious and prevents the kind of cognitive confusion that produces a non-existent bug report.
Interview Q&A — Twelve Questions You Will Be Asked
A class declared inside the body of another class. The nested class's fully-qualified name includes the outer's name (outer::inner), and it is referenced that way from outside the outer's scope. Nesting is purely a compile-time organisational tool — at runtime, nested classes behave exactly like top-level classes.
Best Practices — Ten Rules for Using Nested Classes Well
- Nest a helper class only if it is used by exactly one outer class. If two or more outer classes reach for it, promote the helper to top-level.
- Always prefer nested
typedef enumover global enums for class-local state machines. The qualified name (my_driver::S_IDLE) makes ownership self-documenting. - Pass an outer-instance handle to the nested constructor when the nested class needs outer-instance state. SystemVerilog does not provide automatic enclosing-instance access — the link must be explicit.
- Do not assume nested classes are "friends" of their outer. They cannot reach the outer's
localmembers; expose accessors or widen access if needed. - Reference nested classes from external code by qualified name only.
outer::inneris the contract; the short name is invisible from outside. - Use
typedef class X;for forward declarations when nested classes reference each other. The same mechanism that works for top-level forward declarations works for nested ones. - Keep nested classes small. Once a nested class grows beyond ~50 lines, it usually deserves its own file — file boundaries are good thinking-units even when the namespace boundary isn't.
- Treat nested static members as ordinary statics. Address them through the qualified name (
outer::inner::count); avoid accessing through an outer instance to keep the "one shared cell" semantics visible. - Don't nest a class purely to "make it private." SystemVerilog has no
privateequivalent for classes; nesting hides the name from the global scope but does not prevent any code that can write the qualified name from using it. - Print
$typename(this)in nested-class methods during bring-up. The qualified name in log lines instantly identifies the owning component — one of the under-appreciated debug wins of nesting.