Skip to content

Constructors and destructors

Sometimes you want to initialize the field of a struct or execute some action right after creating an instance of it. Spice enables you doing that, by supporting constructors. To clean up a struct instance, destructors can be used.

Constructors

Whenever you create a struct instance like this, Spice will automatically call the ctor method of this struct:

Spice
type ExampleStruct struct {
    string message
    bool showMessage
}

p ExampleStruct.ctor() {
    this.message = "Hello world!";
    showMessage = false;
}

f<int> main() {
    // Note the parentheses instead of the braces of a normal struct instantiation
    ExampleStruct es = ExampleStruct();
    // Do something with 'es' ...
}

Constructors can also have arguments to pass some information from the caller to the constructor of the struct:

Spice
// Struct definition ...

p ExampleStruct.ctor(string msg = "Hello World!") {
    this.message = msg;
    showMessage = false;
}

f<int> main() {
    ExampleStruct es = ExampleStruct("Hi fellow programmers!");
    // Do something with 'es' ...
}

The ctor method can also be called manually like calling other methods.

Constructor overloading

A struct can have multiple constructors with different parameter lists, just like function overloading:

Spice
type Vec2 struct {
    double x
    double y
}

p Vec2.ctor() {
    this.x = 0.0;
    this.y = 0.0;
}

p Vec2.ctor(double x, double y) {
    this.x = x;
    this.y = y;
}

f<int> main() {
    Vec2 origin = Vec2();         // calls the no-arg constructor
    Vec2 point  = Vec2(3.0, 4.0); // calls the two-arg constructor
}

A constructor can delegate to another constructor of the same struct, by starting its body with a this.ctor(...) call:

Spice
1
2
3
p Vec2.ctor(double xy) {
    this.ctor(xy, xy); // delegates to the two-arg constructor
}

The called constructor then initializes the fields, i.e. it calls the constructors of struct fields and stores the default field values. The delegating constructor does not do this itself, so every field is constructed exactly once. If the this.ctor(...) call is not the first statement, or its arguments access this (e.g. this.ctor(this.size)), the fields are initialized by both constructors.

Copy constructors

When a struct value is copied (passed by value, returned by value, or assigned), Spice calls the copy constructor if one is defined. The copy constructor takes a const reference to an instance of the same type:

Spice
type Buffer struct {
    int copies
}

p Buffer.ctor() {
    this.copies = 0;
}

p Buffer.ctor(const Buffer& other) {
    this.copies = other.copies + 1;
    printf("copy #%d\n", this.copies);
}

f<int> main() {
    Buffer a = Buffer();
    Buffer b = a;   // prints "copy #1"
    Buffer c = b;   // prints "copy #2"
}

If no copy constructor is defined, Spice generates a default one that copies each field.

Destructors

You have the option to create a destructor by providing a dtor method on a struct. It does not allow any arguments and has no return type, since it is a procedure. Destructors can be especially useful for de-allocating objects in heap memory, that were allocated via malloc(). Whenever a struct variable goes out of scope somewhere in the program, the compiler searches for a destructor and calls it if available.

Here is an example for a custom destructor:

Spice
// Declarations of the generic type T as well as malloc() and free() ...

type ExampleStruct<T> struct {
    string message
    T* messageObject
}

p ExampleStruct.dtor() {
    free(this.messageObject);
}

By-value parameters

A function, that takes a struct with a copy constructor by value, gets its own copy of the argument, even if the argument is a reference. The function owns this copy: it is destructed when the parameter goes out of scope at the end of the function, not when the caller's scope ends. Changes to the parameter therefore never affect the caller's value. A temporary, like the result of a constructor or function call, is moved into the parameter instead of copied, since the caller has no further use for it.

Spice
1
2
3
p consume(Buffer buffer) {
    buffer.append("x"); // Only changes the copy
} // buffer is destructed here

Structs without a copy constructor, whose fields are all trivially copyable (e.g. a struct holding a handle), are passed as bitwise copies instead. Those stay owned by the caller and are not destructed by the called function. Functions declared with ext never destruct their parameters, so the caller destructs the copies it passes to them.

Visibility

Constructors and destructors follow the same visibility rules as other methods. A non-public constructor or destructor can only be called from the source file that defines it. This also applies to the calls, that the compiler inserts implicitly: copying a value of an imported struct requires a public copy constructor, and a local variable or field of an imported struct requires a public destructor, if the struct defines one. Otherwise, the compiler reports an insufficient visibility error.

Spice
// dep.spice
public type S struct {
    public int x
}

public p S.ctor() {
    this.x = 1;
}

p S.ctor(const S& original) { // Non-public copy constructor
    this.x = original.x;
}
Spice
1
2
3
4
5
6
7
// main.spice
import "dep";

f<int> main() {
    dep::S a = dep::S();
    dep::S b = a; // Error: the copy constructor of dep::S has insufficient visibility
}