3
\$\begingroup\$

Context

You can probably skim most of this, it's unlikely to be useful for a review and is just for background info. Also download the files


I've tried to create a Boolean object - some truthy reference type which can be used in place of VBA's builtin Boolean. I have 2 motivations for this:

  1. I need an object which encapsulates a Boolean value, so that it can be passed around by reference, even to procedures expecting a Boolean ByVal.

    In one project, I'm trying to have child subs signal something to a parent caller. The caller passes a flag to the child, and the child can choose to modify this flag's value. I won't justify here why the flag is used rather than say, a function return, or a global variable or the child calling a method of the parent; that will come when I post the rest of the project. For now my challenge (and what I want reviewed) is to determine what sort of data structure/ approach should be used for that flag.

    Ordinarily a perfect candidate for such a flag would be a ByRef Boolean. A caller creates a variable with a value at some address. It passes the address to the child, the child can choose to modify the value at the address, and the parent's copy of the variable receives those modifications. The parent could keep track of all the flags in a Boolean array.

    However this approach is messy when the number of children is dynamic; in order for the parent to keep track of all the flags, it needs some dynamic array - a Collection perhaps? That won't work, because when a primitive value type like a Boolean flag is added to a Collection, the Collection keeps the value of, not the reference to (address in memory of) the flag. So the child can't use the address it receives to modify the value of the flag in the collection, since the copy of the flag in the collection lives at a different address entirely.

    Now whilst you could do some clever stuff (like keeping a collection of VarPtrs and dereferencing them manually, or some clever redim preserve stuff with arrays), I think the easiest workaround is to ditch Booleans in favour of Bool objects.

    Objects are reference types meaning they can be added to Collections and the child process can still modify them. (This is because the value of an object variable is that object's address, so when Collections discard the variable address and store the contents of a variable, they are still storing the address of an object. Therefore a Collection can be used to keep track of references to objects, instead of values of variables).

    Summary: Objects are always passed to procedures and saved as references. Booleans/ primitives are only sometimes stored as references, sometimes their raw values are saved. That's a problem because it means you can't use Collections/Dictionaries to store the boolean flags which is a real headache, but they can be used to store object references.

Sub test()
    Dim a As Object, b As Boolean
    Set a = CreateObject("Scripting.Dictionary")
    a.Add Key:="flag1", Item:=b
    Debug.Assert b = a.Item("flag1") 'fine
    childRoutine b 'editing b should also edit the value in the dictionary if passed by reference
    Debug.Assert b = a.Item("flag1") 'fails
End Sub

Sub test2()
    Dim a As Object, b As New Bool
    Set a = CreateObject("Scripting.Dictionary")
    a.Add Key:="flag1", Item:=b
    Debug.Assert b = a.Item("flag1") 'fine
    childRoutine b 'editing b also edits the value in the dictionary since objects are always passed by reference
    Debug.Assert b = a.Item("flag1") 'fine again
End Sub

Sub childRoutine(ByRef flag As Variant)
    flag = Not flag 'make some change
End Sub
  1. I want object oriented data types a bit like python has, but more VBA idiomatic.

    I've been planning to replace many of the mundane data types in VBA with some more jazzy object-y ones which will eventually allow me to create classes which determine their own responses to operations. This Bool class is a sort of template for such object oriented data types, and explores the data storage aspects; how my class can be used in place of the datatype it encapsulates. Later I'll look at stuff like operator overloading and inheritance and other object stuff.


Since this class is going to be used in both projects, it doesn't make sense to post it exclusively as part of either of them. Especially for the latter I want this to act as a strong foundation, so any criticism or feedback, however minor is useful.

Here's the main class (not much code for all that chatting!)

Bool.cls

'@Folder("API.Utils")
'@ModuleDescription("Boolean object that can be passed byRef")
'@PredeclaredID
Option Explicit

'NOTE RtlCopyMemory would be faster (as source and dest won't overlap) but is not exposed to VBA
''@Description("API: Destination and Source can be byVal pointers or byRef variables, length is LenB(dataType)")
#If Win64 Then
    Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef destination As Any, ByRef source As Any, ByVal length As Long)
    Private Declare PtrSafe Sub ZeroMemory Lib "kernel32.dll" Alias "RtlZeroMemory" (ByRef destination As Any, ByVal length As Long)
#Else
    Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef destination As Any, ByRef source As Any, ByVal length As Long)
    Private Declare Sub ZeroMemory Lib "kernel32.dll" Alias "RtlZeroMemory" (ByRef destination As Any, ByVal length As Long)
#End If

Private Type tBool
    Value As Boolean
End Type

Private this As tBool

'@Description("Truthy encapsulated value")
'@DefaultMember
Public Property Get Value() As Boolean
    Value = this.Value
End Property

Public Property Let Value(ByVal newVal As Boolean)
    this.Value = newVal
End Property

'@Description("Create instance of Bool class from objPtr")
Public Function FromPtr(ByVal pData As LongPtr) As Bool 'TODO fails in VB6 as no LongPtr
    Dim result As New Bool
    CopyMemory result, pData, LenB(pData)
    Set FromPtr = result
    ZeroMemory result, LenB(pData)               ' free up memory, equiv: CopyMemory result, 0&, LenB(pData)
End Function

'@Description("Class Constructor takes Boolean or Boolean-like values")
Public Function Create(ByVal initialValue As Variant) As Bool
    Dim result As New Bool
    result.Value = CBool(initialValue)
    Set Create = result
End Function

Worth noting;

  • '@PredeclaredId; I'm using this class as a factory for instances of itself - it'll probably go into an addin as PublicNotCreatable so requires a factory of some sort. It has 2 constructor methods, Create and FromPtr
  • No .Self; I like using With New Blah statements in the constructors like in this answer, but I thought the additional .Self method or equivalent would clutter the interface and lead to additional confusion given the next point
  • '@DefaultMember; going against all of Rubberduck's advice, I've used a default member which is not a .Item method. But here I think it's justified, as the class is meant only to be a wrapper for actual data; the class must have an intrinsic encapsulated value in order to operate correctly. And that value is all that is required to Create an indistinguishable instance of the class. I feel a default member should represent what a class is - a Bool is the value of the encapsulated variable, the point in a Collection specified by a key/ index is the item located there. A Range is not simply the value in the cell, it is also the address and the formatting and ... that's why Range's default member is so confusing.

As well as feedback on the approach and functionality of the class, I'd really appreciate some insight on best practices regarding layout, documentation and structure of the project, as such I've included all the comments/attributes and here are some unit tests:

UnitTests.bas

Option Explicit
Option Private Module

'@TestModule
'@Folder("API.Utils.Tests")

Private Assert As Rubberduck.PermissiveAssertClass
Private Fakes As Rubberduck.FakesProvider

'@ModuleInitialize
Private Sub ModuleInitialize()
    'this method runs once per module.
    Set Assert = New Rubberduck.PermissiveAssertClass
    Set Fakes = New Rubberduck.FakesProvider
End Sub

'@ModuleCleanup
Private Sub ModuleCleanup()
    'this method runs once per module.
    Set Assert = Nothing
    Set Fakes = Nothing
End Sub

'@TestInitialize
Private Sub TestInitialize()
    'this method runs before every test in the module.
End Sub

'@TestCleanup
Private Sub TestCleanup()
    'this method runs after every test in the module.
End Sub

'@TestMethod("Uncategorized")
Private Sub DefaultPropertyLetGet()
    On Error GoTo TestFail

    'Arrange:
    Dim b As New Bool
    b.Value = False

    'Act:
    b = True

    'Assert:
    Assert.AreEqual True, b.Value
    Assert.AreEqual True, (b) 'should just be b - this is an issue with the assert class

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

'@TestMethod("Uncategorized")
Private Sub ClassConstructor()
    On Error GoTo TestFail

    'Arrange:
    Dim a As Bool, b As Bool, c As Bool

    'Act:
    Set b = Bool.Create(True)
    Set a = Bool.Create(False)
    Set c = Bool.Create(a)                       'implicit conversion with CBool

    'Assert:
    Assert.AreEqual True, b.Value
    Assert.AreEqual False, a.Value
    Assert.AreEqual a.Value, c.Value
    Assert.AreNotSame a, c                       'c only has the same value as a

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

'@TestMethod("Uncategorized")
Private Sub AssigningByReferenceCanOverwrite()
    On Error GoTo TestFail

    'Arrange:
    Dim base As Bool, copy As Bool

    'Act:
    Set base = Bool.Create(True)
    Set copy = Bool.FromPtr(ObjPtr(base))
    copy = False

    'Assert:
    Assert.AreEqual False, base.Value
    Assert.AreSame base, copy

TestExit:
    Exit Sub
TestFail:
    Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub

'@TestMethod("Uncategorized")
Private Sub InvalidConstructionRaisesTypeMismatchError()
    Const ExpectedError As Long = 13             'type mismatch
    On Error GoTo TestFail

    'Arrange:
    Dim b As Bool

    'Act:
    Set b = Bool.Create("Not a boolean!")

Assert:
    Assert.Fail "Expected error was not raised"

TestExit:
    Exit Sub
TestFail:
    If Err.Number = ExpectedError Then
        Resume TestExit
    Else
        Resume Assert
    End If
End Sub

I'm not sure if I'm using these quite right; I wrote them all after I'd written the class itself. Additionally, I've had to qualify the default member explicitly with b.Value in the tests; I believe this is a bug/ incorrect behaviour in the Rubberduck.PermissiveAssertClass (I made a comment to that effect in the repository)

\$\endgroup\$
8
  • 1
    \$\begingroup\$ My Understanding is that the immutability of collection items is part of the design of the collection object. You can show this to be true by trying to assign to a collection item. You can an object required error. I think where you are getting confused is that when a collection stores an object it stores the reference. You can't change the reference but you can change the content of the object the reference points to. If you want to change the values held by a collection, whether it be a simple value or an object reference, then you need to use a scripting.dictionary. \$\endgroup\$
    – Freeflow
    Commented Jun 29, 2019 at 13:25
  • 1
    \$\begingroup\$ Is there a thing called 'boxing' in VBA? \$\endgroup\$
    – dfhwze
    Commented Jun 29, 2019 at 14:35
  • \$\begingroup\$ @Freeflow actually I'm not worried about whether the collection is immutable or not. The distinction I'm trying to make is between a Collection holding references (addresses) pointing to values in memory, vs actually holding the values (ok, holding its own pointers to its own copies of the values, same difference). The former is required if you want something to be able to edit the values pointed to by the Collection, since with the latter nobody other than the collection knows where its values are saved so they are untouchable. Anyway, I've made a big edit to the question to explain all that \$\endgroup\$
    – Greedo
    Commented Jun 29, 2019 at 15:11
  • \$\begingroup\$ To do what you want with a collection you read the item into a local variable, change the local variable, remove the old item then add a new item using the local variable. You can insert the new item using the insertafter or insertbefore options. Otherwise, use a scripting.dictionary. \$\endgroup\$
    – Freeflow
    Commented Jun 29, 2019 at 15:23
  • 2
    \$\begingroup\$ Why not use MSForms.ReturnBoolean? \$\endgroup\$ Commented Jun 29, 2019 at 20:04

0