Skip to content

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.

Constructor — Basic Anatomy
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 == -1

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

Default Constructor — No Code Needed
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 defaults

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

Custom Constructor — Arguments and Initialisation
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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=9600

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

Default Arguments — All Valid Call Styles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 three

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

this Keyword — Resolving Name Conflicts
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Re-assigning new() — Not a Reset
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

ActionAllowed?Reason / Workaround
Set property values✅ YesPrimary purpose of a constructor
Call other functions✅ YesAny function is fine — no time consumption
Call $display✅ YesUseful for debug — just remember it fires on every new()
Call a task❌ NoFunctions cannot call tasks. Move such logic to an init() task called after construction
Use #delay or @event❌ NoConstructor is a function — no simulation time allowed
Call randomize()✅ YesUseful for auto-randomising on creation
Create sub-objects with new()✅ YesCommon pattern — build nested objects in the constructor
Use return❌ NoA 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.

super.new() — Preview for Inherited Classes
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_C2D3

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

Full Example — SPI Transaction with Constructor Best Practices
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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
endmodule

Common Constructor Mistakes

MistakeWhat goes wrongFix
Using handle before new()Null-handle crash at runtimeAlways call new() before first use
Calling a task inside the constructorCompile error — functions cannot call tasksMove time-consuming setup to a separate init() task
Forgetting super.new() in child classParent properties uninitialised — null crashes deep in the call chainMake it the very first statement in every child constructor
Name collision without thisArgument assigned to itself — property stays at defaultUse this.property = argument
Re-using a handle across loop iterationsAll queue entries point to same final objectDeclare ClassName t = new() inside the loop body
Adding a return statementCompile error — constructors have no returnRemove 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.

SystemVerilog — Three constructor archetypes used in verification
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 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.

SystemVerilog — Tracing constructor execution
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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=42

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

SystemVerilog — Construction-rate telemetry
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

1

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 here
2

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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()"
3

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endclass
4

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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
end
5

DEBUG
Buggy Code
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class 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
end

Interview 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

  1. Initialise every property explicitly in new(). Don't rely on declaration-site initialisers; tool behaviour varies.
  2. 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().
  3. Call super.new(...) explicitly as the first statement in subclass constructors. Implicit zero-arg calls fail unpredictably when parents add required arguments.
  4. Use default-value arguments for optional configuration. Constructor signature stays stable across versions; new arguments don't break existing callers.
  5. Don't spawn threads in the constructor. Threads see partial state. Spawn in a separate start() or run() method called after construction.
  6. Don't pass this to other code mid-construction. The object isn't fully built yet; receivers may access uninitialised state.
  7. Static properties initialise at declaration, not in new(). Constructor-side static init resets on every construction.
  8. Constructor failures should $fatal, not return a half-built object. Fail loudly at the construction site; never push a broken object downstream.
  9. Keep the constructor short. Property init only; push complex setup (mailbox routing, listener registration, thread spawning) into a separate build() method.
  10. Provide static factory methods for common configurations. axi_xact::read_default(addr) reads better than new(.is_write(0), .addr(addr)) at every call site.