Inheritance and Polymorphism
A drawing program has circles, rectangles and squares, and somewhere in it a loop that has to ask
each one for its area. Written with the tools of Chapter 12, every shape is its own type, and the
loop needs a select case on some "kind" field to know which code to run. The next shape means
editing that loop, and every other loop like it.
Inheritance lets one type start as another and add to it. Polymorphism lets a single call reach the right code for whatever object is really there, so the loop asks "what is your area?" and never needs to know what it is talking to.
tiko gives you two ways to build them, and this chapter is in two parts. Part 1 is the classic model, the one C++ and FreeBASIC share: a base type with virtual methods, extended by every shape, and the three places it goes wrong: an object deleted through the wrong pointer, an object passed by value, and a relationship that was never really "is a". Part 2 is interfaces: a contract that any type can take on, as many as it needs, without spending its one base on it. The two mix freely, and the chapter ends with how to choose.
Part 1: The classic model
A derived type starts as its base and adds to it, and a method the base marks virtual can be
replaced by each derived type. Everything in this part works the same whether the type is
declared with type … extends or, as Part 2 shows, with class.
Extending a type
'' extends.tiko -- a derived type starts as its base, and adds to it.
type Vehicle
declare constructor( byval wheels as int32 = 4 )
declare function Describe( ) as ustring
wheels as int32
speed as int32
end type
constructor Vehicle( byval wheels as int32 )
this.wheels = wheels
end constructor
function Vehicle.Describe( ) as ustring
return wheels & " wheels at " & speed & " km/h"
end function
type Bicycle extends Vehicle
declare constructor( byval gears as int32 )
declare function Describe( ) as ustring
gears as int32
end type
constructor Bicycle( byval gears as int32 )
base( 2 ) '' build the Vehicle part first
this.gears = gears
end constructor
function Bicycle.Describe( ) as ustring
return base.Describe( ) & ", " & gears & " gears"
end function
dim as Bicycle bike = Bicycle( 21 )
bike.speed = 25 '' a field it inherited
print bike.Describe( )
dim as Vehicle car
print car.Describe( )
print "sizes:"; sizeof( Vehicle ); sizeof( Bicycle )
print "offsets:"; offsetof( Bicycle, wheels ); _
offsetof( Bicycle, speed ); offsetof( Bicycle, gears )
2 wheels at 25 km/h, 21 gears
4 wheels at 0 km/h
sizes: 8 12
offsets: 0 4 8
type Bicycle extends Vehicle makes a Bicycle a Vehicle with more fields. It has wheels and
speed without declaring them, and the offsets show where they live: the base's fields come first,
exactly as they are laid out in a Vehicle, and gears follows them. That layout is the point. A
Bicycle starts with a complete Vehicle, so anything that works on a Vehicle can be handed one.
base( 2 ) runs the base's constructor first. The Vehicle part of a Bicycle is built
before the Bicycle constructor's own body runs, and base( … ) chooses which Vehicle
constructor builds it and with what. Written without a base( … ) call, the base's default
constructor runs instead.
base.Describe( ) calls the base's version of a method from inside the derived one. Bicycle's
Describe has the same name as Vehicle's and hides it: on a Bicycle, Describe means
Bicycle.Describe. Reusing the base's text through base. means the two descriptions cannot
drift apart.
A base without a default constructor
Vehicle's constructor takes wheels = 4 as a default, so it can be called with nothing at all.
That default is load-bearing:
'' nodefault.tiko -- a base with no default constructor leaves the
'' derived type with no way to copy itself.
type Vehicle
declare constructor( byval wheels as int32 )
wheels as int32
end type
constructor Vehicle( byval wheels as int32 )
this.wheels = wheels
end constructor
type Bicycle extends Vehicle
declare constructor( )
gears as int32
end type
constructor Bicycle( )
base( 2 )
end constructor
dim as Bicycle bike
print bike.wheels
nodefault.tiko(13,1): error TKO0926: 'bicycle' needs a copy constructor: its base has no constructor that takes no argument, so none can be made for it
|
13 | type Bicycle extends Vehicle
| ^
A derived type gets a copy constructor for free, and that free one works by building a fresh base part with the base's default constructor, then copying every field across. With no default constructor in the base, there is nothing to build the fresh part with, so the derived type must write its own copy constructor. fbc refuses the same program with its error 188.
Give the base a constructor that takes no arguments, or default its parameters as extends.tiko
does, and every type derived from it can be copied without writing anything.
base( … ) passes whatever the base's constructors take, chosen the way any overloaded call is.
Here it hands a label up, and the base's default value for it, "unnamed", is what keeps
Sensor copyable:
'' basestring.tiko -- passing text up to the base's constructor.
type Labelled
declare constructor( byref label as ustring = "unnamed" )
label as ustring
end type
constructor Labelled( byref label as ustring )
this.label = label
end constructor
type Sensor extends Labelled
declare constructor( byref label as ustring, _
byval reading as float64 )
reading as float64
end type
constructor Sensor( byref label as ustring, byval reading as float64 )
base( label )
this.reading = reading
end constructor
dim as Sensor s = Sensor( "boiler", 81.5 )
print s.label; " reads"; s.reading
boiler reads 81.5
Calling through the base
A Bicycle can be handed to anything that takes a Vehicle. What happens to its methods when it
is?
'' hiding.tiko -- without virtual, the call is chosen by the type the
'' compiler sees, not by the object that is really there.
type Vehicle
declare function Kind( ) as ustring
wheels as int32
end type
function Vehicle.Kind( ) as ustring
return "vehicle"
end function
type Bicycle extends Vehicle
declare function Kind( ) as ustring
end type
function Bicycle.Kind( ) as ustring
return "bicycle"
end function
sub Show( byref v as Vehicle )
print " Show sees a "; v.Kind( )
end sub
dim as Bicycle bike
print "directly: "; bike.Kind( )
Show( bike )
directly: bicycle
Show sees a vehicle
Show declares its parameter as a Vehicle, so inside Show the compiler knows only that it has
a Vehicle, and v.Kind( ) is compiled as a call to Vehicle.Kind. Which object arrives at run
time does not enter into it. Hiding a method changes what the name means on the derived type; it
changes nothing for code that sees the object as its base.
That is the right behaviour for most methods, and it costs nothing. But it is exactly wrong for the drawing program's loop, which sees every shape as "a shape" and needs each one's own answer.
Virtual methods
'' shapes.tiko -- one loop over different shapes, each one answering
'' for itself.
type Shape extends Object
declare abstract function Area( ) as float64
declare virtual function Kind( ) as ustring
declare function Describe( ) as ustring
end type
function Shape.Kind( ) as ustring
return "shape"
end function
'' Not virtual, yet the calls inside it are.
function Shape.Describe( ) as ustring
return Kind( ) & ", area " & Area( )
end function
type Circle extends Shape
declare constructor( byval r as float64 )
declare function Area( ) as float64 override
declare function Kind( ) as ustring override
r as float64
end type
constructor Circle( byval r as float64 )
this.r = r
end constructor
function Circle.Area( ) as float64
return 3.14159 * r * r
end function
function Circle.Kind( ) as ustring
return "circle"
end function
type Rect extends Shape
declare constructor( byval w as float64 = 1, _
byval h as float64 = 1 )
declare function Area( ) as float64 override
'' virtual AND override: a type derived from Rect may override
'' it again.
declare virtual function Kind( ) as ustring override
w as float64
h as float64
end type
constructor Rect( byval w as float64, byval h as float64 )
this.w = w : this.h = h
end constructor
function Rect.Area( ) as float64
return w * h
end function
function Rect.Kind( ) as ustring
return "rectangle"
end function
type Square extends Rect
declare constructor( byval side as float64 )
declare function Kind( ) as ustring override
end type
constructor Square( byval side as float64 )
base( side, side )
end constructor
function Square.Kind( ) as ustring
return "square"
end function
dim as Circle c = Circle( 1 )
dim as Rect r = Rect( 2, 3 )
dim as Square s = Square( 2 )
dim as Shape ptr all( 0 to 2 ) = { @c, @r, @s }
for i as int32 = lbound( all ) to ubound( all )
print all( i )->Describe( )
next
circle, area 3.14159
rectangle, area 6
square, area 4
Four words do the work.
extends Object.Objectis the built-in root type. Extending it gives every object of the type one hidden field, a pointer to the type's table of virtual methods, and that pointer is how a call finds the right code at run time.virtualmarks a method that derived types may replace. A call to it looks the method up in the object's table, so it reaches the version belonging to the object that is really there.overridemarks a method that replaces a virtual one. The compiler checks that there is something to replace:overrideon a method the base did not make virtual isTKO0621.abstractdeclares a virtual method with no body at all.Shapecannot say what its area is, so it says only that every shape has one.
Shape.Describe is not virtual, yet its output is different for every shape, because the two calls
inside it are. That is a common and useful shape: the base writes the procedure once, and the
derived types supply the parts that differ.
An override is not itself virtual. Circle.Kind replaces Shape.Kind, but a type derived
from Circle could not override it again. Rect declares its Kind as virtual … override,
which both replaces Shape's and opens it to Rect's own descendants. That is what lets Square
override it in turn. Square keeps Rect's Area, because a square's area is a rectangle's.
Leave virtual off, and the next level down cannot override:
'' notvirtual.tiko -- an override is not itself virtual. A type
'' derived from Dog that writes Speak again hides it; it does not
'' override it.
type Animal extends Object
declare virtual function Speak( ) as ustring
end type
function Animal.Speak( ) as ustring
return "..."
end function
type Dog extends Animal
declare function Speak( ) as ustring override
end type
function Dog.Speak( ) as ustring
return "woof"
end function
type Puppy extends Dog
declare function Speak( ) as ustring '' hides Dog's
end type
function Puppy.Speak( ) as ustring
return "yip"
end function
sub Listen( byref a as Animal )
print " through Animal: "; a.Speak( )
end sub
dim as Puppy pup
print "directly: "; pup.Speak( )
Listen( pup )
directly: yip
through Animal: woof
Dog.Speak is an override but not virtual, so Puppy's Speak hides it without replacing it.
Called on a Puppy directly it answers yip; called through an Animal it reaches Dog's.
Writing override on Puppy.Speak would not help: there is no open slot for it to fill, and the
compiler says so (TKO0621). The fix is one word in Dog, virtual … override, exactly as
Rect does in shapes.tiko.
C++ programmers expect the opposite, because there an override stays virtual all the way down. tiko follows fbc, where it does not, so code moves between the two compilers unchanged.
Abstract types
'' abstract.tiko -- a type with an unwritten method cannot be created.
type Shape extends Object
declare abstract function Area( ) as float64
end type
dim as Shape s
print s.Area( )
abstract.tiko(7,14): error TKO0620: 's' has an abstract type, which cannot be instantiated
|
7 | dim as Shape s
| ^
A type with an abstract method is incomplete on purpose, so it cannot be created: there would be
nothing to run when someone asked for its area. A derived type that fills in every abstract method
can be created, and one that leaves any of them unwritten is abstract too, with the same error.
What can exist is a Shape ptr or a byref Shape parameter, pointing at a complete type.
Seeing the table
tiko --dump-vtable prints each type's table of virtual methods. For shapes.tiko:
object vtable 0 slot(s)
shape vtable 2 slot(s)
slot 0 <abstract>
slot 1 _ZN5SHAPE4KINDEv
circle vtable 2 slot(s)
slot 0 _ZN6CIRCLE4AREAEv
slot 1 _ZN6CIRCLE4KINDEv
rect vtable 2 slot(s)
slot 0 _ZN4RECT4AREAEv
slot 1 _ZN4RECT4KINDEv
square vtable 2 slot(s)
slot 0 _ZN4RECT4AREAEv
slot 1 _ZN6SQUARE4KINDEv
vtables: 5
Every type in the family has the same two slots in the same order, Area then Kind. A derived
type copies its base's table and replaces the entries it overrides, so a call through a Shape
always looks in slot 0 for Area, whichever table it finds. Shape's slot 0 is <abstract>,
which is why a Shape cannot exist; Square's slot 0 still names Rect.Area, which it inherited.
The names are mangled the way C++ mangles them: _ZN6SQUARE4KINDEv is SQUARE::KIND().
The dump is the place to look when a call reaches the wrong method. It shows what the compiler built, and comparing it with what you meant is usually enough to find the mistake.
Virtual destructors
An object created with new and deleted through a pointer to its base is destroyed by whichever
destructor that pointer's type names:
'' vdtor.tiko -- delete through a base pointer runs the destructor the
'' pointer's type names, unless that destructor is virtual.
type Plain
declare destructor( )
id as int32
end type
destructor Plain( )
print " ~Plain"
end destructor
type PlainFile extends Plain
declare destructor( )
handle as int32
end type
destructor PlainFile( )
print " ~PlainFile: the file is closed"
end destructor
type Careful extends Object
declare virtual destructor( )
end type
destructor Careful( )
print " ~Careful"
end destructor
type CarefulFile extends Careful
declare destructor( )
handle as int32
end type
destructor CarefulFile( )
print " ~CarefulFile: the file is closed"
end destructor
print "plain destructor:"
dim as Plain ptr p = new PlainFile
delete p
print "virtual destructor:"
dim as Careful ptr q = new CarefulFile
delete q
plain destructor:
~Plain
virtual destructor:
~CarefulFile: the file is closed
~Careful
delete through a base pointer skips the derived destructor unless the base's is virtual.
delete p ran ~Plain and nothing else. PlainFile's destructor, the one that closes the file,
never ran, and nothing said so: the program compiled cleanly, and the leaked handle surfaces
somewhere else, later.
With declare virtual destructor( ) in the base, delete q finds the right destructor through the
table, runs it, and then runs the base's, in that order. A type meant to be derived from and
deleted through its base declares a virtual destructor, even an empty one.
The rule from the previous section applies to destructors as well, and this is where it costs most. A destructor is virtual only if it says so, so a middle level that declares a plain one closes the chain below it:
'' vdtormiddle.tiko -- 'virtual' is not inherited by a destructor that
'' does not say it. Each level that will be deleted through a base
'' pointer declares its destructor virtual.
type Resource extends Object
declare virtual destructor( )
end type
destructor Resource( )
print " ~Resource"
end destructor
type File extends Resource
declare destructor( ) '' not virtual
handle as int32
end type
destructor File( )
print " ~File"
end destructor
type LogFile extends File
declare destructor( )
lines as int32
end type
destructor LogFile( )
print " ~LogFile: flushed"
end destructor
type SafeFile extends Resource
declare virtual destructor( ) '' virtual again
handle as int32
end type
destructor SafeFile( )
print " ~SafeFile"
end destructor
type SafeLog extends SafeFile
declare destructor( )
lines as int32
end type
destructor SafeLog( )
print " ~SafeLog: flushed"
end destructor
print "middle destructor not virtual:"
dim as Resource ptr r = new LogFile
delete r
print "middle destructor virtual:"
r = new SafeLog
delete r
middle destructor not virtual:
~File
~Resource
middle destructor virtual:
~SafeLog: flushed
~SafeFile
~Resource
LogFile's destructor never ran, although Resource's is virtual and LogFile declares one of its
own. File declared a destructor without virtual, and from there down nothing overrides.
SafeFile says virtual again, and its descendant is destroyed properly. The same happens when
the middle type declares no destructor at all: the one the compiler writes for it is not virtual.
Every level that may be deleted through a base pointer declares virtual destructor.
Asking what an object really is
Sometimes the code holding an Animal needs something only a Dog has:
'' isdown.tiko -- asking what an object really is, and treating it as
'' that type once the answer is yes.
type Animal extends Object
declare virtual function Speak( ) as ustring
end type
function Animal.Speak( ) as ustring
return "..."
end function
type Dog extends Animal
declare virtual function Speak( ) as ustring override
tricks as int32
end type
function Dog.Speak( ) as ustring
return "woof"
end function
type Puppy extends Dog
declare function Speak( ) as ustring override
end type
function Puppy.Speak( ) as ustring
return "yip"
end function
sub Inspect( byref a as Animal )
print a.Speak( );
if a is Dog then
'' a really is a Dog (or something derived from one)
dim as Dog ptr d = cast( Dog ptr, @a )
print ", knows"; d->tricks; " tricks";
end if
if a is Puppy then print ", and is a puppy";
print
end sub
dim as Animal cat
dim as Dog rex
rex.tricks = 3
dim as Puppy pup
pup.tricks = 1
Inspect( cat )
Inspect( rex )
Inspect( pup )
...
woof, knows 3 tricks
yip, knows 1 tricks, and is a puppy
a is Dog is true when the object is a Dog or anything derived from one, which is why the
puppy passes both tests. Once it is true, cast( Dog ptr, @a ) treats the same object as a Dog.
The cast itself checks nothing, so always guard it with is.
is needs the type to extend Object, because the answer is read from the hidden table pointer,
and it only accepts a type derived from the variable's own type: asking whether an Animal is
an Animal is refused (TKO0798), and asking it of a type with no table is TKO0799. fbc refuses
both too.
Use is sparingly. A chain of if a is Dog … elseif a is Cat … is the select case this chapter
set out to remove, wearing different clothes. If the question is "what should this animal do", the
answer is a virtual method.
Passing by value
'' slicing.tiko -- a derived object passed by value arrives as a copy of
'' its base part, and only that.
type Animal extends Object
declare virtual function Speak( ) as ustring
legs as int32
end type
function Animal.Speak( ) as ustring
return "..."
end function
type Dog extends Animal
declare function Speak( ) as ustring override
tricks as int32
end type
function Dog.Speak( ) as ustring
return "woof"
end function
sub ByReference( byref a as Animal )
print "byref: "; a.Speak( ); ", a Dog? "; ( a is Dog )
end sub
sub ByValue( byval a as Animal )
print "byval: "; a.Speak( ); ", a Dog? "; ( a is Dog )
end sub
dim as Dog rex
rex.tricks = 3
ByReference( rex )
ByValue( rex )
dim as Animal copy = rex
print "copied: "; copy.Speak( )
'' The hidden field: one pointer, to the type's table of methods.
print "sizes:"; sizeof( Object ); sizeof( Animal ); sizeof( Dog )
byref: woof, a Dog? -1
byval: ..., a Dog? 0
copied: ...
sizes: 8 16 24
Passing a derived object by value keeps only its base part. ByValue received a fresh
Animal, made by copying the Animal part of rex, and that copy is an Animal through and
through. It speaks ..., it is not a Dog, and tricks is gone. The same happens on assignment:
dim as Animal copy = rex is a plain Animal.
Nothing warns about this, because nothing is wrong with the program from the compiler's point of
view: an Animal was asked for and an Animal was made. Pass an object of a type with virtual
methods byref, or by pointer, never byval.
The last line of the output is the hidden field from the previous section made visible. Object
is eight bytes, the table pointer and nothing else. Animal adds legs and rounds up to 16, and
Dog adds tricks and rounds to 24.
Composition, and abstract types as contracts
Inheritance says "is a". A Bicycle is a Vehicle, and a Square is a Rect. When the
relationship is "has a" or "uses a", inheritance is the wrong tool, even when it would save typing:
a Circle that extends a Point to borrow its x and y can be passed to every procedure that
expects a point, and none of them meant a circle. Give the Circle a Point field instead.
'' pluginhost.tiko -- an abstract type as the contract, and a host that
'' is built from parts rather than derived from one.
'' The contract: anything that can transform a line of text.
type Plugin extends Object
declare abstract function Name( ) as ustring
declare abstract function Apply( byref text as ustring ) as ustring
declare virtual destructor( )
end type
destructor Plugin( )
end destructor
type Shout extends Plugin
declare function Name( ) as ustring override
declare function Apply( byref text as ustring ) as ustring override
end type
function Shout.Name( ) as ustring
return "shout"
end function
function Shout.Apply( byref text as ustring ) as ustring
return ucase( text ) & "!"
end function
type Reverse extends Plugin
declare function Name( ) as ustring override
declare function Apply( byref text as ustring ) as ustring override
end type
function Reverse.Name( ) as ustring
return "reverse"
end function
function Reverse.Apply( byref text as ustring ) as ustring
dim as ustring result
for i as int32 = len( text ) to 1 step -1
result &= mid( text, i, 1 )
next
return result
end function
'' The host HAS plugins; it is not one. It owns them, so it deletes
'' them -- through Plugin ptr, which is why the destructor is virtual.
type Host
declare destructor( )
declare sub Add( byval p as Plugin ptr )
declare function Run( byref text as ustring ) as ustring
plugins( any ) as Plugin ptr
end type
destructor Host( )
for i as int32 = lbound( plugins ) to ubound( plugins )
delete plugins( i )
next
end destructor
sub Host.Add( byval p as Plugin ptr )
redim preserve plugins( 0 to ubound( plugins ) + 1 )
plugins( ubound( plugins ) ) = p
end sub
function Host.Run( byref text as ustring ) as ustring
dim as ustring t = text
for i as int32 = lbound( plugins ) to ubound( plugins )
t = plugins( i )->Apply( t )
print " after "; plugins( i )->Name( ); ": "; t
next
return t
end function
scope
dim as Host h
h.Add( new Reverse )
h.Add( new Shout )
print h.Run( "plugins" )
end scope
after reverse: snigulp
after shout: SNIGULP!
SNIGULP!
Plugin is all contract: two abstract methods and a virtual destructor, with no data. Any type that
extends it and fills in Name and Apply is a plugin, and Host can run it without knowing what
it is. Written this way, the contract is an abstract type.
Host is the other half. It is not a plugin and does not extend one. It has plugins: it owns
them, runs them in order, and deletes them in its destructor. The deletes go through Plugin ptr,
which is why Plugin's destructor is virtual. Adding a third plugin means writing one new type;
Host does not change.
A type extends exactly one base, and that is where this model runs out. A Plugin that must
also be something else, a second contract on the same type, a small value that should follow the
contract without carrying a table pointer: none of them fits in one chain. Part 2 is the answer.
Part 2: Interfaces
Part 1's contracts are base types, and a type has one base. A Drawing that is both a shape and a
document, a Point that should be printable without carrying a table pointer, a library type that
already extends something else: none of them fits. An interface is a contract that is not a
base. Any type can take on as many as it needs, and keep its own base if it has one.
A contract any type can take on
'' ifaceshapes.tiko -- the shapes loop again, with an interface as the
'' contract and classes that implement it.
interface IShape
function Area( ) as float64
function Kind( ) as ustring
end interface
class Circle
implements IShape
declare constructor( byval r as float64 )
declare function Area( ) as float64 implements IShape.Area
declare function Kind( ) as ustring implements IShape.Kind
r as float64
end class
constructor Circle( byval r as float64 )
this.r = r
end constructor
function Circle.Area( ) as float64
return 3.14159 * r * r
end function
function Circle.Kind( ) as ustring
return "circle"
end function
class Rect
implements IShape
declare constructor( byval w as float64, byval h as float64 )
declare function Area( ) as float64 implements IShape.Area
declare function Kind( ) as ustring implements IShape.Kind
w as float64
h as float64
end class
constructor Rect( byval w as float64, byval h as float64 )
this.w = w : this.h = h
end constructor
function Rect.Area( ) as float64
return w * h
end function
function Rect.Kind( ) as ustring
return "rectangle"
end function
'' Written once, against the contract only.
sub Describe( byval s as IShape )
print s.Kind( ); ", area"; s.Area( )
end sub
dim as Circle c = Circle( 1 )
dim as Rect r = Rect( 2, 3 )
dim as IShape shapes( 0 to 1 )
shapes( 0 ) = c '' an object converts to an interface it
shapes( 1 ) = @r '' implements, and so does a pointer to one
for i as int32 = 0 to 1
Describe( shapes( i ) )
next
print "an IShape value is"; sizeof( IShape ); " bytes"
circle, area 3.14159
rectangle, area 6
an IShape value is 16 bytes
interface IShape lists what a shape can do and nothing else: no fields, no code, no
constructor. Every member is public and abstract, so there is nothing to write on one but its
signature. An interface is never created by itself.
class Circle is a type that always extends Object. It is the same as
type Circle extends Object, written so that the object model is the default. Its body starts with
the contracts it takes on, implements IShape, and then each method says which member of the
contract it provides: declare function Area( ) as float64 implements IShape.Area. The mapping is
explicit. A public method called Area does not become IShape.Area until it says so, and a
member the class forgets is an error that names it:
'' missing.tiko -- a member of the contract with no method mapped
'' to it.
interface IShape
function Area( ) as float64
function Kind( ) as ustring
end interface
class Circle
implements IShape
declare function Area( ) as float64 implements IShape.Area
r as float64
end class
function Circle.Area( ) as float64
return 3.14159 * r * r
end function
missing.tiko(10,16): error TKO1000: 'circle' does not implement 'ishape.kind'; add 'implements ishape.kind' to one of its methods
|
10 | implements IShape
| ^
missing.tiko(6,5): note: 'kind' is required by interface 'ishape', declared here
|
6 | function Kind( ) as ustring
| ^
An interface value is a reference. dim as IShape shapes( 0 to 1 ) holds two of them, and each
is the address of an object together with that object's table for IShape: sixteen bytes, as the
last line says. A Circle converts to IShape implicitly, and so does a pointer to one. Members
are called with ., never ->, because the value is not a pointer you can follow. Describe
is written against IShape alone, and runs unchanged for every type that implements it.
Two contracts at once
'' twocontracts.tiko -- one type, two contracts, and a member name the
'' two contracts both use for different things.
interface IShape
function Area( ) as float64
function Name( ) as ustring '' what the shape is called
end interface
interface IDocument
function Name( ) as ustring '' the file it is saved in
sub Save( )
sub Close( )
end interface
interface IResource
sub Close( )
end interface
class Drawing
implements IShape, IDocument, IResource
declare function Area( ) as float64 implements IShape.Area
declare function Title( ) as ustring implements IShape.Name
declare function FileName( ) as ustring implements IDocument.Name
declare sub Close( ) implements IDocument.Close, IResource.Close
w as float64 = 4
h as float64 = 2
private:
declare sub WriteOut( ) implements IDocument.Save
end class
function Drawing.Area( ) as float64
return w * h
end function
function Drawing.Title( ) as ustring
return "floor plan"
end function
function Drawing.FileName( ) as ustring
return "plan.txt"
end function
sub Drawing.Close( )
print "closed"
end sub
sub Drawing.WriteOut( )
print "saving "; FileName( )
end sub
dim as Drawing d
dim as IShape s = d
dim as IDocument doc = d
dim as IResource res = d
print s.Name( ); " / "; doc.Name( ); ", area"; s.Area( )
doc.Save( ) '' reaches the private WriteOut
res.Close( )
floor plan / plan.txt, area 8
saving plan.txt
closed
Drawing takes on three interfaces on one implements line. Part 1 could not express any of this:
- Two contracts both have a
Name, and they mean different things. The shape's name is its title, the document's is a file name. They are different members, soDrawingmaps each to its own method:TitleimplementsIShape.Name,FileNameimplementsIDocument.Name. A method's name does not have to match the member's. - One method can provide several members.
CloseimplementsIDocument.CloseandIResource.Closeat once, because closing the drawing means the same thing to both. - A private method can implement a member.
WriteOutis underprivate:, so code outsideDrawingcannot call it directly (TKO0625), yetdoc.Save( )reaches it. Saving is something you do to a document, not to a drawing that happens to be one.
Small values
A type that implements an interface does not need a table pointer of its own. The table travels in
the interface value instead, so a plain type can implement one:
'' ifacevalue.tiko -- a plain type implements an interface and stays
'' the size of its fields.
interface IShow
function Show( ) as ustring
end interface
type Point
implements IShow
x as int32
y as int32
declare function Show( ) as ustring implements IShow.Show
end type
function Point.Show( ) as ustring
return "(" & x & ", " & y & ")"
end function
'' The same contract written as an abstract base type.
type Showable extends Object
declare abstract function Show( ) as ustring
end type
type OldPoint extends Showable
x as int32
y as int32
declare function Show( ) as ustring override
end type
function OldPoint.Show( ) as ustring
return "(" & x & ", " & y & ")"
end function
dim as Point p = ( 3, 4 )
dim as IShow v = p
print v.Show( )
print "Point:"; sizeof( Point ); " bytes"
print "OldPoint:"; sizeof( OldPoint ); " bytes"
(3, 4)
Point: 8 bytes
OldPoint: 16 bytes
Point is eight bytes, its two fields. OldPoint follows the same contract written as an abstract
base type, and every one of them pays eight bytes more for its table pointer. An array of a million
points is 8 MB one way and 16 MB the other. The interface's sixteen bytes are paid only where an
interface value is actually held.
Asking an interface what it holds
'' ifacequery.tiko -- asking what is behind an interface value.
interface IShow
function Show( ) as ustring
end interface
interface ISize
function Size( ) as int32
end interface
class Note
implements IShow, ISize
declare function Show( ) as ustring implements IShow.Show
declare function Size( ) as int32 implements ISize.Size
text as ustring = "buy milk"
end class
function Note.Show( ) as ustring
return text
end function
function Note.Size( ) as int32
return len( text )
end function
class Label
implements IShow
declare function Show( ) as ustring implements IShow.Show
end class
function Label.Show( ) as ustring
return "label"
end function
sub Inspect( byval v as IShow )
print v.Show( ); ":"
'' A cross cast: does the same object ALSO implement ISize?
dim as ISize sz = trycast( ISize, v )
if sz <> nullptr then print " size"; sz.Size( )
'' A downcast: is it a Note?
if v is Note then
dim as Note ptr np = cast( Note ptr, v )
print " a note saying "; np->text
end if
print " is ISize: "; cbool( v is ISize )
end sub
dim as Note n
dim as Label l
Inspect( n )
Inspect( l )
buy milk:
size 8
a note saying buy milk
is ISize: true
label:
is ISize: false
is works with an interface on either side. v is Note asks whether the object behind v is a
Note, or derives from one; v is ISize asks whether it implements ISize. Neither needs the
object's type to be related to IShow.
trycast answers the same questions with a value. trycast( ISize, v ) is a cross cast:
the same object, seen through another interface it implements, or an empty interface value, equal
to nullptr, when it implements no such thing. trycast( Note ptr, v ) is the checked downcast.
cast( Note ptr, v ) is the unchecked one, which is why the listing asks v is Note first: a
wrong cast is undefined behaviour, exactly as it is in Part 1.
Who owns the object
An interface value refers to an object. It does not own it, copy it or keep it alive. The compiler refuses the one case it can see, a temporary that would be gone by the next statement:
'' temporary.tiko -- an interface value refers to an object, and a
'' temporary would be gone before the reference was used.
interface IShow
function Show( ) as ustring
end interface
type Tag
implements IShow
text as ustring
declare function Show( ) as ustring implements IShow.Show
end type
function Tag.Show( ) as ustring
return text
end function
function MakeTag( ) as Tag
dim as Tag t
t.text = "new"
return t
end function
dim as IShow v = MakeTag( )
print v.Show( )
temporary.tiko(24,14): error TKO1012: an interface reference cannot refer to a temporary; store the object in a variable first
|
24 | dim as IShow v = MakeTag( )
| ^
delete through an interface is refused too (TKO1013): the interface does not know how the
object was made, or whether it was made with new at all.
An interface value can outlive its object. Return an IShape that refers to a local Circle,
or keep one in a list after the Circle it refers to has gone out of scope, and it compiles
cleanly. The next call through it reads whatever is in that memory by then.
Keep the owning variable, or the owning pointer, as the object's own type, and make sure it lives at least as long as every interface value that refers to it. A host that holds plugins as interfaces is given them by code that owns them; it does not own them itself.
Classes and interfaces together
Interfaces do not replace class inheritance. They sit beside it, and a call through an interface honours every override below the class that implements it:
'' ifaceclass.tiko -- a class hierarchy behind an interface: the call
'' through the interface reaches the most derived override.
interface ISpeaker
sub Speak( )
end interface
class Animal
implements ISpeaker
declare virtual sub Speak( ) implements ISpeaker.Speak
end class
sub Animal.Speak( )
print "..."
end sub
class Dog
inherits Animal
declare virtual sub Speak( ) override
end class
sub Dog.Speak( )
print "woof"
end sub
class Puppy
inherits Dog
declare sub Speak( ) override
end class
sub Puppy.Speak( )
print "yip"
end sub
dim as Animal a
dim as Dog d
dim as Puppy p
dim as ISpeaker speakers( 0 to 2 )
speakers( 0 ) = a
speakers( 1 ) = d
speakers( 2 ) = p
for i as int32 = 0 to 2
speakers( i ).Speak( )
next
...
woof
yip
class Dog names its base with an inherits line, the class's spelling of extends, at the top
of its body. Animal implements ISpeaker.Speak with a virtual method, so an ISpeaker that
refers to a Dog or a Puppy reaches their overrides. The rule from Part 1 still holds: Dog
writes virtual … override so that Puppy can override it in turn.
Use this only when you want a base class that supplies code its subclasses replace. The default
style is the one in the previous sections: each class implements the interfaces it needs, with no
base class and no virtual anywhere, and shared code goes in a procedure that takes the interface.
Generic interfaces and constraints
An interface can take type parameters. Each instance, IComparable( of Money ) or
IComparable( of Date ), is an interface of its own. And a generic procedure or type can require
its type argument to implement one, with as in its parameter list:
'' ifacegeneric.tiko -- a generic interface, and a generic procedure
'' that requires one of its type argument.
interface IComparable( of T )
function CompareTo( byref other as T ) as int32
end interface
class Money
implements IComparable( of Money )
declare constructor( byval cents as int32 = 0 )
declare function CompareTo( byref other as Money ) as int32 _
implements IComparable( of Money ).CompareTo
cents as int32
end class
constructor Money( byval cents as int32 )
this.cents = cents
end constructor
function Money.CompareTo( byref other as Money ) as int32
return sgn( cents - other.cents )
end function
'' T must implement IComparable( of T ); the body calls T's own
'' CompareTo directly.
function Largest( of T as IComparable( of T ) )( a( ) as T ) as T
dim as int32 best = lbound( a )
for i as int32 = lbound( a ) + 1 to ubound( a )
if a( i ).CompareTo( a( best ) ) > 0 then best = i
next
return a( best )
end function
dim as Money prices( 0 to 2 ) = _
{ Money( 250 ), Money( 1999 ), Money( 75 ) }
print "largest:"; Largest( prices( ) ).cents
largest: 1999
of T as IComparable( of T ) is a constraint: whatever T is, it must implement
IComparable of itself. Several are written in braces, of K as { IComparable( of K ), IShow }.
The constraint is checked once, where Largest is used, and a type argument that falls short is
refused there:
'' constraint.tiko -- a type argument that does not implement what the
'' parameter requires is refused where the generic is used.
interface IComparable( of T )
function CompareTo( byref other as T ) as int32
end interface
function Largest( of T as IComparable( of T ) )( a( ) as T ) as T
return a( lbound( a ) )
end function
dim as int32 counts( 0 to 2 ) = { 3, 1, 2 }
print Largest( counts( ) )
constraint.tiko(13,14): error TKO1022: type argument 'int32' does not implement 'icomparable( of int32 )', which 'largest( of t )' requires
|
13 | print Largest( counts( ) )
| ^
Without the constraint, Largest( counts( ) ) would fail inside the body, at a( i ).CompareTo,
with an error about a member that int32 does not have, and the reader would have to work out why
from there. A constraint moves the error to the call that caused it.
A constraint does not change how the body runs. Largest is still compiled once for each type
it is used with, and inside it a( i ).CompareTo( … ) is an ordinary call to Money.CompareTo: no
interface value and no table. Chapter 17 covers generics in full.
Choosing between them
| You want | Use |
|---|---|
| A contract that unrelated types follow | an interface |
| A type to follow two contracts, or to keep its own base while following one | interfaces |
| A small value type that follows a contract | a plain type that implements an interface |
| A base class that supplies code its subclasses replace | extends (or inherits) with virtual |
is and delete through the base, with the base owning nothing else | the classic model, with a virtual destructor |
Generic code that needs something of T | a constraint, of T as IFoo |
| A layout that C or COM defines | the classic model: an abstract type that extends Object |
The two mix freely. A class can inherit a base and implement interfaces, a type can extend a
class, and a classic base can implement an interface with virtual methods that its descendants
override.
What it costs
- Every object of a type that extends
Objectcarries one pointer, eight bytes, plus whatever padding it causes. A million small objects pay eight megabytes for it. - A virtual call is an indirect call: load the table pointer, load the slot, call through it. That is a few instructions, but it prevents the call from being inlined, which matters inside a hot loop over small methods.
- A non-virtual method costs nothing extra, inherited or not. Make methods virtual because a derived type needs to replace them, not by default.
iswalks the chain of base types, so it costs a little more the deeper the hierarchy.- Abstract types and
overrideare compile-time only. They cost nothing at run time. - An interface value is sixteen bytes, the object's address and its table for that interface, and a call through it is one indirect call, the same as a virtual call. A class whose implementing method is virtual adds a short hop through its own table, so an override is reached.
- Implementing an interface adds nothing to an object. A plain
typestays the size of its fields; the table is in the interface value. trycastandiswith an interface search a small table at run time. Converting an object to an interface, or an interface to one it inherits, does not.- Mappings and constraints are compile-time only. A constrained generic calls
T's methods directly.
Summary
type B extends AstartsBas a completeA, base fields first.base( … )chooses the base constructor, which runs before the derived body;base.Method( )calls the base's version.- A base with no default constructor forces every derived type to write a copy constructor
(
TKO0926). Default the base constructor's parameters instead. - Without
virtual, a call is chosen by the type the compiler sees. A method of the same name on a derived type hides the base's; it does not replace it for code that sees the base. extends Objectadds a table pointer.virtualmethods are looked up in it at run time;overridereplaces one (TKO0621if there is nothing to replace);abstractdeclares one with no body. An override is not virtual unless you also writevirtual, so a type derived from it hides the method instead of overriding it.- A type with an abstract method cannot be created (
TKO0620).--dump-vtableshows the table the compiler built. - Give a base that will be deleted through a pointer a virtual destructor, or the derived
destructor never runs, and repeat
virtualat every level below it: a plain destructor in the middle stops the chain. a is Tis true forTand anything derived from it; guard everycast( T ptr, @a )with it. Prefer a virtual method to a chain ofistests.- Passing by value slices an object to its base part. Pass objects with virtual methods
byref. - Use inheritance for "is a" and composition for "has a". A type extends one base, so an abstract type as a contract uses that base up.
- An
interfacelists methods and properties with no data or code. Atypeorclasstakes it on with animplementsline and maps each member withimplements IFoo.Member, explicitly; a missing one isTKO1000.classis a type that always extendsObject, with its base on aninheritsline. - A type may implement any number of interfaces. Members of the same name in two interfaces are different members; a method's name need not match its member's, one method may provide several, and a private method may provide one.
- An interface value is a 16-byte reference. It converts from any implementing object or pointer,
is called with
., and does not own its object: temporaries are refused (TKO1012),deletethrough it is refused (TKO1013), and the object must outlive it. isandtrycasttake interfaces on either side;trycastanswers null instead of guessing.- A plain
typecan implement an interface without a table pointer of its own. - Generic interfaces are interfaces per instance. A constraint,
of T as IFoo, is checked where the generic is used (TKO1022) and costs nothing in the body.
Exercises
- Add a
Trianglewith a base and a height to the shapes program, without changingShape,Circleor the loop. What did you have to write, and what did you not? {solution="ex14-01"} - Write a
Counterplugin whoseApplyappends the length of the text and counts its calls, and whose destructor prints the count. Create it withnew, use it through aPlugin ptr, and delete it through that pointer. Which line makes the destructor run? {solution="ex14-02"} - Remove
virtualfromvdtor.tiko'sCarefuldestructor. Predict the output, then run it. - Change
ByValueinslicing.tikoto take its parameterbyref. Which lines of output change, and why? - A
Stackthat extendsArray, or aStackthat has anArrayfield: argue for one. What can a caller do to the first that a stack should not allow? - Add a
Triangletoifaceshapes.tiko. Then giveRecta second interface,IScalable, withsub Scale( byval k as float64 ), and scale a rectangle through it. Check withisthat a triangle is not scalable. Which existing code did you change? {solution="ex14-03"} - Rewrite
pluginhost.tikowith anIPlugininterface. Who deletes the plugins now, and why can the host no longer do it through the interface?