Objects
Object types and their methods, hidden members, virtual dispatch, init and done, copy and move, owners, interfaces, embeds, properties, operators, is / as, and objects at the C++ boundary.
An object is a value type holding fields and methods. Coming from Delphi, note that an object is a value (not a reference), members are always reached through self, a method is not virtual unless it says so, and objects on the heap are held by owners rather than freed by hand.
Objects and methods
TName = object ... end; declares an object; a method's body is written in the type.
- A parent goes in parentheses: an object of the module (
object(TBase)), a chain of them, an imported unit's object (qualified), or a C++ struct declared as plain C++ in the same file. A record, a choices type or an alias of int32 as the parent is SM0308. - Inside a method the members, inherited ones too, are reached through
self(self.X,self.Show()); a member named bare is SM0018 ('N' is a member of 'TA': write self.N). A parameterXand the fieldself.Xare two things. selfis the object itself: it is read, passed, returned (a copy) and assigned (self := AOther).selfoutside a method is SM0011.parent.<member>reaches the parent level's member:parent.Show()calls the method the child'sShowhides,parent.Z := 1sets the inherited field.parentoutside a method is SM0011, in an object with no parent SM0012, and as a value SM0016.- A method may reuse a parent method's name: it hides every parent method of that name (
parent.Putstill reaches them). A field or method named like a field of any level of the chain is SM0309; two methods of one name in one object are SM0004. - A method is not virtual unless it says so:
TBase.Twicecallingself.Show()runsTBase.Showeven on a descendant. - A method sees a method declared after it in the same object, takes a
varblock, and runs on a copy, a local and through a pointer (P^.Move(1, 1),P->Show()). - A method that is not a
const method, called through a const parameter, is SM0426. See const methods.
hidden
hidden F: T; and hidden method M are reached only by the methods of their type and of its descendants; a descendant reaches them through self, parent. and a value of its own type. A hidden member read anywhere else is SM0019; hidden in a record is PR0034.
The object words are not reserved
init done embed implements interface property read write hidden override final abstract copy own ref weak is as are words only where an object type expects them. Everywhere else they are ordinary names: record fields, variables, routines, parameters, and a C library's read() / write() keep their names.
Virtual methods
virtual method opens dispatch: a call runs the method of the object's own type, also through a var parameter of an ancestor type and through a pointer to an ancestor. override method continues it with the same parameters and result.
module exe objects_virtual;
type
TShape = object
virtual method Area(): int32; abstract;
virtual method Name(): string;
begin
return "shape";
end;
method Kind(): string;
begin
return "plain";
end;
end;
TSquare = object(TShape)
S: int32;
override method Area(): int32;
begin
return self.S * self.S;
end;
override method Name(): string;
begin
return "square";
end;
end;
// A final object: no type may extend TCube
TCube = final object(TSquare)
override final method Name(): string;
begin
return "cube";
end;
override method Area(): int32;
begin
// parent.Area() is TSquare's own, not dispatched
return 6 * parent.Area();
end;
end;
routine Show(var AShape: TShape);
begin
println("{} {} {}", AShape.Name(), AShape.Area(), AShape.Kind());
end;
var
LSq: TSquare;
LCube: TCube;
LP: ptr to TShape;
begin
LSq.S := 3;
LCube.S := 2;
Show(LSq);
Show(LCube);
LP := address of LCube;
println("ptr {} {}", LP^.Name(), LP^.Area());
end.
square 9 plain
cube 24 plain
ptr cube 24
[Heap] Allocs: 0, Frees: 0, Leaked: 0
overrideis required: a method named like an ancestor's virtual method without it is SM0332 ('M' is virtual in 'TA': write 'override method M').overrideof a plain method or of nothing is SM0333, an override with other parameters or another result SM0334, overriding a final method SM0335.virtual method M(): T; abstract;has no body. An object with an abstract method cannot be held by value or created (SM0338);parent.M()of an abstractMis SM0340.finalcloses a method (override final method) or a whole type (final object(...)): a final object as a parent is SM0339.finalorabstracton a plain method is SM0336 / SM0337.parent.Area()calls the parent level's own method, not dispatched.
init and done
init(params) runs when an object is built and done when it ends; they are member kinds of their own, never methods. LB: TBox(1, 3); builds a variable with an init's arguments, and LP: TPoint; runs the init that takes none.
module exe objects_init;
type
TLog = object
Name: string;
init(const AName: string);
begin
self.Name := AName;
println("init log {}", self.Name);
end;
done;
begin
println("done log {}", self.Name);
end;
end;
TShape = object
Tag: int32;
init(const ATag: int32);
begin
self.Tag := ATag;
println("init shape {}", self.Tag);
end;
done;
begin
println("done shape {}", self.Tag);
end;
end;
TBox = object(TShape)
Side: int32;
Log: TLog;
init(const ATag: int32; const ASide: int32);
begin
// The parent first, then the object fields in declaration order
parent.init(ATag);
self.Log.init("box");
self.Side := ASide;
println("init box {}", self.Side);
end;
done;
begin
println("done box {}", self.Side);
end;
end;
TPoint = object
X: int32;
Y: int32;
init();
begin
self.X := 1;
self.Y := 2;
end;
init(const AX: int32; const AY: int32);
begin
self.X := AX;
self.Y := AY;
end;
end;
routine UseBox();
var
LB: TBox(1, 3);
begin
println("area {}", LB.Side * LB.Side);
end;
var
LP: TPoint;
LQ: TPoint(5, 6);
begin
UseBox();
println("point {} {} / {} {}", LP.X, LP.Y, LQ.X, LQ.Y);
end.
init shape 1
init log box
init box 3
area 9
done box 3
done log box
done shape 1
point 1 2 / 5 6
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- An object is built parent first, then its fields, then its own init body, and ends the other way round: its done, its fields' dones, its parent's done.
- An init begins with
parent.init(...), thenself.F.init(...)for its object fields in declaration order.parent.initafter a field's init is SM0344, field inits out of order SM0345, and an init that does not begin withparent.init(...)when the parent has no init without arguments SM0347. - Inits overload by parameter types (two with the same types: SM0324). Arguments no init takes are SM0351; a variable built without arguments when no init takes none is SM0349.
- When an init raises, what it already built ends (a field's done runs) and its own done never runs: the object never existed.
- A C/C++ type takes its constructor's arguments the same way: a field (
self.Text.init("label")) and a variable (LT: std::string("cpp");). - An object has one done (SM0341); init and done are never called by name (SM0342 / SM0343); a virtual method called in an init is SM0348, since the object is not its own type yet.
Copy, move and slicing
An object is a value. One with a done and no copy cannot be copied: it moves. A local returned from a routine moves out (its done does not run there); LA := Make(4) takes the new value and the old one ends then.
module exe objects_copy;
type
// A done and no copy: TRes cannot be copied, it moves
TRes = object
Id: int32;
init(const AId: int32);
begin
self.Id := AId;
println("open {}", self.Id);
end;
done;
begin
println("close {}", self.Id);
end;
end;
TCp = object
Id: int32;
init(const AId: int32);
begin
self.Id := AId;
end;
copy(const ASource: TCp);
begin
self.Id := ASource.Id + 100;
println("copy {} -> {}", ASource.Id, self.Id);
end;
done;
begin
println("end {}", self.Id);
end;
end;
routine Make(const AId: int32): TRes;
var
LR: TRes(AId);
begin
// The local moves out: its done does not run here
return LR;
end;
routine Moves();
var
LA: TRes(3);
begin
// LA takes the new value; its old value ends now
LA := Make(4);
println("show {}", LA.Id);
end;
routine Copies();
var
LA: TCp(7);
LB: TCp(8);
begin
LB := LA;
println("lb {}", LB.Id);
end;
begin
Moves();
Copies();
end.
open 3
open 4
close 3
show 4
close 4
copy 7 -> 107
end 8
lb 107
end 107
end 7
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- Copying an object that cannot be copied is SM0427: a variable initialized from another, a value argument, a
varparameter returned by value. copy(const ASource: T)makes an object copyable: it builds the copy as an init builds an object, and its body fills the fields.LB := LAcopies, and the old value ofLBends.- Values end at the end of their scope, in reverse order, and every value with a done ends exactly once.
- A descendant that is not polymorphic converts to its parent by value (the parent part is copied). A polymorphic one reaches its parent only through
var,constor a pointer: by value it is SM0428 ('TB' would be cut down to its 'TA' part here). - A polymorphic object reached where it may be a descendant (a const parameter, a deref, the result of
as), copied by value, is SM0428; assigned whole through avarparameter, a deref orselfit is SM0429. - A record or an overlay holds no object (nor an own, ref or weak): SM0357.
Owners: own, ref, weak
An object on the heap is held by an owner.
own Tis the sole owner:own T(args)builds the object with the init the arguments pick (own TKidwith none), a routine returns one (it moves), and the object ends with its own. An own of a polymorphic root frees the whole object (the descendant's done, then the base's).ref Tshares the object, which ends with its last ref. A ref gives aweak T, which watches:alive(W)tells whether the object still exists, andlock(W)gives a ref to it (nil once it is gone).LO.Xreads the object an own or ref holds; through a nil one it raises (exccode 3).- A ref cycle is never freed; the debug build's heap report names where each leaked object was built (
[Heap] Leaked object created at <file>(<line>)).
module exe objects_owners;
type
TNode = object
Name: int32;
init(const AN: int32);
begin
self.Name := AN;
println("init {}", self.Name);
end;
done;
begin
println("done {}", self.Name);
end;
end;
TBase = object
virtual method Speak();
begin
println("base");
end;
done;
begin
println("done base");
end;
end;
TKid = object(TBase)
override method Speak();
begin
println("kid");
end;
done;
begin
println("done kid");
end;
end;
routine Make(const AN: int32): own TNode;
begin
return own TNode(AN);
end;
routine Owns();
var
LO: own TNode;
LB: own TBase;
begin
LO := Make(1);
LO.Name := 11;
println("own {}", LO.Name);
// An own of a polymorphic root frees the whole object
LB := own TKid;
LB.Speak();
end;
routine Shares();
var
LA: ref TNode;
LB: ref TNode;
LW: weak TNode;
begin
LA := ref TNode(2);
LB := LA;
LW := LA;
println("alive {}", alive(LW));
LA := nil;
println("still {}", lock(LW).Name);
// The last ref goes: the object ends
LB := nil;
println("alive {}", alive(LW));
end;
routine NilAccess();
var
LO: own TNode;
begin
guard
println("{}", LO.Name);
except
println("nil {}", exccode());
end;
end;
begin
Owns();
Shares();
NilAccess();
end.
init 1
own 11
kid
done kid
done base
done 11
init 2
alive true
still 2
done 2
alive false
nil 3
[Heap] Allocs: 3, Frees: 3, Leaked: 0
- own, ref and weak hold an object:
own int32is SM0358. - An own is never copied (SM0427:
an own is the sole owner of its object); it reaches an ancestor only when the ancestor is polymorphic, and a weak never converts to a ref (both SM0200). - A weak builds nothing (
weak Tis SM0359) and reads no member (SM0360:read it through lock(AW)). createanddestroytake no object (SM0361): an object on the heap is an own or a ref.
Interfaces
IName = interface ... end; lists methods with no body; it may name one parent interface (interface(INamed)).
object ... implements I1, I2 promises every method of those interfaces and of their parents. Each is a virtual method of the object or of a level above it, with the same parameters and result. An inherited method fulfils it too, and an override below runs through the interface.
module exe objects_interfaces;
type
INamed = interface
method Name(): string;
end;
IShape = interface(INamed)
method Area(): int32;
end;
TSquare = object implements IShape
S: int32;
virtual method Name(): string;
begin
return "square";
end;
virtual method Area(): int32;
begin
return self.S * self.S;
end;
end;
TPlain = object
virtual method Name(): string;
begin
return "plain";
end;
end;
// TPlain's Name fulfils INamed
TBorrow = object(TPlain) implements INamed
end;
routine Report(var AShape: IShape);
begin
println("{} {}", AShape.Name(), AShape.Area());
end;
routine Named(var AName: INamed);
begin
println("named {}", AName.Name());
end;
var
LSq: TSquare;
LBorrow: TBorrow;
LP: ptr to IShape;
LR: ref IShape;
begin
LSq.S := 4;
Report(LSq);
Named(LSq);
LP := address of LSq;
println("ptr {}", LP^.Area());
Named(LBorrow);
LR := ref TSquare();
println("ref {}", LR.Name());
end.
square 16
named square
ptr 16
named plain
ref square
[Heap] Allocs: 1, Frees: 1, Leaked: 0
- An interface is reached through
var,const,ptr to, own or ref, never held by value (LV: IShape;is SM0338). - An interface holds methods and properties, nothing else: a field in it is PR0034, a method with a body PR0060, a method its parent already declares SM0368.
- After
implements: a type that is no interface is SM0362, an interface named twice SM0363, a method the object does not have SM0364, a plain method SM0365 (write 'virtual method Area'), another signature SM0366, a hidden method SM0367.
Embeds
embed F: T; in an object holds a T as a field (built, copied and freed as one) whose members are reached on the object too: self.Bump(2) is self.C.Bump(2), and from outside LW.Bump(1).
module exe objects_embed;
type
TCounter = object
Count: int32;
method Bump(const AStep: int32);
begin
self.Count := self.Count + AStep;
end;
method Name(): string;
begin
return "counter";
end;
end;
TPoint = record
X: int32;
Y: int32;
end;
INamed = interface
method Name(): string;
end;
// C's Name fulfils INamed: the object forwards to it
TWidget = object implements INamed
embed C: TCounter;
embed P: TPoint;
// The object's own Count wins over C's
Count: int32;
method Show();
begin
self.Bump(2);
self.X := 5;
self.Count := 9;
println("{} {} {} {}", self.C.Count, self.Count, self.X, self.Name());
end;
end;
TDeep = object
embed W: TWidget;
end;
routine Named(var AName: INamed);
begin
println("named {}", AName.Name());
end;
var
LW: TWidget;
LD: TDeep;
begin
LW.Show();
LW.Bump(1);
println("{}", LW.C.Count);
Named(LW);
LD.Bump(10);
LD.X := 7;
println("{} {}", LD.W.C.Count, LD.W.P.X);
end.
2 9 5 counter
3
named counter
10 7
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- The object's own and inherited members win over an embed's. Embeds nest; the nearest depth wins, and a name two embeds at one depth have is SM0370 at the use (name one:
self.A.X). - An embed's members are named through
selflike any member. An embed of an object or a record works; of another type (embed N: int32) it is SM0369. - An embed fulfils an interface: the object forwards to the embedded method, which needs no
virtual. - An embedded type's hidden members stay hidden (SM0019).
Properties
property X: T read ...; [[hidden] write ...;] in an object is a value read and written through accessors, never storage. A read or a write is bound to a field (read FC; write FC;) or written with a body: read begin ... end; returns a value, write(AValue) begin ... end; takes one of the property's type.
module exe objects_properties;
type
TTemp = object
FC: int32;
FLog: string;
property Celsius: int32 read FC; write FC;
property Fahrenheit: int32 read
begin
return self.FC * 9 div 5 + 32;
end;
write(AValue)
begin
self.FC := (AValue - 32) * 5 div 9;
end;
// Read anywhere, written only by the methods of TTemp and its descendants
property Log: string read FLog; hidden write FLog;
method Note(const AText: string);
begin
self.Log := self.Log + AText;
end;
end;
TBag = object
FItems: array[0..3] of int32;
property Items[const AIndex: int32]: int32 read
begin
return self.FItems[AIndex];
end;
write(AValue)
begin
self.FItems[AIndex] := AValue;
end;
default;
end;
TAbs = object
virtual property V: int32 read abstract;
end;
TCon = object(TAbs)
override property V: int32 read
begin
return 9;
end;
end;
routine ShowAbs(var AA: TAbs);
begin
println("abs {}", AA.V);
end;
var
LT: TTemp;
LB: TBag;
LC: TCon;
begin
LT.Celsius := 100;
println("{} {}", LT.Celsius, LT.Fahrenheit);
LT.Fahrenheit := 50;
println("{}", LT.Celsius);
LT.Celsius += 5;
println("{}", LT.Celsius);
LT.Note("a");
LT.Note("b");
println("{}", LT.Log);
// default: LB[i] is LB.Items[i]
LB[1] := 5;
LB.Items[2] := 7;
LB[1] += 10;
println("{} {}", LB[1], LB.Items[2]);
ShowAbs(LC);
end.
100 212
10
15
ab
15 7
abs 9
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- A
hidden writemakes the property readable anywhere and written only in the methods of its type and its descendants (SM0019 elsewhere); a property without a write, assigned, is SM0375. X OP= vreads, operates and writes; the object (an index first), then the value, are each evaluated once, in that order.- A read changes nothing of its object: changing a field of
selfor calling a method throughselfin a read is SM0373; an accessor that uses its own property throughselfis SM0374. - A property is not storage:
address ofit and++on it are SM0376, passing it to avarparameter SM0405; an assignment to a property is a statement, never a value (SM0377). - A property takes a method's dispatch words:
virtual,override,final property. An override may restate its read only (the base's write still runs), andread abstract;makes the object abstract. The method dispatch codes apply with the same numbers. - An interface holds properties (
property X: T read; [write;]); an object implements one with a virtual property, its own or an inherited one. - A property takes one index,
property Items[const AIndex: T]: U, read and written withLO.Items[i];default;makesLO[i]mean it. Read without its index it is SM0378.
Operators
A record or an object declares operators, each operator OP(params): T; with a body: + - * / div mod, the unary minus (- with one operand), = and <> (a pair), < <= > >= and in.
module exe objects_operators;
type
TVec = record
X: int32;
Y: int32;
operator +(const A: TVec; const B: TVec): TVec;
var
LR: TVec;
begin
LR.X := A.X + B.X;
LR.Y := A.Y + B.Y;
return LR;
end;
// TVec * int32 scales
operator *(const A: TVec; const AK: int32): TVec;
var
LR: TVec;
begin
LR.X := A.X * AK;
LR.Y := A.Y * AK;
return LR;
end;
// TVec * TVec is the dot product
operator *(const A: TVec; const B: TVec): int32;
begin
return A.X * B.X + A.Y * B.Y;
end;
operator -(const A: TVec): TVec;
var
LR: TVec;
begin
LR.X := -A.X;
LR.Y := -A.Y;
return LR;
end;
operator =(const A: TVec; const B: TVec): bool;
begin
return (A.X = B.X) and (A.Y = B.Y);
end;
operator <>(const A: TVec; const B: TVec): bool;
begin
return not (A = B);
end;
end;
TSpan = record
Lo: int32;
Hi: int32;
operator in(const AV: int32; const AR: TSpan): bool;
begin
return (AV >= AR.Lo) and (AV <= AR.Hi);
end;
end;
var
LA: TVec;
LB: TVec;
LC: TVec;
LS: TSpan;
begin
LA.X := 1;
LA.Y := 2;
LB.X := 3;
LB.Y := 4;
LC := LA + LB * 2;
println("{} {}", LC.X, LC.Y);
println("{}", LA * LB);
LC := -LA;
println("{} {}", LC.X, LC.Y);
println("{} {} {}", LA = LB, LA <> LB, LA = LA);
LS.Lo := 1;
LS.Hi := 6;
println("{} {}", 5 in LS, 9 in LS);
end.
7 10
11
-1 -2
false true true
true false
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- Several operators of one symbol are picked by their operand types, as a call is:
TVec * int32scales,TVec * TVecis the dot product. Operands no declared operator takes are SM0430, operands two take alike SM0431. - An object compares only through an operator its type declares:
=,<>or<on two objects, or on arrays of them, that the type does not declare is SM0252. An object or a record whose parent is a C++ class compares through that class's ownoperator==. inis declared with the type on the set operand's side:operator in(const AV: int32; const AR: TSpan): bool.- An operator has no
self(SM0011): it is a routine of its type, and one of its operands is the declaring type (SM0381). A comparison that does not give a bool is SM0382,=without<>SM0386, an operand passedvarSM0384.
is and as
X is T tests the dynamic type of a polymorphic object; X as T casts to T and raises when the object is not one (exccode 13, invalid cast: the object is not a TCircle). X is an object with a virtual method, or a ptr to, own or ref of one; T is an object or an interface type.
module exe objects_is_as;
type
IRound = interface
method Radius(): int32;
end;
TShape = object
virtual method Name(): string;
begin
return "shape";
end;
end;
TCircle = object(TShape) implements IRound
R: int32;
override method Name(): string;
begin
return "circle";
end;
virtual method Radius(): int32;
begin
return self.R;
end;
end;
TSquare = object(TShape)
S: int32;
end;
routine Describe(const AShape: TShape);
begin
println("{} {} {}", AShape is TCircle, AShape is TSquare, AShape is IRound);
if AShape is TCircle then
println("r {}", (AShape as TCircle).R);
end;
end;
routine Grow(var AShape: TShape);
begin
// as on a var parameter is the object itself: its fields can be set
(AShape as TCircle).R := (AShape as TCircle).R + 1;
end;
var
LC: TCircle;
LS: TSquare;
LP: ptr to TShape;
LQ: ptr to TCircle;
begin
LC.R := 2;
Describe(LC);
Describe(LS);
Grow(LC);
println("{}", LC.R);
LP := address of LC;
LQ := LP as TCircle;
println("{} {}", LP is TCircle, LQ^.R);
guard
Grow(LS);
except
println("{} {}", exccode(), excmsg());
end;
end.
true false true
r 2
false true false
3
true 3
13 invalid cast: the object is not a TCircle
[Heap] Allocs: 0, Frees: 0, Leaked: 0
ason avarparameter gives the object as T, so its fields can be set:(AShape as TCircle).R := .... Through a pointer it gives aptr to T(nil stays nil), through an own the object the own holds, through a ref aref T.ison an object with no virtual method is SM0271, a test between two objects of different branches (never true) SM0273,ison a variable (which holds exactly its type) SM0274, on a weak SM0275.
Objects and C++ classes
object(CShape) derives from a C++ struct declared as plain C++ in the same file: its members are C++'s (self.sides), and override method area() overrides the C++ virtual, so C++ code calling area() through a CShape& runs the PaskalCPP body.
module exe objects_cpp;
struct CShape {
virtual ~CShape() = default;
virtual int area() {
return 0;
}
virtual int corners() const {
return 0;
}
int sides = 0;
};
int cpp_area(CShape& s) {
return s.area();
}
int cpp_corners(const CShape& s) {
return s.corners();
}
type
TSquare = object(CShape)
S: int32;
init(const AS: int32);
begin
self.S := AS;
// sides is the C++ base's member
self.sides := 4;
end;
override method area(): int32;
begin
return self.S * self.S;
end;
override const method corners(): int32;
begin
return self.sides;
end;
end;
TRuler = object
Len: int32;
// A const method calls only const methods through self
const method Size(): int32;
begin
return self.Twice();
end;
const method Twice(): int32;
begin
return self.Len * 2;
end;
end;
routine Measure(const AR: TRuler): int32;
begin
return AR.Size();
end;
var
LSq: TSquare(3);
LR: TRuler;
begin
println("{}", cpp_area(LSq));
println("{} {}", LSq.sides, LSq.S);
println("{}", cpp_corners(LSq));
LR.Len := 6;
println("{}", Measure(LR));
end.
9
4 3
4
12
[Heap] Allocs: 0, Frees: 0, Leaked: 0
- A method matching a C++ virtual needs
override: without it, or withvirtual, it is SM0332;overrideof a name no C++ base declares, or of a C++ method that is not virtual, is SM0333;overrideof a C++ const virtual withoutconstis SM0334. clang judges these from the generated class, and they are reported in PaskalCPP's words. - An exported object of a unit is a plain C++ class: C++ code derives from it and builds it with its init's arguments.
const methodchanges nothing ofself: it may be called through a const parameter and calls only const methods throughself. It overrides a C++ const virtual, and an interface may require one. A const method that changesselfis SM0387, one that calls a method that is not const throughselfSM0388; an override that drops the const is SM0389.- A method named without its call is SM0432: a method runs on its object, so it is never a routine value.