addrpropertydatapropertyformat()method
SystemVerilog OOP interview preparation
Everything you need to refresh about SystemVerilog object-oriented programming: classes, constructors, inheritance, polymorphism, virtual methods, $cast(), copying, and reusable design verification patterns before a DV interview.
SystemVerilog class model and visibilityKnow what the handle actually owns.
Start with the SystemVerilog object model, the four OOP ideas, and the visibility rules that keep verification classes reusable without exposing mutable internals.
A class defines behavior; handles reach objects
addr = 10addr = 42addr = 91unqualifiedclasschildoutsideprotectedclasschildoutsidelocalclasschildoutsideThe class is one type definition. Each new allocation creates a distinct object, while handle assignment can make multiple names refer to the same object.
A class is a type definition. An object is one allocated instance of that class. A class variable stores a handle that is either null or refers to an object; the handle is not the object itself.
Reason it through
- Declaring Packet p creates a handle initialized to null. Executing p = new allocates a Packet object and places its handle in p.
- Two different calls to new create two independent objects. Assigning one handle to another creates an alias to the same object.
- Methods execute against the object reached through the handle, so a null-handle dereference is a runtime error.
class Packet;
int id;
endclass
Packet p; // null handle
Packet alias_p;
p = new; // allocate one Packet object
p.id = 7;
alias_p = p; // same object, second handle
alias_p.id = 9; // p.id is now 9Encapsulation controls access to state, abstraction exposes the behavior callers need, inheritance extends an existing type, and polymorphism lets one base-typed call select behavior from the actual derived object.
Reason it through
- Encapsulation is the boundary around representation and invariants; visibility qualifiers are one implementation tool.
- Abstraction is about the public contract. A
virtual classor pure virtual method can make that contract explicit. - SystemVerilog supports single class inheritance through extends. Polymorphism depends on virtual methods, not inheritance alone.
| Principle | Question it answers | SystemVerilog mechanism |
|---|---|---|
| Encapsulation | Who may touch this state? | public, protected, local |
| Abstraction | What behavior is promised? | base API, virtual class |
| Inheritance | What definition is extended? | extends and super |
| Polymorphism | Which implementation runs? | virtual method dispatch |
Public members are visible wherever the object is visible. Protected members are visible inside the declaring class and its derived classes. Local members are visible only inside the declaring class.
Reason it through
- Public access is the default when no visibility qualifier is written; ordinary public members are left unqualified in SystemVerilog source.
- SystemVerilog uses
localfor the private-style visibility level; private is not the class-member keyword. - Prefer
protectedstate when derived verification classes need controlled access, and public methods when outside callers need a stable interface.
class Base;
int visible_everywhere; // public by default
protected int visible_to_children;
local int visible_only_here;
endclass
class Child extends Base;
function void configure();
visible_everywhere = 1; // legal
visible_to_children = 2; // legal
// visible_only_here = 3; // illegal
endfunction
endclassNo. A class cannot declare multiple methods with the same name and different argument lists the way C++ or Java can. Use distinct names, default arguments, or a typed wrapper instead.
Reason it through
- Overloading chooses among same-named signatures at compile time. That language feature is not available for SystemVerilog class methods.
- Overriding is different: a derived class supplies the same signature as a virtual base method.
- Default arguments can cover closely related call forms when one return type and one implementation are appropriate.
class MathOps;
function int add_int(int a, int b);
return a + b;
endfunction
function real add_real(real a, real b);
return a + b;
endfunction
endclassA method boundary lets the class preserve invariants, validate updates, and change its representation without forcing every caller to change.
Reason it through
- A setter can reject an illegal value or update related fields atomically.
- A getter can expose a stable view without giving an outside component permission to corrupt internal state.
- In a verification environment, a scoreboard should observe driver state through a deliberate interface rather than mutate the driver's internals.
SystemVerilog construction and specializationInitialize the hierarchy in order.
Reason about new(), base-constructor chaining, this and super, parameterized class types, and state shared at class scope.
Construction flows from base state to specialization
super.new(args)Base constructorestablish inherited statethis.tag = tagDerived constructorfinish specialized statePacket #(WIDTH, T)WIDTH = 64value parameterT = bit[15:0]type parameterPacket #(64, bit[15:0])A derived constructor establishes inherited state before its own fields. Value and type parameters specialize the class at compile time rather than at object creation.
new allocates an object and runs that class's constructor function to establish valid initial state. The constructor may accept arguments and assign instance properties, but it cannot consume simulation time.
Reason it through
- The constructor is declared as function new with no explicit return type.
- Use this.member when a formal argument has the same name as a property.
- Default property initialization happens before constructor statements execute.
class Packet;
int id;
string kind;
function new(int id = 0, string kind = "READ");
this.id = id;
this.kind = kind;
endfunction
endclass
Packet p = new(7, "WRITE");The base constructor always runs before the derived constructor body. If the base constructor needs arguments, the derived constructor must call super.new(...) as its first executable statement.
Reason it through
- When the base constructor has no required arguments, the compiler can insert an implicit super.new call.
- Explicitly calling super.new makes required base-state initialization visible in review.
- Construction proceeds from the oldest base class toward the most-derived class.
class BasePacket;
int id;
function new(int id);
this.id = id;
endfunction
endclass
class TaggedPacket extends BasePacket;
string tag;
function new(int id, string tag);
super.new(id);
this.tag = tag;
endfunction
endclassthis is the handle to the current object. super is a lexical keyword that selects the immediate base-class implementation or constructor; it is not a handle that can be stored.
Reason it through
- Use this.field to disambiguate an instance property from a same-named argument.
- Use super.method() to call the immediate base implementation from an override.
- Use
super.new(...)only in a constructor and with the arguments required by the immediate base.
A parameterized class is a family of class types. Value parameters control constants such as width, while type parameters substitute a data type; each distinct parameter tuple is a distinct specialized type.
Reason it through
- Defaults let callers use Packet #() while named overrides document specializations clearly.
- Fields and method signatures can derive from both the value and type parameters. If T is declared rand, callers must choose a type the randomization model supports.
- Parameter selection happens in the type declaration, not as arguments to new.
class Packet #(
parameter int WIDTH = 32,
type T = int
);
rand bit [WIDTH-1:0] data;
T id;
endclass
Packet #() default_packet = new;
Packet #(.WIDTH(64), .T(bit [15:0])) wide_packet = new;A static property has one value shared by every object of that class type. A static method belongs to class scope, cannot use per-object state without an explicit handle, and is normally called with ClassName::method().
Reason it through
- Use a static counter for type-wide statistics, instance numbering, registries, or one shared service handle.
- The class-scope operator makes the owning type explicit and does not require an object.
- A derived class that declares a same-named static member creates a separate hidden member; it does not override the base static.
class Counter;
static int total = 0;
int serial;
function new();
total++;
serial = total;
endfunction
static function int get_total();
return total;
endfunction
endclass
int count = Counter::get_total();SystemVerilog inheritance and dispatchSeparate handle type from object type.
Trace virtual calls, non-virtual hiding, super calls, static method resolution, and the constructor-time dispatch trap without guessing from the variable name.
The virtual keyword changes the lookup path
Animal awhat the compiler seesDogwhat was allocatedDog::speak()woofRuntime dispatchThe base declaration marked the method virtual, so the object's dynamic type selects the implementation.
Virtual calls follow the dynamic object type. Non-virtual and static calls are bound from the declared type or class scope, even when the handle refers to a derived object.
When the base declaration is virtual, a call through any compatible handle dispatches from the dynamic type of the object. A base handle referring to a Dog therefore invokes Dog::speak().
Reason it through
- The handle's declared type controls which members are legal to name at compile time.
- The object's dynamic type controls which virtual override executes at runtime.
- Once a method is virtual in a base class, matching overrides remain virtual down the hierarchy.
class Animal;
virtual function string speak();
return "noise";
endfunction
endclass
class Dog extends Animal;
function string speak();
return "woof";
endfunction
endclass
Dog dog = new;
Animal animal = dog;
$display("%s", animal.speak()); // woofA derived method overrides a matching virtual base method and participates in runtime dispatch. A same-named method whose base declaration is non-virtual only hides that name; calls remain bound by the handle's declared type.
Reason it through
- Derived d = new; d.who() can call
Derived::whoeven when the method is non-virtual because d is declared Derived. - Base b = d; b.who() calls
Base::whowhen the base declaration is non-virtual. - Make the base method virtual and preserve the signature when polymorphic substitution is required.
| Base declaration | Call through Base handle | Call through Derived handle |
|---|---|---|
| virtual | Derived override | Derived override |
| non-virtual | Base method | Derived hidden method |
super.method() invokes the immediate superclass implementation directly from the derived method. It is useful for extending base behavior rather than replacing it completely.
Reason it through
- In
C::vf, super.vf() namesB::vfwhen C extends B. - If
B::vfalso calls super.vf(), that nested call reachesA::vf. - The call is selected lexically from the class hierarchy, not by searching the runtime object again.
class A;
virtual function string name(); return "A"; endfunction
endclass
class B extends A;
function string name();
return {"B(", super.name(), ")"};
endfunction
endclass
class C extends B;
function string name();
return {"C(", super.name(), ")"};
endfunction
endclass
// C object prints C(B(A))Static members belong to a class type, not an object. A derived same-named static member shadows a separate base member, and a call is resolved from class scope or the handle's declared type rather than the dynamic object type.
Reason it through
Base::inc()updatesBase::cntandDer::inc()updatesDer::cnt.- A Base handle referring to a Der object still resolves a static call as Base.
- In the deck's sequence,
Base::get()is 3,Der::get()is 120, and hb.get()resolves to the base value 3.
class Base;
static int cnt = 0;
static function void inc(); cnt++; endfunction
static function int get(); return cnt; endfunction
endclass
class Der extends Base;
static int cnt = 100;
static function void inc(); cnt += 10; endfunction
static function int get(); return cnt; endfunction
endclass
Base b = new;
Der d = new;
Base hb = d;
Base::inc(); Der::inc();
b.inc(); d.inc(); hb.inc();
// Base::get() = 3, Der::get() = 120, hb.get() = 3Virtual dispatch can enter the derived override while the base constructor is still running, before the derived constructor has initialized its fields. The override observes default or partially initialized derived state.
Reason it through
- If BaseB.new calls
build()and the object is DerB,DerB::buildcan run before DerB.new assigns ready = 1. - The printed ready value is therefore 0, the default for int, rather than the intended initialized value.
- Keep constructors non-polymorphic and call a separate build or configure phase only after construction completes.
class DerB extends BaseB;
int ready;
function new();
super.new();
ready = 1;
endfunction
function void build();
$display("ready=%0d", ready);
endfunction
endclass
DerB d = new;
d.build(); // safe: construction is completeFor every call, write down the handle's declared type, the object's dynamic type, and whether the base declaration is virtual. Virtual calls follow the dynamic type; non-virtual calls follow the handle type; super calls follow the immediate base implementation.
Reason it through
- An A handle referring to C calls
C::vfwhen vf is virtual butA::nfwhen nf is non-virtual. - After a successful
$castto B, the B handle still refers to the same C object; a non-virtual nf call now binds toB::nf. - A C handle binds the same non-virtual call to
C::nf, while every compatible handle still dispatches virtual vf to C.
| Call kind | Lookup source | Object copied? |
|---|---|---|
| virtual instance method | Dynamic object type | No |
| non-virtual instance method | Declared handle type | No |
| static method | Class scope or handle type | No |
| super.method() | Immediate superclass implementation | No |
SystemVerilog abstract types and castingProgram to a contract you can check.
Use virtual classes and pure virtual methods to define incomplete base types, then move through the hierarchy with safe upcasts and checked downcasts.
Abstract bases define the contract boundary
virtual class Shapepure virtual function area()Rectarea() implementedCirclearea() implementedRect handle→Shape handleupcast · always safeShape handle→Rect handle$cast() · runtime checkA concrete child must satisfy every inherited pure virtual contract. Upcasting preserves the object behind a base handle; $cast() verifies a downcast at runtime.
A virtual class is an abstract class type that cannot be instantiated directly. A virtual method is an instance method that supports runtime overriding; a concrete class may contain virtual methods and still be instantiable.
Reason it through
- A
virtual classmay contain implemented properties and ordinary or virtual methods. - Declaring a class virtual says the type itself is incomplete, even when it has no pure method.
- Declaring a method virtual changes dispatch for that method but does not make the class abstract.
| Construct | Main effect | Can enclosing class be instantiated? |
|---|---|---|
virtual class | Type is abstract | No |
| virtual method | Call dispatches dynamically | Yes, if class is concrete |
| pure virtual method | Derived concrete type must implement it | Only legal in an abstract class |
A pure virtual method declares a signature without an implementation. Every non-virtual derived class must provide a matching implementation or the derived class must remain virtual.
Reason it through
- The containing class must be declared virtual.
- A virtual intermediate class may defer the implementation to a later concrete child.
- Pure methods are useful for interface-style contracts such as
format(), send_pkt(), or area().
virtual class Shape;
pure virtual function int area();
endclass
class Rect extends Shape;
int w, h;
function new(int w = 1, int h = 1);
this.w = w;
this.h = h;
endfunction
function int area();
return w * h;
endfunction
endclassEvery derived object satisfies its base type, so assigning a derived handle to a base handle is safe. A base handle may refer to many possible dynamic types, so $cast() must verify a downcast at runtime.
Reason it through
- An upcast changes only the handle view; the same object remains allocated.
$cast(destination, source)returns 1 and assigns the handle when the dynamic object is compatible.- A failed cast returns 0 and should take an explicit error or alternate path.
Rect original = new(3, 4);
Shape base = original; // safe upcast
Rect recovered;
if ($cast(recovered, base))
$display("area=%0d", recovered.area());
else
$display("incompatible dynamic type");The derived method must preserve the base method's name and compatible prototype, including return type and formal argument contract. It then remains virtual even if the keyword is omitted in the derived declaration.
Reason it through
- Changing the argument list does not create a useful overload; SystemVerilog method overloading is not supported.
- Changing only the implementation is the normal override.
- Use a differently named helper when the derived class needs extra inputs beyond the base contract.
Yes. A virtual class cannot be constructed directly, but its handle may refer to any compatible concrete derived object and invoke the abstract contract polymorphically.
Reason it through
- Shape s; is legal because it declares only a handle.
- Rect r = new(3, 4); s = r; is a safe upcast.
- Calling s.area() dispatches to
Rect::areaand returns 12.
SystemVerilog copying and ownershipDraw the handle graph before copying.
Distinguish handle assignment, built-in shallow object copy, explicit deep copy, shared configuration, and object lifetime so nested state cannot alias by accident.
Copy values deliberately; do not copy aliases blindly
p1id = 7H1v = 100P1[1] [2] [3]p2id = 7H1v = 100P1[1] [2] [3]same object IDs repeatH1 · P1 are aliased
Alias riskScalar fields are copied, but nested class handles still reach the same objects.
A shallow copy duplicates scalar members but preserves nested handles. A deep copy allocates every owned child object and then copies its value.
Handle assignment copies only the reference, so both variables name the same object. Object copying creates a second outer object, but the built-in copy is shallow for nested class handles.
Reason it through
- b = a aliases the complete object reached by a.
- b = new a allocates a second outer object and copies each member value.
- If a member value is itself a class handle, both outer objects still refer to the same nested object after a shallow copy.
| Operation | New outer object | Nested handles |
|---|---|---|
| b = a | No | Everything is shared |
| b = new a | Yes | Still shared |
b = a.deep_clone() | Yes | New objects when implemented correctly |
The copy form new rhs allocates a new object of the destination class and copies member values from rhs without running the normal user constructor. Nested object members are handles, so those handles are copied rather than the nested objects.
Reason it through
- Scalar and packed members become independent values in the new outer object.
- Queues or arrays of class handles are new containers whose element handles still point to the original child objects.
- Use this syntax only when shallow semantics are deliberate and documented.
class Field;
int v;
endclass
class Packet;
int id;
Field hdr;
endclass
Packet p1 = new;
p1.hdr = new;
p1.hdr.v = 1;
Packet p2 = new p1; // shallow object copy
p2.hdr.v = 9; // p1.hdr.v is also 9Allocate a new outer object, copy scalar values, allocate a new object for every owned nested handle, recursively copy each nested value, and define how null or shared references should be treated.
Reason it through
- The deep-copy policy must follow ownership. Shared services may intentionally remain shared, while packet-owned headers and payload elements should be cloned.
- Clear constructor-created container contents before rebuilding them when the constructor adds defaults.
- Test independence by mutating the original after cloning and confirming the clone does not change.
function Packet deep_clone();
Packet clone = new(this.id);
if (this.hdr != null)
clone.hdr = new(this.hdr.v);
clone.payload.delete();
foreach (this.payload[i])
clone.payload.push_back(new(this.payload[i].v));
return clone;
endfunctionThe queue copy duplicates the sequence of handle values, not the objects those handles reference. Corresponding elements in both queues therefore point to the same child objects.
Reason it through
- Deleting or resizing one queue changes only that container.
- Mutating an object reached through either queue is visible through the matching handle in the other queue.
- A deep copy must loop over the source queue, allocate each child, and copy its data.
No. SystemVerilog class objects are garbage collected after no live handles reference them, and a user method named delete() has no special destructor semantics.
Reason it through
- Setting a handle to null only removes that reference; the object remains reachable if another handle still points to it.
- Use explicit close(), stop(), or cleanup methods for resources or protocol actions that must happen deterministically.
- Container delete methods remove elements from a queue, dynamic array, or associative array; they are not class destructors.
SystemVerilog OOP in design verificationKeep components stable as transactions evolve.
Apply polymorphic transactions, factories, singletons, and inherited constraints to verification architectures without turning every extension into a component rewrite.
Base handles let new packet types reuse old components
read_packetwrite_packetpacket handle→pkt.display()pkt.format()base_c + child_cboth active · intersectbase_c → base_cderived block replaces baseDrivers and monitors can accept a base transaction handle while virtual methods preserve type-specific behavior. Constraint block names decide whether inherited rules layer or replace.
Verification code models many related transactions, policies, and components that evolve faster than the RTL interface. OOP lets common structure live in base types while derived classes add scenario-specific fields and behavior without rewriting generic infrastructure.
Reason it through
- Encapsulation keeps protocol invariants next to the data and operations they protect.
- Inheritance and parameterization reuse common transaction or component structure.
- Polymorphism lets a driver, monitor, or scoreboard work through a stable base contract as new derived types are added.
Accept a base packet handle and call virtual methods declared on the base. A read_packet or write_packet object then supplies its own override through runtime dispatch.
Reason it through
- The component depends only on the stable packet contract, not every concrete subtype.
- Adding a new derived packet does not require an if-
$castchain inside the driver. - Shared fields remain available through the base handle while type-specific formatting or processing stays in the override.
class packet;
virtual function void display();
$display("BASE PACKET");
endfunction
endclass
class read_packet extends packet;
function void display();
$display("READ PACKET");
endfunction
endclass
class driver;
task drive(packet pkt);
pkt.display();
endtask
endclassA factory separates the request for a base contract from the decision about which concrete class to construct. Callers remain written against the base type while configuration selects a compatible derived implementation.
Reason it through
- The factory centralizes construction policy instead of scattering type-selection conditionals.
- The returned base handle still dispatches virtual behavior from the concrete object.
- A UVM factory adds registered type and instance overrides, but the underlying OOP idea is the same.
virtual class Driver;
pure virtual function void send(string message);
endclass
class UartDriver extends Driver;
function void send(string message);
$display("UART: %s", message);
endfunction
endclass
class DriverFactory;
static function Driver create(string kind);
UartDriver uart;
if (kind == "uart") begin
uart = new;
return uart;
end
return null;
endfunction
endclassA singleton provides one globally shared instance through a static accessor, which can suit a registry or process-wide service. Its risk is hidden global mutable state that couples tests and complicates reset, ordering, and parallel execution.
Reason it through
- Store the instance in a static
localproperty and protect construction so callers useget(). - Create lazily when get first sees null, or initialize deterministically at a known startup point.
- Document how the singleton is reset between tests; many services are better passed explicitly.
class Logger;
local static Logger instance;
local function new();
endfunction
static function Logger get();
if (instance == null)
instance = new;
return instance;
endfunction
endclassInherited constraint blocks with different names are all active and their legal sets intersect. A derived constraint block with the same name replaces the inherited block of that name.
Reason it through
- With base x inside [0:5] and derived x inside [4:9] in differently named blocks, x is limited to 4 or 5.
constraint_mode(0)can disable a named active block on an object at runtime.- Reusing a base block name is an override, not an additional intersection; choose names deliberately.
class BaseC;
rand int x;
constraint c_base { x inside {[0:5]}; }
endclass
class DerivedC extends BaseC;
constraint c_derived { x inside {[4:9]}; }
endclass
DerivedC d = new;
assert(d.randomize()); // x is 4 or 5
d.c_base.constraint_mode(0);
assert(d.randomize()); // x may be 4 through 9Check the return value from randomize() and $cast(). A failed randomization can leave stale field values, while a failed downcast leaves the destination handle unchanged and unusable as the requested derived type.
Reason it through
- Use assert(object.
randomize()) or an explicit failure branch that reports enough context to debug the active constraint set. - Use if (
$cast(derived, base)) before accessing derived-only members, or assert the cast when failure is a testbench error. - Treat both operations as runtime decisions, not declarations that the solver or object hierarchy must accept.
BasePacket base;
WritePacket write;
assert(base.randomize())
else $fatal(1, "packet randomization failed");
if (!$cast(write, base))
$fatal(1, "expected a WritePacket");Keep base contracts small, make ownership explicit, prefer composition unless a true substitutable subtype exists, use virtual methods only at intentional extension points, and keep construction separate from runtime work.
Reason it through
- A derived type should satisfy every assumption made by code that accepts the base type.
- Document which component allocates, mutates, clones, and releases each object handle.
- Use factories where construction policy must vary, not as a replacement for clear dependencies.
| Concern | Preferred practice | Failure signal |
|---|---|---|
| Subtype design | Small substitutable base contract | Frequent casts or type switches |
| Ownership | One documented owner or immutable sharing | Mutation visible in unrelated components |
| Construction | Factory or explicit new at a clear boundary | Concrete types scattered through callers |
| Lifecycle | Explicit phases and cleanup methods | Work starts from partially built objects |
