Beginners Guide to Types as Objects

FreeBASIC

Beginners Guide to Types as Objects
 
Introduction

This tutorial is aimed at people who want to know more about the new features added to Type, commonly being referred to as 'types as objects', and 'that OOP stuff'. It aims to walk you through these new features, so is aimed at people who don't really understand it yet, but want to learn. A Type in FreeBASIC is an aggregate data type, like a struct in C, or a record in Pascal. Here's just a short sample of typical Type usage.

Type person_info
  first_name As String
  last_name As String
  house_number As Integer
  street_name As String
  town As String
End Type


In this usage it's used as a kind of container for related data; in this example it could be as an entry in an address book. With the new features, however, it can be used more like the class in C++, in that it can do much more than contain just simple fields of data. It becomes a way to express an idea of an object, and this makes object oriented programming much simpler. We will now look at these new features.

Property

We'll start by looking at property. When you add a property to a Type, you access it as if it were an ordinary member, but what happens, is instead of just getting or setting a variable as normal, it calls a function instead. Take a look at this example:

Type bar
  Declare Property x() As Integer
  Declare Property x(ByVal n As Integer)
  p_x As Integer
End Type

Property bar.x() As Integer
  Print "bar.x()"
  Property = p_x
End Property

Property bar.x(ByVal n As Integer)
  Print "bar.x(ByVal n As Integer)"
  p_x = n
End Property

'---

Dim foo As bar

foo.x = 5
Print foo.x


We include in our Type some declarations for a Property; they are very similar to ordinary function declarations. The first one declares a getter, the second a setter. The p_x member is just an ordinary Integer member.

Next we write the code for the properties; again, the syntax is very similar to that of normal functions. Note the way we return a value: instead of Function = value, we do Property = value. You can do Return value as well. Also note that you can refer to the member directly as p_x; you can also use the keyword this, for example this.p_x = n; using this isn't usually needed, but it can help in some ambiguous circumstances.

Then follows some testing code; this shows how we can use the property as if it were any ordinary member. When you run the program it will also print to screen to show that the property get/set code has been called.

Now this code is fairly trivial, but as you get used to the idea you'll see it can be put to some good uses. Imagine as an example you are writing a GUI, and the TYPE represents a button on the screen, you could do button.text = "Hello World!", and make the property code update the screen to show the changes. Or maybe you are using the Type to maintain some kind of list; you could do list.size += 10 and then put some code in your property to make the list larger.

Constructor/Destructor

Constructors are functions that are called when the Type gets created - when you use Dim, for example. A Destructor is a function that gets called when the Type goes out of scope; this could be when the program ends, for a Type in the main code, or when a function ends, for a local Type. Look at the following example, expanded from the last.

Type bar
  Declare Constructor()
  Declare Destructor()
  Declare Property x() As Integer
  Declare Property x(ByVal n As Integer)
  p_x As Integer Ptr
End Type

Constructor bar()
  Print "Constructor bar()"
  p_x = Allocate(SizeOf(Integer))
  *p_x = 10
End Constructor

Destructor bar()
  Print "Destructor bar()"
  Deallocate(p_x)
End Destructor

Property bar.x() As Integer
  Print "bar.x()"
  Property = *p_x
End Property

Property bar.x(ByVal n As Integer)
  Print "bar.x(ByVal n As Integer)"
  *p_x = n
End Property

'---

Dim foo As bar

Print foo.x
foo.x = 5
Print foo.x


Again the syntax is somewhat similar to normal functions. Note that this time I changed p_x to be an Integer ptr. The constructor then Allocates the memory for this when foo is created, and gives it a default value; then it De-Allocates this memory once it is destroyed. So you can use Constructors and Destructors to set things up for you, then clean up once its finished with. Again a trivial example, but bring back the example of some kind of list, and having it set the list up for you, and clean it up when it's finished with can be quite handy.

Methods

You can also have regular Subs and Functions inside your Type; in some terminology, these are referred to as methods. We'll carry on our example:

Type bar
  Declare Constructor()
  Declare Destructor()
  Declare Property x() As Integer
  Declare Property x(ByVal n As Integer)
  Declare Sub Mul5()
  Declare Function Addr() As Integer Ptr
  p_x As Integer Ptr
End Type

Constructor bar()
  Print "Constructor bar()"
  p_x = Allocate(SizeOf(Integer))
  *p_x = 10
End Constructor

Destructor bar()
  Print "Destructor bar()"
  Deallocate(p_x)
End Destructor

Property bar.x() As Integer
  Print "bar.x()"
  Property = *p_x
End Property

Property bar.x(ByVal n As Integer)
  Print "bar.x(ByVal n As Integer)"
  *p_x = n
End Property

Sub bar.mul5()
  *p_x *= 5
End Sub

Function bar.Addr() As Integer Ptr
  Function = p_x
End Function

'---

Dim foo As bar

Print foo.x
foo.x = 5
Print foo.x
foo.mul5()
Print foo.x
Print "address p_x points to", foo.Addr()


So this time we added a Sub, that multiplies the integer pointed to by p_x by five, and a function that gets the memory address that the pointer holds.

Private/Public

By default all of the members of the bar type are public; that means that we can read/write or call them. However, sometimes you might want to make them private. Take for example our member p_x; we can currently do Print *foo.p_x, and it will allow us to print the value it points to. We might want to make it private, so that only the members of the bar type (the constructor, destructor, property, and methods) can access it. That way we can make sure we only deal with p_x by the ways we choose. If for example we did 'DeAllocate(foo.p_x)' in our main code, then when the destructor runs, it would try to free it again, known as a 'double free'. Change the Type declaration as follows:

Type bar
  Declare Constructor()
  Declare Destructor()
  Declare Property x() As Integer
  Declare Property x(ByVal n As Integer)
  Declare Sub Mul5()
  Declare Function Addr() As Integer Ptr
Private:
  p_x As Integer Ptr
End Type


Now try adding Print *foo.p_x to the main code and compile it. You'll get a message from fbc "error 173: Illegal member access, found 'p_x' in 'Print *foo.p_x'", showing that indeed the compiler is now enforcing the fact we made p_x private. When you use private: or public:, any members following that statement follow the rule. Here's a rather pointless example just to show the syntax:

Type bar
Private:
  a As Integer
  b As Integer
Public:
  c As Integer
  d As Integer
Private:
  e As Integer
End Type


In the above type, the members a, b, and e are private; c and d are public.

Operator overloading

Operator overloading is a way of telling the compiler what to do in the case where we want to perform some kind of operation involving our Type. Take this example:

Type bar
  n As Integer
End Type

Dim As bar x, y, z

z = x + y


Now normally the compiler will throw an error when it sees this, as it has no idea how to add together two Types, but we can define what we want to happen. Here's how:

Type bar
  n As Integer
End Type

Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
  Operator = Type(lhs.n + rhs.n)
End Operator

Dim As bar x, y, z

x.n = 5
y.n = 10
z = x + y
Print z.n


In this code, I use lhs and rhs to refer to the left and right hand side operands of the operator. Note also the expression type(lhs.n + rhs.n); this builds the Type that will be returned. If you had a type like:

Type bar
  x As Integer
  y As Integer
  z As Integer
End Type


Then you would build it like type(xpart, ypart, zpart).

Most or all operators can be overloaded, and most of them are binary ops, meaning they have two operands like the + example above. Some are unary ops having only a right hand side, like Not and unary minus. They would be done like 'Operator Not(ByRef rhs As bar) As bar'.

There are some special cases where they have to be declared inside the Type; these are the assignment operators and casts.

Assignment operators are things like += -= mod= etc, and also Let. Let is used when you do an assignment like:

Dim As bar foo
Dim As Integer x
foo = x


And casts are kind of the reverse; they are used when you cast to another datatype like:

Dim As bar foo
Dim As Integer x
x = foo


Heres a short example using Let and Cast:

Type bar
  n As Integer
  Declare Operator Let(ByRef rhs As Integer)
  Declare Operator Let(ByRef rhs As String)
  Declare Operator Cast() As String
End Type

Operator bar.Let(ByRef rhs As Integer)
  n = rhs
End Operator

Operator bar.Let(ByRef rhs As String)
  n = Val(rhs)
End Operator

Operator bar.Cast() As String
  Operator = Str(n)
End Operator

Operator +(ByRef lhs As bar, ByRef rhs As bar) As bar
  Operator = Type(lhs.n + rhs.n)
End Operator

Dim As bar x, y, z

x = 5
y = "10"
z = x + y
Print z


You need to have separate lets and casts for each data type you want to support. The operators that need declaring within the type are known as non-static, and the ones that don't are known as global. There is a technical reason for this; the non-static ones need to know which instance (in the technical jargon, in our example above, we would say that x is an instance of bar) of the Type they are referring to, and this is accomplished by a hidden 'this' reference. This hidden 'this' reference is how the other members like operators and methods know which instance of the Type the call refers to. Most operators can be overloaded; here's a list of the ones that currently can be:

Assignment ops:
let, +=, -=, *=, /=, \=, mod=, shl=, shr=, and=, or=, xor=, imp=, eqv=, ^=
Unary ops:
-, not, @, *, ->
Binary ops:
+, -, *, /, \, mod, shl, shr, and, or, xor, imp, eqv, ^, =, <>, <, >, <=, >=

Overloaded Constructors/Methods

As with normal functions, our Type's constructor and methods can be overloaded. For constructors, this provides a way to specify details on how the instance should be constructed. Here's a short example:

Type bar
  Declare Constructor()
  Declare Constructor(ByVal initial_val As Integer)
  x As Integer
End Type

Constructor bar()
  x = 10
End Constructor

Constructor bar(ByVal initial_val As Integer)
  x = initial_val
End Constructor

Dim foo As bar
Print foo.x

Dim baz As bar = bar(25)
Print baz.x


The first Constructor, that had no arguments, is known as the default constructor. This sets up foo.x to an initial value of 10. However, we have also specified another constructor that will accept an initial value. Note the way we ask for this to be called Dim baz As bar = bar(25). You can also leave out the default constructor, and then you will always have to specify the initial value using the constructor that takes an argument. You can't have an overloaded destructor, because there's no way to manually choose which one would be called.

Overloaded methods are very similar:

Type bar
  Declare Sub foo()
  Declare Sub foo(ByVal some_value As Integer)
  Declare Sub foo(ByRef some_value As String, ByVal some_other As Integer)
  x As Integer
End Type


They work just they same as normal overloaded functions.

Closing

I hope this tutorial has been useful for you, although there are still a few things left to learn; if you've got this far, it shouldn't be too hard for you to pick them up. There is some more information available in the wiki and on the forums, and also in part 2 of this tutorial, available here - Beginners Guide to Types as Objects (Part 2)

More reading