Skip to content

constructor

IsaacShelton edited this page Nov 16, 2022 · 6 revisions

constructor

Constructors can be defined using the constructor keyword.

import basics
import random

pragma ignore_unused

struct Person (name String, uid int) {
    constructor(name String){
        this.name = name
        this.uid = randomInt(100)
    }
}

func toString(person Person) String {
    return person.name + " : id=" + person.uid
}

func main {
    randomize()

    john Person("John")                               // constructed (living on stack)

    james *Person = new Person("John")                // constructed (living on heap)
    defer delete james

    print("Constructed people:")
    print(john)
    print(*james)

    // --------- vs ---------

    uninitialized_guy Person = undef                  // undefined (living on stack)

    uninitialized_heap_guy *Person = new undef Person // undefined (living on heap)
    defer delete uninitialized_heap_guy

    unconstructed_guy Person                          // zero-initialized (living on stack)

    unconstructed_heap_guy *Person = new Person       // zero-initialized (living on heap)
    defer delete unconstructed_heap_guy

    print("Unconstructed people")
    print(unconstructed_guy)
    print(*unconstructed_heap_guy)

    print("Uninitialized people")
    print("(cannot print these, since their names would be garbage memory)")
}

You can also define multiple constructors on the same type.

struct Thing (a, b, c String) {
    constructor(value String){
        this.a = value
        this.b = value
        this.c = value
    }

    constructor(int value){
        this.a = toString(value)
        this.b = toString(value + 1)
        this.c = toString(value + 2)
    }

    constructor(a String = "a", b String = "b", c String = "c"){
        this.a = a
        this.b = b
        this.c = c
    }
}

The primary use case of constructors is to construct values on their creation. You can easily invoke constructors on value creation by using parentheses immediately after. For example:

import basics

struct Thing (value double, owner String) {
    constructor(initial_value double){
        this.value = initial_value
        this.owner = "nobody"
    }
}

func main {
    one Thing(1.0)
    two *Thing = new Thing(2.0)

    defer {
        two.__defer__() // destroy
        delete two      // delete
    }

    print(one.value)
    print(two.value)
}

You can also call a constructor like any other method, using value.__constructor__(arg1, arg2, argN). However, you are responsible for making sure the memory of the value is zero-initialized before-hand, since constructors are allowed to assume zero-initialization.

import basics

struct MyInteger (value int) {
    constructor(){
        this.value = 12435
    }
}

func main {
    my_integer MyInteger
    my_integer.__constructor__()

    // which is equivalent to

    /*
    my_integer MyInteger()
    */

    // -----------------------------

    print(my_integer.value)
}

Calling __constructor__ on memory that is not zero-initialized is type-dependant. Types are NOT REQUIRED to be able to be constructed on non zero-initialized memory. If uncertain, zero-initialize the memory before manual construction using something like memset(memory, 0, size_in_bytes)!

import basics

struct Person (name String, level int) {
    constructor(name String){
        this.name = name
        this.level = 1
    }
}

func main(){
    // Example of manually constructing values from garbage memory

    count usize = 100
    raw_memory *Person = new undef Person * count // equivalent to `malloc(sizeof Person * count)`

    repeat count {
        person *Person = &raw_memory[idx]

        memset(person, 0, sizeof Person)
        person.__constructor__("Abby")
    }

    each Person in [raw_memory, count] {
        printf("[%d] Hello %S\n", idx, it.name)
    }

    defer {
        each Person in [raw_memory, count], it.__defer__() // destroy each person (necessary because it contains `String` and so is not POD (plain-old-data))
        delete raw_memory                                  // equivalent to `free(raw_memory)`
    }
}

When defined on Classes

Constructors have an additional behavior when defined on classes. They are responsible for initializing the __vtable__ field used for dynamic dispatch.

This is why instances of classes must be constructed before using dynamic dispatch.

class MyClass () {
    constructor {}

    virtual func doTheThing {}
}

func main {
    one MyClass()
    one.doTheThing() // ok - `one` is constructed

    two *MyClass = new MyClass()
    defer delete two
    two.doTheThing() // ok - the value pointed to by `two` is constructed

    three MyClass
    three.doTheThing()  // this will crash!!! The value `three` must be constructed in order to use dynamic dispatch!!!
}
Clone this wiki locally