Beginners Guide to Types as Objects (Part 2)
Welcome to the second part of the tutorial, In this part I assume that you have read through Part 1, tried the examples, and experimented with some tests of your own. I'll now cover some topics that I didn't include in Part 1.
Indexed property.
An indexed property is a property that behaves like an array, except that like in the case of a regular property, a function gets called when you access it. I'll start with a very short example just to show the syntax.
Type foo
Declare Property bar(ByVal index As Integer, ByVal value As Integer)
Declare Property bar(ByVal index As Integer) As Integer
dummy As Integer
End Type
Property foo.bar(ByVal index As Integer, ByVal value As Integer)
Print "Property set, index=" & index & ", value=" & value
End Property
Property foo.bar(ByVal index As Integer) As Integer
Print "Property get, index=" & index
Property = 0
End Property
Dim baz As foo
baz.bar(0) = 42
Print baz.bar(0)
Declare Property bar(ByVal index As Integer, ByVal value As Integer)
Declare Property bar(ByVal index As Integer) As Integer
dummy As Integer
End Type
Property foo.bar(ByVal index As Integer, ByVal value As Integer)
Print "Property set, index=" & index & ", value=" & value
End Property
Property foo.bar(ByVal index As Integer) As Integer
Print "Property get, index=" & index
Property = 0
End Property
Dim baz As foo
baz.bar(0) = 42
Print baz.bar(0)
As you can see, the declaration for our indexed property is very similar to a regular one, except this time we add an argument for the index. I include a dummy integer member, because a type must have at least one data member. As you can see, the property is then used with (0), to denote we want to get/set the zeroth index, just the same as we would for an ordinary array. Now I'll show you a slightly more useful example, and I will describe it:
Type foo
Declare Constructor(ByVal num_elements As Integer)
Declare Destructor()
Declare Property bar(ByVal index As Integer, ByVal value As Integer)
Declare Property bar(ByVal index As Integer) As Integer
Private:
x As Integer Ptr
size As Integer
End Type
Constructor foo(ByVal num_elements As Integer)
x = CAllocate(num_elements * SizeOf(Integer))
size = num_elements
End Constructor
Destructor foo()
Deallocate(x)
End Destructor
Property foo.bar(ByVal index As Integer, ByVal value As Integer)
If (index >= 0) And (index < size) Then
x[index] = value
Else
Error 6
End If
End Property
Property foo.bar(ByVal index As Integer) As Integer
If (index >= 0) And (index < size) Then
Property = x[index]
Else
Error 6
End If
End Property
Dim baz As foo = foo(10)
baz.bar(1) = 42
Print baz.bar(1)
Declare Constructor(ByVal num_elements As Integer)
Declare Destructor()
Declare Property bar(ByVal index As Integer, ByVal value As Integer)
Declare Property bar(ByVal index As Integer) As Integer
Private:
x As Integer Ptr
size As Integer
End Type
Constructor foo(ByVal num_elements As Integer)
x = CAllocate(num_elements * SizeOf(Integer))
size = num_elements
End Constructor
Destructor foo()
Deallocate(x)
End Destructor
Property foo.bar(ByVal index As Integer, ByVal value As Integer)
If (index >= 0) And (index < size) Then
x[index] = value
Else
Error 6
End If
End Property
Property foo.bar(ByVal index As Integer) As Integer
If (index >= 0) And (index < size) Then
Property = x[index]
Else
Error 6
End If
End Property
Dim baz As foo = foo(10)
baz.bar(1) = 42
Print baz.bar(1)
This time, I've added a constructor and destructor, which will allocate and deallocate a dynamic memory array, x, with the number of elements specified in the constructor. Then when the property functions are invoked, I check if the index is within the bounds of the array, if it is then I perform the requested get or set. If the index specified is out of bounds, then 'Error 6' occurs, which is a way to abort the program with FB's 'out of bounds error', you could replace this with your own error handling routines. Try this out, by changing the code 'baz.bar(1) = 42' to 'baz.bar(10) = 42', and you'll see it in action, as we specified only 10 elements (index 0-9)
Copy constructor.
A copy constructor is a special type of constructor, that is used to make a copy from an existing object. When you write code like this:
Type foo
...
End Type
Dim As foo a
Dim As foo b = a
...
End Type
Dim As foo a
Dim As foo b = a
What happens is FreeBASIC automatically generates hidden code to construct b, by making a copy of a, this is the default copy constructor, and simply copies the data fields (members) across. We can define our own copy constructor, here's just a brief snippet to show how we declare it.
Type foo
Declare Constructor(ByRef obj As foo)
...
End Type
Declare Constructor(ByRef obj As foo)
...
End Type
This will come in very useful for a reason I will now explain.
Deep/Shallow copy.
In that previous example, where we did the code 'Dim As foo b = a', that was what is known a shallow copy, it just simply copied the data fields across, however sometimes this is not desirable, imagine that one of the members is a pointer, what will happen is that the address that pointer points to will be copied across, so both objects will point to the same memory. An example of this follows:
Type foo
x As Integer Ptr
End Type
Dim As foo a
a.x = Allocate(SizeOf(Integer))
*a.x = 42
Dim As foo b = a
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
Deallocate(a.x)
x As Integer Ptr
End Type
Dim As foo a
a.x = Allocate(SizeOf(Integer))
*a.x = 42
Dim As foo b = a
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
Deallocate(a.x)
As you see, because they both point to the same memory, changing one affects the other. As explained in the previous section on the copy constructor, FreeBASIC creates the code to do shallow copies by default. This is also true if we do an assignment like:
Dim As foo a, b
b = a
b = a
In this case also, FreeBASIC creates a default assignment operator (Let) to perform a shallow copy. In order to do deep copies, we need to define a copy constructor, and an assignment operator, that is overloaded to accept our type. Here's an example using them.
Type foo
Declare Constructor()
Declare Constructor(ByRef obj As foo)
Declare Destructor()
Declare Operator Let(ByRef obj As foo)
x As Integer Ptr
End Type
Constructor foo()
Print "Default ctor"
x = CAllocate(SizeOf(Integer))
End Constructor
Constructor foo(ByRef obj As foo)
Print "Copy ctor"
x = CAllocate(SizeOf(Integer))
*x = *obj.x
End Constructor
Destructor foo()
Print "dtor"
Deallocate(x)
End Destructor
Operator foo.Let(ByRef obj As foo)
Print "Let"
*x = *obj.x
End Operator
Dim As foo a
*a.x = 42
Dim As foo b = a 'Uses the copy constructor
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
Declare Constructor()
Declare Constructor(ByRef obj As foo)
Declare Destructor()
Declare Operator Let(ByRef obj As foo)
x As Integer Ptr
End Type
Constructor foo()
Print "Default ctor"
x = CAllocate(SizeOf(Integer))
End Constructor
Constructor foo(ByRef obj As foo)
Print "Copy ctor"
x = CAllocate(SizeOf(Integer))
*x = *obj.x
End Constructor
Destructor foo()
Print "dtor"
Deallocate(x)
End Destructor
Operator foo.Let(ByRef obj As foo)
Print "Let"
*x = *obj.x
End Operator
Dim As foo a
*a.x = 42
Dim As foo b = a 'Uses the copy constructor
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
As you can see, the copy constructor gets called on the line 'Dim As foo b = a' and this time, we allocate some memory, and copy the data in the new copy constructor, so that we can adjust x in one object without it affecting the other. If we change the main code as follows:
Dim As foo a, b
*a.x = 42
b = a 'The assignment operator (Let) gets used this time.
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
*a.x = 42
b = a 'The assignment operator (Let) gets used this time.
Print *a.x, *b.x
*a.x = 420
Print *a.x, *b.x
Then this time the assignment operator is used. Note that in the assignment operator code, we don't need to allocate any memory because it has already been allocated in the default constructor, we just need to copy the data across. The line '*x = *obj.x' performs this copy. If we had something more advanced, like a dynamic memory array, then we would need to reallocate the memory to be the correct size to fit the data being copied. Here's a more advanced version just to show that.
Type foo
Declare Constructor(ByVal num_elements As Integer)
Declare Constructor(ByRef obj As foo)
Declare Destructor()
Declare Operator Let(ByRef obj As foo)
x As Integer Ptr
size As Integer
End Type
Constructor foo(ByVal num_elements As Integer)
Print "Default ctor"
x = CAllocate(SizeOf(Integer) * num_elements)
size = num_elements
End Constructor
Constructor foo(ByRef obj As foo)
Print "Copy ctor"
x = CAllocate(SizeOf(Integer) * obj.size)
size = obj.size
For i As Integer = 0 To size - 1
x[i] = obj.x[i]
Next i
End Constructor
Destructor foo()
Print "dtor"
Deallocate(x)
End Destructor
Operator foo.Let(ByRef obj As foo)
Print "Let"
x = Reallocate(x, SizeOf(Integer) * obj.size)
size = obj.size
For i As Integer = 0 To size - 1
x[i] = obj.x[i]
Next i
End Operator
Dim As foo a = foo(5)
a.x[0] = 42
a.x[1] = 420
Dim As foo b = a 'Uses the copy constructor
Print a.x[0], a.x[1], b.x[0], b.x[1]
b.x[0] = 10
b.x[1] = 20
Print a.x[0], a.x[1], b.x[0], b.x[1]
b = a ' Now using the assignment operator
Print a.x[0], a.x[1], b.x[0], b.x[1]
Declare Constructor(ByVal num_elements As Integer)
Declare Constructor(ByRef obj As foo)
Declare Destructor()
Declare Operator Let(ByRef obj As foo)
x As Integer Ptr
size As Integer
End Type
Constructor foo(ByVal num_elements As Integer)
Print "Default ctor"
x = CAllocate(SizeOf(Integer) * num_elements)
size = num_elements
End Constructor
Constructor foo(ByRef obj As foo)
Print "Copy ctor"
x = CAllocate(SizeOf(Integer) * obj.size)
size = obj.size
For i As Integer = 0 To size - 1
x[i] = obj.x[i]
Next i
End Constructor
Destructor foo()
Print "dtor"
Deallocate(x)
End Destructor
Operator foo.Let(ByRef obj As foo)
Print "Let"
x = Reallocate(x, SizeOf(Integer) * obj.size)
size = obj.size
For i As Integer = 0 To size - 1
x[i] = obj.x[i]
Next i
End Operator
Dim As foo a = foo(5)
a.x[0] = 42
a.x[1] = 420
Dim As foo b = a 'Uses the copy constructor
Print a.x[0], a.x[1], b.x[0], b.x[1]
b.x[0] = 10
b.x[1] = 20
Print a.x[0], a.x[1], b.x[0], b.x[1]
b = a ' Now using the assignment operator
Print a.x[0], a.x[1], b.x[0], b.x[1]
This may seem quite complex at first, it's worth just reading through it again, and experimenting with the examples, it's not too tricky once you're used to it.
Passing objects to functions ByVal
The idea of deep and shallow copies also applies to passing an object to a function by value. When you pass a reference to an object (ByRef), you can modify the object, and these modifications will persist, however you can also pass by value, which will mean you can modify it without the changes persisting outside of the function. When an object is passed by value to a function, a new copy is created, and if that object has a copy constructor, then this is invoked, if it doesn't, then the hidden shallow copy is performed. Once the function ends, the objects destructor is called.
New/Delete
New and delete are special operators for dynamically allocating memory, then destroying it. Because it is used with dynamic memory, it is used with pointers. In all the examples up until now, we just used Dim to create our objects, this will create them on the stack, but by using new we can create them dynamically, which can allow more flexibility, just like using Allocate/DeAllocate with normal memory. Another important thing about new, is that you don't need to check if the pointer is NULL after new, like you would if you did allocate. If new fails, it causes an exception, which will end the program. In later versions of FreeBASIC, it is likely that some kind of try..catch mechanism will be created to allow better exception handling, but as of the time of writing, this is not yet implemented.
There are two different varieties of the new/delete. The first type, creates just a single element or object, for example:
Dim As Integer Ptr foo = New Integer
*foo = 1
Print *foo
Delete foo
*foo = 1
Print *foo
Delete foo
This will create a new Integer, then destroy it when we call delete. Remember I used ptr, because it is dynamic memory. For simple data types you can also specify a default value, by placing it in parenthesis after the data type, ie:
Dim As Integer Ptr foo = New Integer(42)
Print *foo
Delete foo
Print *foo
Delete foo
This also works for UDT's with just simple data fields:
Type foo
x As Integer
y As Integer
End Type
Dim As foo Ptr bar = New foo(1, 2)
Print bar->x, bar->y
Delete bar
x As Integer
y As Integer
End Type
Dim As foo Ptr bar = New foo(1, 2)
Print bar->x, bar->y
Delete bar
This initialization won't work for more complex types involving constructors/destructors etc., however a useful feature is that when using new/delete with objects, it also calls the constructor and destructor, try the following example:
Type foo
Declare Constructor()
Declare Destructor()
x As Integer
y As Integer
End Type
Constructor foo()
Print "ctor"
End Constructor
Destructor foo()
Print "dtor"
End Destructor
Dim As foo Ptr bar = New foo
Delete bar
Declare Constructor()
Declare Destructor()
x As Integer
y As Integer
End Type
Constructor foo()
Print "ctor"
End Constructor
Destructor foo()
Print "dtor"
End Destructor
Dim As foo Ptr bar = New foo
Delete bar
You will see that the constructor and destructor for the object are called.
The second type of new/delete is for creating arrays, this time the number of elements is placed after the dataype in square brackets '[]'. When using the array version, you must also use 'delete[]' instead of 'delete', so that FreeBASIC knows you are deleting an array, here is a simple example using the Integer type:
Dim As Integer Ptr foo = New Integer[20]
foo[1] = 1
Print foo[1]
Delete[] foo
foo[1] = 1
Print foo[1]
Delete[] foo
This will create a dynamic array, with 20 Integer elements. It should be noted this is different from Allocate, which takes the number of bytes as its argument; using new, you should specify the number of elements. The array method works just the same for objects:
Type foo
Declare Constructor()
Declare Destructor()
x As Integer
y As Integer
End Type
Constructor foo()
Print "ctor"
End Constructor
Destructor foo()
Print "dtor"
End Destructor
Dim As foo Ptr bar = New foo[3]
Delete[] bar
Declare Constructor()
Declare Destructor()
x As Integer
y As Integer
End Type
Constructor foo()
Print "ctor"
End Constructor
Destructor foo()
Print "dtor"
End Destructor
Dim As foo Ptr bar = New foo[3]
Delete[] bar
When you run this code, you will see that three constructor/destructor pairs are called, because we created an array of three instances of foo.
You must remember to call Delete, or Delete[] for any memory allocated with New, or you will cause a memory leak, just like the way you must rememeber to call DeAllocate for any memory you allocate with the Allocate function.
Name Mangling
Name mangling, also known as name decoration, is something that happens behind the scenes, at a lower level, and as such is not essential to know about. The reason for name mangling is to resolve problems that are involved with more than one function sharing the same name, which happens when functions are overloaded, or are part of a type. Take for example the overloaded subs shown below:
Sub foo Overload ()
End Sub
Sub foo(ByVal i As Integer)
End Sub
End Sub
Sub foo(ByVal i As Integer)
End Sub
If we didn't have name mangling, then both might be known at a lower level as FOO, which would cause a name clash, so they have to be decorated in order to know which one should be called when they are used. For the first sub, the compiler actually creates a sub called _Z3FOOv, and for the second it creates a sub called _Z3FOOi. The compiler then remembers these, and chooses the appropriate sub to call, depending on how you call it, for example 'foo()' will actually call _Z3FOOv, and 'foo(1)' will actually call _Z3FOOi. We can spot something from this, that the 'v' stands for void (no argument), and 'i' stands for integer. The full details of name mangling are quite complex, and vary between compilers, the Microsoft compilers use a different name mangling scheme to GNU compilers, and other compilers may use different schemes as well. The main thing we need to know, is that FreeBASIC follows the GCC 3.x ABI (Application binary interface), meaning that any overloaded functions, or complex types will only be compatible with other compilers using the same scheme. This is an unfortunate limitation, but it is not really a FreeBASIC problem, it is common of all the compilers that use advanced features, and even if all the compiler authors agreed on a common name mangling scheme, there are still other issues that would cause incompatability.
Implicit this
This again is not necessary to know about mostly, its something that happens behind the scenes at a lower level. When you call a member function of an object, what actually happens is a hidden first parameter is passed, so that the function knows which instance of the object is being refered to. This is also true for the property/constructor/destructor/operator members. If we look at a very simple example:
Type foo
Declare Sub bar(ByVal n As Integer)
x As Integer
End Type
Sub foo.bar(ByVal n As Integer)
x = n
End Sub
Dim baz As foo
baz.bar(5)
Declare Sub bar(ByVal n As Integer)
x As Integer
End Type
Sub foo.bar(ByVal n As Integer)
x = n
End Sub
Dim baz As foo
baz.bar(5)
What actually happens behind the scenes is something essentially equivalent to this:
Type foo
x As Integer
End Type
Sub foo_bar(ByRef _this As foo, ByVal n As Integer)
_this.x = n
End Sub
Dim baz As foo
foo_bar(baz, 5)
x As Integer
End Type
Sub foo_bar(ByRef _this As foo, ByVal n As Integer)
_this.x = n
End Sub
Dim baz As foo
foo_bar(baz, 5)
This method using an explicit 'this' is often used in languages that do not have facilities to make it easier. OOP is really just a set of concepts, that can be mostly coded in almost any language, some things are more difficult to implement, such as constructors, you would have to explicitly call a 'create', or 'init' function. For some things such as private/public distinction, it is even more difficult or impossible because the compiler does not know to enforce them. The reason for adding OOP features to a language is to hide a lot of this, and add syntactic sugar to make it simpler to do, or more transparent in use, such as the way we can use properties as if they were ordinary data members, rather than functions, which is what they really are.
Hints for debugging/profiling
When using GDB or other debuggers, and the gprof profiling tool, the information shown is in the C++ syntax, and all your variable names and other symbols are shown in upper case, here is just a very short overview to help you understand how these are shown:
Here's an example type:
Type bar
Declare Constructor()
Declare Constructor(ByRef obj As bar)
Declare Constructor(ByVal n As Integer)
Declare Destructor()
Declare Operator Cast() As Any Ptr
Declare Operator Let(ByVal n As Integer)
Declare Property foo(ByVal n As Integer)
Declare Property foo() As Integer
member As Any Ptr
End Type
Declare Constructor()
Declare Constructor(ByRef obj As bar)
Declare Constructor(ByVal n As Integer)
Declare Destructor()
Declare Operator Cast() As Any Ptr
Declare Operator Let(ByVal n As Integer)
Declare Property foo(ByVal n As Integer)
Declare Property foo() As Integer
member As Any Ptr
End Type
When using GDB, these will be shown as follows (note in C++ they use :: where we would use . (dot), '::' is known as the scope resolution operator):
BAR::BAR() - The default constructor
BAR::BAR(BAR&) - The copy constructor (& in C++ means a reference, like byref)
BAR::BAR(int) - The constructor taking an integer argument (note there is no special symbol to denote ByVal, as this is the default passing method in C/C++)
BAR::~BAR() - The destructor
BAR::operator void*() - A cast to Any ptr (void is similar to Any, * means pointer)
BAR::operator=(int) - The assignment operator (Let), dentoted by '=', in C/C++ '=' is assignment, '==' is equality testing.
BAR::FOO(int) - Property foo setter, taking an integer argument
BAR::FOO() - Property foo getter
Member sub/functions are shown in the same way as properties, indexed properties are shown the same also, just with the extra argument for the index.
Here is how the FB data types will be shown:
Any ptr - void *
ZString ptr - char *
String - FBSTRING
byte - signed char
ubyte - bool
short - short
ushort - unsigned short
integer - int
uinteger - unsigned int
longint - long long
ulongint - unsigned long long
I hope that helps you get started with understanding how things are displayed in GDB/gprof, a little experimentation will always help.
More reading
http://www.freebasic.net/wiki/wikka.php?wakka=KeyPgOpNew
http://www.freebasic.net/wiki/wikka.php?wakka=KeyPgOpDelete
http://en.wikipedia.org/wiki/Copy_constructor
http://en.wikipedia.org/wiki/Object_copy
http://en.wikipedia.org/wiki/Name_mangling