SystemVerilog · Module 9
Constructors & new()
Custom constructors, default argument values, explicit vs implicit initialization, and the rules that govern construction across an inheritance chain.
Module 9 · Page 9.4
What Is a Constructor?
A constructor is a special function that runs automatically the moment you call new(). It is your one guaranteed chance to set up an object before anyone else touches it — initialise values, connect interfaces, set IDs, allocate sub-objects.
In SystemVerilog, the constructor is always named function new(). You can only have one per class. It has no return type — not even void. That is the compiler's way of knowing it is special.
class BusTxn;
bit [31:0] addr;
bit [31:0] data;
int txn_id;
// ── Constructor ───────────────────────────────────────────
// keyword name (no return type — not even void)
function new();
addr = 32'h0; // safe starting values
data = 32'h0;
txn_id = -1; // -1 = "not assigned yet"
endfunction
endclass
// The constructor runs here — you never call it by name
BusTxn t = new();
// t.addr == 0, t.data == 0, t.txn_id == -1What Happens When new() Is Called — Step by Step
When the simulator sees t = new(), it does not just jump into your constructor. There is a precise sequence. Understanding this sequence will save you hours of debugging later, especially once you add inheritance.
1Memory is allocated
The simulator reserves exactly enough memory for the object — one slot for every property declared in the class. At this point all bits are uninitialised (or zero-initialised depending on the simulator).
2Inline initialisers run
Any property with an inline initial value — like int count = 0; — is applied first, before the constructor body runs. Think of these as the base layer.
3If a parent class exists, super.new() is called
For inherited classes, the parent's constructor must run before the child's body. This is enforced — if you write your own constructor in a child class, the very first statement must be super.new(). More on this in Page 9.9.
4Your constructor body runs
Now your function new() body executes. Arguments passed to new() are available here. You can override inline defaults, set up relationships between properties, or call other functions.
5The handle is returned to the caller
The new() expression evaluates to a reference (handle) pointing to the newly constructed object. This is stored in whatever variable is on the left side of the assignment.
The Default Constructor — When You Don't Write One
If you declare a class and write no constructor at all, SystemVerilog quietly generates a default one for you. This default constructor takes no arguments and initialises every property to its type's default value: 0 for integers and bit types, null for class handles, empty string for string, and X for 4-state logic.
class SimplePacket;
bit [7:0] data; // default: 8'h00
int count; // default: 0
string label; // default: "" (empty string)
logic [3:0] status; // default: 4'bXXXX (4-state X)
// No constructor written — the default one handles it
endclass
SimplePacket p = new();
// p.data == 8'h00
// p.count == 0
// p.label == ""
// p.status == 4'bXXXX ← be careful with 4-state logic defaultsThe default constructor is fine for simple transaction classes where zero is a meaningful starting state. Write your own constructor the moment you need non-zero defaults, constructor arguments, or sub-object creation.
Writing Your Own Constructor
The moment your class needs anything beyond zero-initialised properties — a name, an ID, a connection to an interface, a sub-object — you write your own constructor.
class UartTransaction;
rand bit [7:0] payload;
rand bit parity_bit;
int txn_id;
string tag;
int baud_rate;
// Constructor accepts an ID and baud rate
function new(int id, int baud = 115200);
txn_id = id;
baud_rate = baud;
tag = $sformatf("uart_txn_%0d", id);
payload = 8'h00;
parity_bit = 0;
endfunction
function void display();
$display("[%s] payload=0x%02h parity=%0b baud=%0d",
tag, payload, parity_bit, baud_rate);
endfunction
endclass
// Using the constructor
UartTransaction t1 = new(1); // id=1, baud=115200 (default)
UartTransaction t2 = new(2, 9600); // id=2, baud=9600
UartTransaction t3 = new(3, 460800); // id=3, baud=460800
t1.display(); // [uart_txn_1] payload=0x00 parity=0 baud=115200
t2.display(); // [uart_txn_2] payload=0x00 parity=0 baud=9600Notice how tag is built from id using $sformatf right inside the constructor. This is perfectly fine — any function call is allowed inside a constructor, as long as it does not consume simulation time.
Default Argument Values — Keeping Call Sites Clean
Constructor arguments can have default values. If the caller does not pass a value for that argument, the default is used automatically. This lets one constructor handle multiple common cases without forcing every call site to provide all arguments.
class I2cTransaction;
rand bit [6:0] slave_addr;
rand bit [7:0] data;
bit read_write; // 0=write, 1=read
int freq_khz;
string name;
// All arguments have defaults — every call style below is legal
function new(
string n = "i2c_txn",
int freq = 400, // 400 kHz Fast-mode
bit rw = 0 // write by default
);
name = n;
freq_khz = freq;
read_write = rw;
endfunction
endclass
// Every one of these is valid:
I2cTransaction a = new(); // all defaults
I2cTransaction b = new("cfg_write"); // custom name only
I2cTransaction c = new("fast_plus", 1000); // name + freq
I2cTransaction d = new("read_back", 400, 1); // all threeUsing this Inside a Constructor
this is a reference to the object currently being constructed. It becomes essential when a constructor argument has the same name as a class property — without this, the compiler cannot tell which one you mean.
class CanFrame;
bit [10:0] id; // class property named 'id'
bit [7:0] data;
string name;
// Argument also named 'id' — same name as the property above
function new(bit [10:0] id, string name);
this.id = id; // this.id = property
this.name = name; // id / name = argument
endfunction
endclass
// Without 'this', the assignment would be id = id — no-op!
// this.id = id makes it unambiguous.
CanFrame f = new(11'h7FF, "broadcast");this is covered fully in Page 9.6 — The this Keyword. For now, just remember: use it in constructors whenever your argument names match your property names.
Calling new() Again — What Actually Happens
You can call new() on a handle that already points to an object. It does not reinitialise the existing object. Instead, it creates a brand new object and makes the handle point to it. The original object, if nothing else references it, becomes eligible for garbage collection.
BusTxn t = new(); // Object A created, t → A
t.txn_id = 10;
BusTxn saved = t; // saved also points to A
t = new(); // Object B created, t → B
// Object A still exists! saved still points to A.
// This did NOT reset Object A.
$display("t.txn_id = %0d", t.txn_id); // -1 (fresh object B)
$display("saved.txn_id = %0d", saved.txn_id); // 10 (still Object A)What You Cannot Do in a Constructor
Constructors are functions — not tasks. That single distinction has practical consequences. Here is a quick reference so you do not hit compile errors mid-project.
| Action | Allowed? | Reason / Workaround |
|---|---|---|
| Set property values | ✅ Yes | Primary purpose of a constructor |
Call other functions | ✅ Yes | Any function is fine — no time consumption |
Call $display | ✅ Yes | Useful for debug — just remember it fires on every new() |
Call a task | ❌ No | Functions cannot call tasks. Move such logic to an init() task called after construction |
Use #delay or @event | ❌ No | Constructor is a function — no simulation time allowed |
Call randomize() | ✅ Yes | Useful for auto-randomising on creation |
Create sub-objects with new() | ✅ Yes | Common pattern — build nested objects in the constructor |
Use return | ❌ No | A constructor has no return value — omit it entirely |
super.new() — A Quick Preview
When a class inherits from another class (covered in Page 9.8 — Inheritance), the child class's constructor must call the parent's constructor first. This is done with super.new(), and it must be the very first statement in the child constructor body.
class BaseTxn;
int txn_id;
function new(int id);
txn_id = id;
endfunction
endclass
class WriteTransaction extends BaseTxn;
bit [31:0] wdata;
function new(int id, bit [31:0] data);
super.new(id); // ← MUST be first — initialises BaseTxn
wdata = data; // then set child-specific properties
endfunction
endclass
WriteTransaction wt = new(5, 32'hA0B1_C2D3);
// wt.txn_id == 5 (set by BaseTxn constructor via super.new)
// wt.wdata == 32'hA0B1_C2D3Full Working Example — SPI Transaction
Here is a complete SPI transaction class that puts everything from this page together: custom constructor, default arguments, this for disambiguation, sub-object creation, and a call to randomize() inside the constructor.
// ── Nested status object ──────────────────────────────────────
class SpiStatus;
bit busy;
bit overflow;
function new(); busy = 0; overflow = 0; endfunction
endclass
// ── Main SPI transaction ──────────────────────────────────────
class SpiTransaction;
rand bit [7:0] mosi_data; // master → slave
bit [7:0] miso_data; // slave → master (filled after drive)
rand bit [1:0] cpol_cpha; // SPI mode 0-3
int txn_id;
string name;
int clk_mhz;
SpiStatus status; // nested object
constraint c_mode { cpol_cpha inside {2'b00, 2'b11}; } // mode 0 or 3
// name matches property — use 'this' to disambiguate
function new(
int txn_id = 0,
string name = "spi_txn",
int clk_mhz = 10
);
this.txn_id = txn_id;
this.name = name;
this.clk_mhz = clk_mhz;
miso_data = 8'h00;
// Create the nested status object inside constructor
status = new();
// Auto-randomise on construction (optional pattern)
if (!this.randomize())
$fatal(1, "[%s] Constructor randomize failed", name);
endfunction
function void display();
$display("[%s #%0d] mosi=0x%02h mode=%02b clk=%0dMHz busy=%0b",
name, txn_id, mosi_data,
cpol_cpha, clk_mhz, status.busy);
endfunction
endclass
// ── Testbench ─────────────────────────────────────────────────
module tb;
initial begin
// All defaults
SpiTransaction t1 = new();
t1.display();
// Custom name and clock
SpiTransaction t2 = new(1, "flash_write", 50);
t2.display();
// Verify nested object was created
t2.status.busy = 1;
t2.display(); // busy=1
end
endmoduleCommon Constructor Mistakes
| Mistake | What goes wrong | Fix |
|---|---|---|
Using handle before new() | Null-handle crash at runtime | Always call new() before first use |
| Calling a task inside the constructor | Compile error — functions cannot call tasks | Move time-consuming setup to a separate init() task |
Forgetting super.new() in child class | Parent properties uninitialised — null crashes deep in the call chain | Make it the very first statement in every child constructor |
Name collision without this | Argument assigned to itself — property stays at default | Use this.property = argument |
| Re-using a handle across loop iterations | All queue entries point to same final object | Declare ClassName t = new() inside the loop body |
Adding a return statement | Compile error — constructors have no return | Remove the return entirely |
Verification Usage — Constructor Patterns in Production Testbenches
A well-written constructor is the difference between a transaction class that "just works" everywhere it's used and one that requires every caller to remember "did I initialise the mailbox? did I set the default ID? did I link the parent agent?" Production VIP follows a few disciplined patterns that scale from one-off prototypes to multi-million-line testbench codebases.
// ── Archetype 1: TRANSACTION — defaults + identity ─────────────
class axi_xact;
static int next_id; // shared counter
const int id; // locked at construction
rand bit [31:0] addr;
rand bit [63:0] data;
bit is_write;
function new(string default_mode = "READ");
id = next_id++;
is_write = (default_mode == "WRITE");
// addr/data are rand — left for randomize() to set
endfunction
endclass
// ── Archetype 2: COMPONENT — allocate nested dynamic types ────
class axi_driver;
virtual axi_if vif;
mailbox #(axi_xact) inbox;
int sent_count;
function new(virtual axi_if v);
vif = v;
inbox = new(0); // unbounded mailbox
sent_count = 0;
// Note: thread starts later, via a separate run() task
endfunction
task run();
forever begin
axi_xact tr;
inbox.get(tr);
tr.drive(vif);
sent_count++;
end
endtask
endclass
// ── Archetype 3: SINGLETON — lazy construction, one instance ──
class global_config;
local static global_config inst;
int timeout_ns;
int verbosity;
local function new();
timeout_ns = 10_000;
verbosity = 2;
endfunction
static function global_config get();
if (inst == null) inst = new();
return inst;
endfunction
endclass
// Usage: every caller gets the same object
// global_config::get().timeout_ns = 50_000;Simulation Behavior — Constructor Execution Order and Object Visibility
The simulator allocates an object's memory before the constructor runs, but the memory is uninitialised garbage until the constructor finishes. Understanding the allocation-then-construction-then-return sequence is the difference between code that works and code that randomly null-pointer-crashes in the middle of a long regression.
class traced;
int id;
mailbox #(int) inbox;
function new(int i);
$display("[%0t] new() ENTER: id arg = %0d", $time, i);
$display("[%0t] new() ENTER: this.id = %0d (uninit)", $time, this.id);
$display("[%0t] new() ENTER: this.inbox = %s",
$time, (this.inbox == null) ? "null" : "alloc");
id = i;
inbox = new(0);
$display("[%0t] new() EXIT: id=%0d inbox=alloc", $time, id);
endfunction
endclass
initial begin
traced t;
$display("[%0t] before new()", $time);
t = new(42);
$display("[%0t] after new(), t.id=%0d", $time, t.id);
end
// Expected output:
// [0] before new()
// [0] new() ENTER: id arg = 42
// [0] new() ENTER: this.id = 0 (uninit — 2-state tools show 0; 4-state show X)
// [0] new() ENTER: this.inbox = null
// [0] new() EXIT: id=42 inbox=alloc
// [0] after new(), t.id=42Waveform Analysis — Tracking Object Creation Over Time
Constructors don't produce waveforms directly, but the events they fire — object creation, handle assignments, mailbox allocations — can be made visible by mirroring counters into module-level signals. For long-running regressions where memory growth is a concern, the creation/destruction rate signals are your first diagnostic.
module tb;
// Static counter mirrored to a module signal — visible in waveform
int tb_xacts_created;
int tb_xacts_per_us; // rate calculation
int last_count, last_sample_time;
always @(posedge clk) begin
tb_xacts_created = axi_xact::next_id; // snapshot the static counter
// Compute rate over each 1us window
if ($time - last_sample_time >= 1us) begin
tb_xacts_per_us = tb_xacts_created - last_count;
last_count = tb_xacts_created;
last_sample_time = $time;
end
end
endmodule
// In Verdi, browse to:
// tb.tb_xacts_created — total objects created (monotonically increasing)
// tb.tb_xacts_per_us — instantaneous creation rate (should be bounded)
//
// A linear ramp on tb_xacts_created means steady allocation rate.
// A quadratic curve (rate increasing over time) hints at a leak — objects
// being created faster than they're collected; classic memory growth bug.Industry Insights — Constructor Patterns Senior Teams Use
Debugging Academy — 5 Real Constructor Bugs
Each lab is a real failure mode from production projects. Buggy code, symptom, root cause, fix.
class scoreboard;
mailbox #(packet) expected_q;
int match_count;
function new();
match_count = 0;
// ❌ forgot: expected_q = new(0);
endfunction
function void add_expected(packet p);
expected_q.put(p); // crash: null mailbox
endfunction
endclass
scoreboard scb = new;
scb.add_expected(my_packet); // null-pointer crash hereclass base_xact;
function new(string name); // ← parent requires a name arg
this.name = name;
endfunction
endclass
class apb_xact extends base_xact;
function new(bit [31:0] addr);
// ❌ implicit super.new() with no args fails because parent requires one
this.addr = addr;
endfunction
endclass
apb_xact tr = new(32'h100); // compile error: "no matching super.new()"class watcher;
virtual axi_if vif;
int hit_count;
function new(virtual axi_if v);
// Start the monitor thread immediately on construction
fork
forever begin
@(posedge vif.clk); // ← vif might be null here!
if (vif.awvalid) hit_count++;
end
join_none
vif = v; // ← assignment AFTER fork
hit_count = 0;
endfunction
endclassclass config_reg;
bit [7:0] reg_value;
function new(byte initial_value); // ← byte arg, not int
reg_value = initial_value;
endfunction
endclass
initial begin
// Caller passes an int — silently truncated to byte
config_reg r1 = new(300); // 300 truncated to 8 bits = 44
$display("r1.reg_value = %0d", r1.reg_value); // prints 44, not 300
endclass packet;
static int next_id;
function new();
next_id = 0; // ❌ resets static counter on every new()
next_id++;
endfunction
endclass
initial begin
packet p1 = new;
packet p2 = new;
packet p3 = new;
$display("next_id = %0d", packet::next_id); // prints 1, not 3
endInterview Q&A — 12 Questions on Constructors & new()
Drawn from real interviews at chip-design and verification companies. Try to answer before reading each response.
A constructor is a special method named new that runs when an object is allocated. Its job: initialise the object's properties to valid starting values, allocate any nested dynamic types (mailboxes, queues, sub-objects), and establish whatever invariants the class promises. After the constructor returns, the object is ready for use; before, it's just uninitialised heap memory.
Best Practices — Constructor Rules to Walk Away With
- Initialise every property explicitly in
new(). Don't rely on declaration-site initialisers; tool behaviour varies. - Allocate every nested dynamic type in the constructor. Mailboxes, queues, dynamic arrays, associative arrays, and class-handle properties all start null — allocate them in
new(). - Call
super.new(...)explicitly as the first statement in subclass constructors. Implicit zero-arg calls fail unpredictably when parents add required arguments. - Use default-value arguments for optional configuration. Constructor signature stays stable across versions; new arguments don't break existing callers.
- Don't spawn threads in the constructor. Threads see partial state. Spawn in a separate
start()orrun()method called after construction. - Don't pass
thisto other code mid-construction. The object isn't fully built yet; receivers may access uninitialised state. - Static properties initialise at declaration, not in
new(). Constructor-side static init resets on every construction. - Constructor failures should
$fatal, not return a half-built object. Fail loudly at the construction site; never push a broken object downstream. - Keep the constructor short. Property init only; push complex setup (mailbox routing, listener registration, thread spawning) into a separate
build()method. - Provide static factory methods for common configurations.
axi_xact::read_default(addr)reads better thannew(.is_write(0), .addr(addr))at every call site.