Skip to content
PaskalCPP
Language

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 parameter X and the field self.X are two things.
  • self is the object itself: it is read, passed, returned (a copy) and assigned (self := AOther). self outside a method is SM0011.
  • parent.<member> reaches the parent level's member: parent.Show() calls the method the child's Show hides, parent.Z := 1 sets the inherited field. parent outside 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.Put still 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.Twice calling self.Show() runs TBase.Show even on a descendant.
  • A method sees a method declared after it in the same object, takes a var block, 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.

objects_virtual.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
square 9 plain
cube 24 plain
ptr cube 24
[Heap] Allocs: 0, Frees: 0, Leaked: 0
  • override is required: a method named like an ancestor's virtual method without it is SM0332 ('M' is virtual in 'TA': write 'override method M'). override of 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 abstract M is SM0340.
  • final closes a method (override final method) or a whole type (final object(...)): a final object as a parent is SM0339. final or abstract on 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.

objects_init.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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(...), then self.F.init(...) for its object fields in declaration order. parent.init after a field's init is SM0344, field inits out of order SM0345, and an init that does not begin with parent.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.

objects_copy.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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 var parameter 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 := LA copies, and the old value of LB ends.
  • 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, const or 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 a var parameter, a deref or self it 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 T is the sole owner: own T(args) builds the object with the init the arguments pick (own TKid with 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 T shares the object, which ends with its last ref. A ref gives a weak T, which watches: alive(W) tells whether the object still exists, and lock(W) gives a ref to it (nil once it is gone).
  • LO.X reads 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>)).
objects_owners.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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 int32 is 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 T is SM0359) and reads no member (SM0360: read it through lock(AW)).
  • create and destroy take 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.

objects_interfaces.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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).

objects_embed.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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 self like 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.

objects_properties.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
100 212
10
15
ab
15 7
abs 9
[Heap] Allocs: 0, Frees: 0, Leaked: 0
  • A hidden write makes 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= v reads, 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 self or calling a method through self in a read is SM0373; an accessor that uses its own property through self is SM0374.
  • A property is not storage: address of it and ++ on it are SM0376, passing it to a var parameter 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), and read 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 with LO.Items[i]; default; makes LO[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.

objects_operators.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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 * int32 scales, TVec * TVec is 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 own operator==.
  • in is 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 passed var SM0384.

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.

objects_is_as.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
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
  • as on a var parameter gives the object as T, so its fields can be set: (AShape as TCircle).R := .... Through a pointer it gives a ptr to T (nil stays nil), through an own the object the own holds, through a ref a ref T.
  • is on an object with no virtual method is SM0271, a test between two objects of different branches (never true) SM0273, is on 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.

objects_cpp.pcpp
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.
output on x86_64-windows-gnu and x86_64-linux-gnu
9
4 3
4
12
[Heap] Allocs: 0, Frees: 0, Leaked: 0
  • A method matching a C++ virtual needs override: without it, or with virtual, it is SM0332; override of a name no C++ base declares, or of a C++ method that is not virtual, is SM0333; override of a C++ const virtual without const is 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 method changes nothing of self: it may be called through a const parameter and calls only const methods through self. It overrides a C++ const virtual, and an interface may require one. A const method that changes self is SM0387, one that calls a method that is not const through self SM0388; 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.