2.12. Struct
Daslang uses a structure mechanism similar to languages like C/C++, Java, C#, etc. However, there are some important differences. Structures are first-class objects like integers or strings and can be stored in table slots, other structures, local variables, arrays, tuples, variants, etc., and passed as function parameters.
2.12.1. Struct Declaration
A structure object is created through the keyword struct:
struct Foo {
x, y: int
xf: float
}
Structures can be private or public:
struct private Foo {
x, y: int
}
struct public Bar {
xf: float
}
If not specified, structures inherit module publicity (i.e. in public modules structures are public, and in private modules structures are private).
Structure instances are created through a ‘new expression’ or a variable declaration statement:
let foo: Foo
let foo: Foo? = new Foo()
Structures intentionally have no member functions — only data members — since they represent a pure data type. Structures can contain members of function type as data (that is, function pointers that can be reassigned at runtime). Initializers simplify complex structure initialization. A function with the same name as the structure acts as its initializer. The compiler will generate a ‘default’ initializer if there are any members with an initializer:
struct Foo {
x: int = 1
y: int = 2
}
Structure fields are initialized to zero by default, regardless of ‘initializers’ for members, unless you specifically call the initializer:
let fZero : Foo // no initializer is called, x, y = 0
let fInited = Foo() // initializer is called, x = 1, y = 2
Structure field types are inferred where possible:
struct Foo {
x = 1 // inferred as int
y = 2.0 // inferred as float
}
Explicit structure initialization during creation leaves all unspecified members zeroed:
let fExplicit = Foo(uninitialized x=13) // x = 13, y = 0
The previous code example is syntactic sugar for:
let fExplicit: Foo
fExplicit.x = 13
Post-construction initialization only needs to specify overwritten fields:
let fPostConstruction = Foo(x=13) // x = 13, y = 2
The previous code example is syntactic sugar for:
let fPostConstruction: Foo
fPostConstruction.x = 13
fPostConstruction.y = 2
The “Clone initializer” is a useful pattern for creating a clone of an existing structure when both structures are on the heap:
def Foo ( p : Foo? ) { // "clone initializer" takes pointer to existing structure
var self := *p
return <- self
}
...
let a = new Foo(x=1, y=2.) // create new instance of Foo on the heap, initialize it
let b = new Foo(a) // clone of b is created here
2.12.2. Structure Function Members
Daslang doesn’t have embedded structure member functions, virtual (that can be overridden in inherited structures) or non-virtual.
Those features are implemented for classes.
For ease of Objected Oriented Programming, non-virtual member functions can be easily emulated with the pipe operator |>:
struct Foo {
x, y: int = 0
}
def setXY(var self: Foo; X, Y: int) {
with ( self ) {
x = X
y = Y
}
}
var foo: Foo
foo |> setXY(10, 11) // this is syntactic sugar for setXY(foo, 10, 11)
setXY(foo, 10, 11) // exactly same thing as the line above
Since function pointers are first-class values, you can emulate virtual functions by storing function pointers as members:
struct Foo {
x, y: int = 0
set = @@setXY
}
def setXY(var self: Foo; X, Y: int) {
with ( self ) {
x = X
y = Y
}
}
...
var foo: Foo = Foo()
foo->set(1, 2) // this one can call something else, if overridden in derived class.
// It is also just syntactic sugar for function pointer call
invoke(foo.set, foo, 1, 2) // exactly same thing as above
This makes the distinction between virtual and non-virtual calls in the OOP paradigm explicit and intentional. In fact, Daslang classes implement virtual functions in exactly this manner.
Virtual functions can be declared in the body of the structure. This is equivalent to the example above:
struct Foo {
x, y: int = 0
def setXY(X, Y: int) {
x = X
y = Y
}
}
2.12.3. Inheritance
Daslang’s structures support single inheritance by adding a ‘ : ‘, followed by the parent structure’s name in the structure declaration. The syntax for a derived struct is the following:
struct Bar: Foo {
yf: float
}
When a derived structure is declared, Daslang first copies all base’s members to the new structure and then proceeds with evaluating the rest of the declaration.
A derived structure has all members of its base structure. It is just syntactic sugar for copying all the members manually first.
Virtual functions can be overridden in the derived structure:
struct Foo {
x, y: int = 0
def setXY(X, Y: int) {
x = X
y = Y
}
}
struct Bar: Foo {
yf: float = 0.0
def override setXY(X, Y: int) {
x = X + 1
y = Y + 1
yf = x + y
}
}
2.12.4. Alignment
Structure size and alignment are similar to that of C++:
individual members are aligned individually
overall structure alignment is that of the largest member’s alignment
Inherited structure alignment can be controlled via the [cpp_layout] annotation
(see Annotations):
[cpp_layout (pod=false)]
struct CppS1 {
vtable : void? // we are simulating C++ class
b : int64 = 2l
c : int = 3
}
[cpp_layout (pod=false)]
struct CppS2 : CppS1 { // d will be aligned on the class bounds
d : int = 4
}
2.12.5. OOP implementation details
There is sufficient infrastructure to support basic OOP on top of structures. However, it is already available in form of classes with some fixed memory overhead (see Classes).
It’s possible to override the method of the base class with override syntax. Here an example:
struct Foo {
x, y: int = 0
set = @@Foo_setXY
}
def Foo_setXY(var this: Foo; x, y: int) {
this.x = x
this.y = y
}
struct Foo3D: Foo {
z: int = 3
override set = cast<auto> @@Foo3D_setXY
}
def Foo3D_setXY(var thisFoo: Foo3D; x, y: int) {
thisFoo.x = x
thisFoo.y = y
thisFoo.z = -1
}
It is safe to use the cast keyword to cast a derived structure instance into its parent type:
var f3d: Foo3D = Foo3D()
(cast<Foo> f3d).y = 5
It is unsafe to cast a base struct to its derived child type:
var f3d: Foo3D = Foo3D()
def foo(var foo: Foo) {
(cast<Foo3D> foo).z = 5 // error, won't compile
}
If needed, the upcast can be used with the unsafe keyword:
struct Foo {
x: int
}
struct Foo2:Foo {
y: int
}
def setY(var foo: Foo; y: int) { // Warning! Can cause problems if foo is not actually Foo2
unsafe {
(upcast<Foo2> foo).y = y
}
}
As the example above is very dangerous, and in order to make it safer, you can modify it to following:
struct Foo {
x: int
typeTag: uint = hash("Foo")
}
struct Foo2:Foo {
y: int
override typeTag: uint = hash("Foo2")
}
def setY(var foo: Foo; y: int) { // this won't do anything really bad, but will panic on wrong reference
unsafe {
if ( foo.typeTag == hash("Foo2") ) {
(upcast<Foo2> foo).y = y
print("Foo2 type references was passed\n")
} else {
assert(false, "Not Foo2 type references was passed\n")
}
}
}
See also
Classes for the class type with member functions and RTTI,
Move, copy, and clone for struct copy, move, and clone rules,
Finalizers for struct finalization,
Functions for OOP-style |> pipe calls on structs,
Generic programming for typeinfo on structs.