CVector Implementation
The CVector class is essentially a wrapper around an array. Here’s the internal data that makes it work:
Private av() As Variant
Private iLast As Long
Private cChunk As Long
Private Sub Class_Initialize()
cChunk = 10 ‘ Default size can be overridden
ReDim Preserve av(1 To cChunk) As Variant
iLast = 1
End Sub
Most of the implementation is in the Item property. Because CVector is a variant container class and because you don’t know whether the variants in the internal array will be objects or variables, you must implement all three property procedures—Get, Let, and Set. Here’s the code:
‘ Item is the default property
Property Get Item(ByVal i As Long) As Variant
BugAssert i > 0
‘ If index is out-of-range, return default (Empty)
On Error Resume Next
If IsObject(av(i)) Then
Set Item = av(i)
Else
Item = av(i)
End If
End Property
Property Let Item(ByVal i As Long, ByVal vItemA As Variant)
BugAssert i > 0
On Error GoTo FailLetItem
av(i) = vItemA
If i > iLast Then iLast = i
Exit Property
FailLetItem:
If i > UBound(av) Then
ReDim Preserve av(1 To i + cChunk) As Variant
Resume ‘ Try again
End If
ErrRaise Err.Number ‘ Other VB error for client
End Property
Property Set Item(ByVal i As Long, ByVal vItemA As Variant)
BugAssert i > 0
On Error GoTo FailSetItem
Set av(i) = vItemA
If i > iLast Then iLast = i
Exit Property
FailSetItem:
If i > UBound(av) Then
ReDim Preserve av(1 To i + cChunk) As Variant
Resume ‘ Try again
End If
ErrRaise Err.Number ‘ Other VB error for client
As I stated earlier, CVector is the av Variant array in a thin class wrapper. The Item property accesses elements of the array, but because it is marked as the default property, clients don’t have to give the Item method name. Access to the indexed Item property looks like access to an array. The sidebar on page 184 gives more details on how property procedures can be used to simulate array indexes.
The main complication in the Property Get is that you have to check to see whether the variant being accessed is a variable or an object so that you can use Set to assign objects. My implementation turns off error checking and lets invalid data requests return Empty. I could have chosen to raise an out-of-range error, and that might be more appropriate in vectors of integers or other types that don’t have an equivalent of Empty.
The Let and Set property procedures use error trapping to identify when you try to insert beyond the end of the array. The array is then resized. The size of the cChunk variable determines the efficiency of the vector. A larger chunk size means faster element writes because the expensive redimensioning is less fre-quent, but it also means more wasted data space.
The CVector class has a couple of other properties. The Chunk property just gets or (more likely) sets the size of the data chunks. The default size (10) is a good compromise for most vectors, but you can fine-tune it if you like. The Last property returns or sets the last element in the array:
Property Get Last() As Long
Last = iLast
End Property
Property Let Last(iLastA As Long)
BugAssert iLastA > 0
ReDim Preserve av(1 To iLastA) As Variant
iLast = iLastA
End Property
Notice that setting Last specifically resizes the array. Usually it is used to shrink a vector when you know you’ll no longer need data beyond a certain point. You can also use it to expand an array specifically rather than automatically.
What’s the cost? Well, in the Item Property Get, you pay for accessing the array through a procedure call rather than inline code. You also pay for checking to see if the entry is an object, but that’s not really the vector’s fault; you’d have to do the same thing with an array of variants. In the Item Property Let and Property Set, you also pay for the procedure call. In addition, there’s an extra expense for an error trap, even when you don’t hit it. And when you do hit the error trap, you pay an even bigger penalty for resizing the array.
What does this add up to? It depends on what you’re comparing it to. Check the Performance sidebar on page 191. You could get the same benefits at less cost by hard-coding similar error traps into each program that needs an expandable array. But since CVector exists as compiled native code in the VBCore DLL, the cost usually isn’t worth worrying about.
If you want even greater efficiency, use the typed versions in VBCore. I started with the general CVector class for variants, and then I did a Save As to create the CVectorInt class. Next I did a global search and replace to change Variant to Integer, and then I cut out the variant-specific features. From there it was easy to create CVectorInt, CVectorLng, CVectorBool, CVectorStr, CVectorSng, and CVectorDbl.
Of these, the most interesting is CVectorBool. A Visual Basic Boolean is a 16-bit value (the same as an Integer). This is kind of a strange choice, but don’t blame Visual Basic; COM Automation defines the size. Many languages make Boolean types 8-bit to save data space in arrays. Windows makes its BOOL type the system integer size (Long in 32-bit) for greater efficiency. But all it really takes to store a Boolean is one bit. That’s how many bits CVectorBool uses for each element. It actually stores its elements as Longs and crams 32 Boolean values into each of them, using logical operators to insert and extract the bits. All this happens behind the scenes. The Item property returns the bits as Boolean, just as you would expect. The CVectorBool class is 16 times more data efficient than an array of Booleans, and the performance cost for this savings is small. Check it out in VECTORBOOL.CLS.
Property Arrays
Visual Basic is the only language I know that uses parentheses for array subscripts. (Most languages use square brackets.) This practice leads to ambiguity in the language. Does
i = Zap(4)
assign element 4 of the Zap array to i, or does it call the Zap function with argument 4 and assign the result to i? I used to think this was a flaw in the language, but it’s handy when you create properties that look and act like arrays.
For example, assume that you are creating a CParanoia class, whose objects have an EnemyList property. You want to use it as follows:
scared.EnemyList(i) = “Jones”
Calumniate scared.EnemyList(i)
EnemyList looks like an array and acts like an array, but you must implement
it as a property, and properties can’t be arrays. You might expect that if you
declare a public array in a form or a class, it would become a property array:
Public EnemyList(1 To 10) As String
Visual Basic throws this statement out at compile time with a message telling you that arrays can’t be public. You can usually create more reliable properties with Property Get and Property Let anyway, but it’s not immediately clear how to do this. The answer is that you fake subscripts with property parameters.
Behind your properties, you must have a private array or a collection. Let’s use an array here:
You can have lots of fun enhancing my CVector class. Start with multiple dimensions. How about a CMatrix class? Maybe you could write some functions that do math on vectors or matrices. FORTRAN can do addition, subtraction, multiplication, division, and even exponentiation on arrays. Why can’t Visual Basic? You can also do comparisons and logical operations. Without operator overloading, Visual Basic might not be able to make array expressions as natural as they are in some languages, but with care, you can probably come up with a fairly clean syntax. With native code, the operations themselves can be as fast in Visual Basic as they are in other languages.
Private asEnemies(1 To 10) As String
Property Get procedures normally don’t have parameters, but when faking subscripts, you need one argument for every array dimension. Since you’re faking a one-dimensional array, you need only one argument:
Property Get EnemyList(i As Integer) As String
If i >= 1 And i <= 10 Then EnemyList = asEnemies(i)
End Property
Normally, Property Let procedures have one argument, but you’ll need an extra argument for each array dimension. The assignment parameter must be the last parameter:
Property Let EnemyList(i As Integer, sEnemyA As String)
If i >= 1 And i <= 10 Then asEnemies(i) = sEnemyA
End Property
Expanding on this technique, you can easily design properties that represent multidimensional arrays:
iHiCard = deck.Cards(iClubs, iJack)
You can even have array properties with string or real number indexes:
iHiCard = deck.Cards(“Clubs”, “Jack”)
Print patient.Temperature(98.6)
Obviously, this syntax would be impossible with real arrays. You decide how your parameters relate to the data hidden in your class.