6

I don't like uninitialized VBA arrays, since it's necessary to check if array is initialized, each time prior using UBound() or For Each to avoid an exception, and there is no native VBA function to check it. That is why I initialize arrays, at least doing them empty with a = Array(). This eliminates the need for extra check in most of cases, so there are no problems with 1d arrays.

For the same reason I tried to create an empty 2d array. It's not possible simply do ReDim a(0 To -1, 0 To 0), transpose 1d empty array or something similar. The only way I came across by chance, is to use MSForms.ComboBox, assign empty array to .List property and read it back. Here is the example, which works in Excel and Word, you need to insert UserForm to VBA Project, place ComboBox on it, and add the below code:

Private Sub ComboBox1_Change()

    Dim a()
    ComboBox1.List = Array()
    a = ComboBox1.List
    Debug.Print "1st dimension upper bound = " & UBound(a, 1)
    Debug.Print "2nd dimension upper bound = " & UBound(a, 2)

End Sub

After combo change the output is:

1st dimension upper bound = -1
2nd dimension upper bound = 0

Actually it's really the empty 2d array in debug:

locals window

Is there more elegant way to create an empty 2d array, without using ComboBox, or UserForm controls in general?

omegastripes
  • 12,351
  • 4
  • 45
  • 96
  • What's wrong with `Dim arr(1,10) as Variant`? I'm not really understanding the question maybe =) – JvdV Jan 02 '20 at 18:42
  • I need 2d array containing no elements at all. So that e. g. `For Each` loop skips. – omegastripes Jan 02 '20 at 18:45
  • 6
    I don't know the answer, but I did find a nifty way to crash excel. I'm wondering if this affects others. Try merely writing the following line in a subroutine (save your work first): `a=Array();redim a(,0 to 0)` – JNevill Jan 02 '20 at 18:51
  • 1
    @JNevill Instant crashing! – omegastripes Jan 02 '20 at 18:54
  • That's reassuring. Perhaps as a workaround to an empty multidimensional array you instead write a function to test for emptiness that you can then test with an `If` before performing any code that would raise an exception on an empty array. Those are pretty easy to write and don't add much weight to your code. Either way it feels like a hack to get around stupid VBA behavior. – JNevill Jan 02 '20 at 18:57
  • I just want to get rid of uninitialized arrays and consequent extra checks, and create code which would have uniform flow for any 2d array, regardless of whether it is empty or not. – omegastripes Jan 02 '20 at 19:49
  • 2
    You've got me playing with this now – user1274820 Jan 02 '20 at 19:55
  • 2
    I think this is an XY problem. What you should be asking is _how do I easily check if an array is initialised?_ . A good solution is provided on CPearsom.com [here](http://www.cpearson.com/excel/IsArrayAllocated.aspx) – chris neilsen Jan 02 '20 at 21:01
  • 1
    I must admit the question itself might sound quite broad, so it easily may be treated as XY broblem. Note, I already posted the link [how to check if array is initialized](https://stackoverflow.com/q/183353/2165759), IMO `OERN`-like methods must be the last resort, and the only excuse to use them is the significant simplicity and performance against others. And the real point is about to change 2d array handling pattern and get rid of extra check. You know, "If at first the idea is not absurd, then there is no hope for it." – omegastripes Jan 02 '20 at 21:47
  • 1
    I farted around with this yesterday evening and came up empty. I think what has happened here is that you have stumbled into a rare situation where VBA is able to return an array with dimensions `(0 to -1, 0 to 0)` and it can't be repeated through any non-hacky way. My only other thought here was to build a custom library in C# and "reference" that in your code. That isn't a great solution though if you want to distribute this sheet and It's just a guess that you could pull this off in a custom library anyway. – JNevill Jan 03 '20 at 17:45

2 Answers2

5

This is only going to work for Windows (not for Mac):

Option Explicit

#If Mac Then
#Else
    #If VBA7 Then
        Private Declare PtrSafe Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As LongPtr
        Private Declare PtrSafe Function VariantCopy Lib "OleAut32.dll" (pvargDest As Any, pvargSrc As Any) As Long
        Private Declare PtrSafe Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As LongPtr) As Long
    #Else
        Private Declare Function SafeArrayCreate Lib "OleAut32.dll" (ByVal vt As Integer, ByVal cDims As Long, ByRef rgsabound As SAFEARRAYBOUND) As Long
        Private Declare Function VariantCopy Lib "OleAut32.dll" (pvargDest As Variant, pvargSrc As Any) As Long
        Private Declare Function SafeArrayDestroy Lib "OleAut32.dll" (ByVal psa As Long) As Long
    #End If
#End If

Private Type SAFEARRAYBOUND
    cElements As Long
    lLbound As Long
End Type
Private Type tagVariant
    vt As Integer
    wReserved1 As Integer
    wReserved2 As Integer
    wReserved3 As Integer
    #If VBA7 Then
        ptr As LongPtr
    #Else
        ptr As Long
    #End If
End Type

Public Function EmptyArray(ByVal numberOfDimensions As Long, ByVal vType As VbVarType) As Variant
    'In Visual Basic, you can declare arrays with up to 60 dimensions
    Const MAX_DIMENSION As Long = 60
    
    If numberOfDimensions < 1 Or numberOfDimensions > MAX_DIMENSION Then
        Err.Raise 5, "EmptyArray", "Invalid number of dimensions"
    End If

    #If Mac Then
        Err.Raise 298, "EmptyArray", "OleAut32.dll required"
    #Else
        Dim bounds() As SAFEARRAYBOUND
        #If VBA7 Then
            Dim ptrArray As LongPtr
        #Else
            Dim ptrArray As Long
        #End If
        Dim tVariant As tagVariant
        Dim i As Long
        '
        ReDim bounds(0 To numberOfDimensions - 1)
        '
        'Make lower dimensions [0 to 0] instead of [0 to -1]
        For i = 1 To numberOfDimensions - 1
            bounds(i).cElements = 1
        Next i
        '
        'Create empty array and store pointer
        ptrArray = SafeArrayCreate(vType, numberOfDimensions, bounds(0))
        '
        'Create a Variant pointing to the array
        tVariant.vt = vbArray + vType
        tVariant.ptr = ptrArray
        '
        'Copy result
        VariantCopy EmptyArray, tVariant
        '
        'Clean-up
        SafeArrayDestroy ptrArray
    #End If
End Function

You can now create empty arrays with different number of dimensions and data types:

Sub Test()
    Dim arr2D() As Variant
    Dim arr4D() As Double
    '
    arr2D = EmptyArray(2, vbVariant)
    arr4D = EmptyArray(4, vbDouble)
    Stop
End Sub

Update 30/09/2022

I've created an EmptyArray method (same signature) in my MemoryTools library on GitHub. That version will work on both Windows and Mac.

Cristian Buse
  • 4,020
  • 1
  • 13
  • 34
3

Idk man - I think you stumbling onto this property was pretty wild.

I'd probably stop here and just do:

Function Empty2DArray() As Variant
With CreateObject("Forms.ComboBox.1")
    .List = Array()
    Empty2DArray = .List
End With
End Function

And use it like: a = Empty2DArray

You don't need to create the userform or combobox - you can just use CreateObject.

But as others have said, it probably makes more sense to do error handling when checking whether or not your arrays are initialized.

user1274820
  • 7,786
  • 3
  • 37
  • 74