Get element from multidimensional array with unknown dimensions

268 views Asked by At

If I have an array with n-dimensions, where n is an unknown number until runtime, how do I index into that array?

ReDim indices(1 to n) As Long = array(1,2,3)

data(1,2,3) 'n = 3

data(*indices) 'I want this

(we can work out n using this https://github.com/cristianbuse/VBA-ArrayTools/blob/c23cc6ba550e7ebaed1f26808501ea3afedf1a3b/src/LibArrayTools.bas#L730-L741)

Public Function GetArrayDimsCount(ByRef arr As Variant) As Long
    Const MAX_DIMENSION As Long = 60 'VB limit
    Dim dimension As Long
    Dim tempBound As Long
    '
    On Error GoTo FinalDimension
    For dimension = 1 To MAX_DIMENSION
        tempBound = LBound(arr, dimension)
    Next dimension
FinalDimension:
    GetArrayDimsCount = dimension - 1
End Function

The following does what I want I think but I was wondering is there an obvious way to do this in VBA (*pv void looks like a headache)

HRESULT SafeArrayGetElement(
  [in]  SAFEARRAY *psa,
  [in]  LONG      *rgIndices,
  [out] void      *pv
);
1

There are 1 answers

2
Cristian Buse On BEST ANSWER

With a bit of memory trickery you can look at your multidimensional array as a one dimensional array. You will need LibMemory:

Option Explicit

Public Type FAKE_ARRAY
    sArr As SAFEARRAY_1D
    fakeArrPtr As LongPtr
    values As Variant
End Type

Public Sub ArrayToFakeArray(ByRef arr As Variant, ByRef fakeArray As FAKE_ARRAY)
    Dim aptr As LongPtr: aptr = ArrPtr(arr) 'Will throw if not array
    Dim i As Long
    '
    With fakeArray
        .fakeArrPtr = VarPtr(.sArr)
        MemCopy .fakeArrPtr, aptr, LenB(.sArr)
        With .sArr.rgsabound0
            .cElements = 1
            For i = 1 To fakeArray.sArr.cDims
                .cElements = .cElements * (UBound(arr, i) - LBound(arr, i) + 1)
            Next i
        End With
        .sArr.cDims = 1
        .values = VarPtr(.fakeArrPtr)
        MemInt(VarPtr(.values)) = VarType(arr) Or VT_BYREF
    End With
End Sub

Quick test:

Sub Test()
    Dim arr(2, 3, 2) As Variant
    Dim i As Long, j As Long, k As Long
    Dim m As Long
    Dim v As Variant
    '
    For i = LBound(arr, 1) To UBound(arr, 1)
        For j = LBound(arr, 2) To UBound(arr, 2)
            For k = LBound(arr, 3) To UBound(arr, 3)
                arr(i, j, k) = m
                m = m + 1
            Next k
        Next j
    Next i
    '
    Dim temp As FAKE_ARRAY: ArrayToFakeArray arr, temp
    '
    Dim arr2(1, 1) As Double
    arr2(1, 1) = 17.55
    '
    Dim temp2 As FAKE_ARRAY: ArrayToFakeArray arr2, temp2
    '
    Debug.Print temp.values(0)
    Debug.Print temp.values(4)  '15
    Debug.Print temp.values(35)
    '
    arr(1, 1, 0) = "AAA"
    Debug.Print temp.values(4)  'AAA
    Debug.Print temp2.values(3)
End Sub

Edit #1

This is a response to a series of interesting questions asked by the OP in the comments section. Not only the response is too long but it should definitely be part of the answer.

If I'm understanding correctly, the last line sets the array type as the same as arr but all the elements of the fake one point to the original ByRef?

When copying the SAFEARRAY structure, we also copy the pvData pointer pointing to the actual data. The fake array is pointing to the same data in memory so we're fooling the array handling code to read that data directly (not ByRef). But, we need to set the ByRef flag on the values Variant to avoid releasing the same memory twice which would cause a crash. But nothing so far is ByRef - just 2 array variables pointing to the same data.

Could there be a situation where the original is already ByRef (paramarray of VARIANTARGS?) and this doesn't work?

If the original data is having ByRef members (paramarray of VARIANTARGS) that could only happen if we use something like the CloneParamArray method because otherwise VB doesn't allow to pass the param array around, at least not natively. In this case, accessing the ByRef members via the fake array can only be done correctly via an utility function that can receive such a member ByRef.

Example:

Sub Test()
    Dim t As Long: t = 5
    
    ToParam 1, 2, 3, 4, t
    
    Debug.Print t
End Sub

Public Sub ToParam(ParamArray args() As Variant)
    Dim arr() As Variant
    CloneParamArray args(0), UBound(args) + 1, arr
    
    Dim temp As FAKE_ARRAY: ArrayToFakeArray arr, temp
    
    Debug.Print arr(4)
'    Debug.Print temp.values(4) 'Err 458 - type not supported
    PrintVar temp.values(4)

    args(4) = 7
    
    Debug.Print arr(4)
    PrintVar temp.values(4)
    
    LetByRef(temp.values(4)) = 9
    
    Debug.Print arr(4)
    PrintVar temp.values(4)
End Sub

Private Function PrintVar(ByRef v As Variant)
    Debug.Print v
End Function

Private Property Let LetByRef(ByRef vLeft As Variant, ByVal vRight As Variant)
    vLeft = vRight
End Property

If one uses CloneParamArray on purpose then one should be aware anyway that the ByRef individual Variant members can only be accesed/changed via utility methods like PrintVar and LetByRef or any other method expecting a ByRef Variant as an argument.

Also, why do you take the array byref? Because it's a reference type, byval doesn't make a shallow copy so the only difference is now you can set arr to point to a different array?

Because we don't know what the array type is (e.g. Long() or Variant()) then we obviously must wrap in a Variant when we pass to the ArrayToFakeArray method. Passing the wrapped array ByVal does make a copy and we can see that by runing the below:

Option Explicit

Sub Test()
    Dim arr() As Long
    ReDim arr(0 To 1)
    arr(0) = 12
    arr(1) = 44
    '
    PassArr arr, arr
    Debug.Print
    Debug.Print arr(1) 'Prints 55
End Sub

Private Sub PassArr(ByVal arrByVal As Variant, ByRef arrByRef As Variant)
    #If Win64 Then
        Const dataOffset As Long = 16
    #Else
        Const dataOffset As Long = 12
    #End If
    Dim aPtrByVal As LongPtr: aPtrByVal = ArrPtr(arrByVal)
    Dim aPtrByRef As LongPtr: aPtrByRef = ArrPtr(arrByRef)
    Dim pvDataByVal As LongPtr: pvDataByVal = MemLongPtr(aPtrByVal + dataOffset)
    Dim pvDataByRef As LongPtr: pvDataByRef = MemLongPtr(aPtrByRef + dataOffset)
    '
    Debug.Print "ByVal SAFEARRAY address:", aPtrByVal, "ByVal data address:", pvDataByVal
    Debug.Print "ByRef SAFEARRAY address:", aPtrByRef, "ByRef data address:", pvDataByRef
    '
    Debug.Print MemLong(pvDataByVal + 4) 'Prints 44
    Debug.Print MemLong(pvDataByRef + 4) 'Prints 44
    '
    arrByRef(1) = 55
    '
    Debug.Print MemLong(pvDataByVal + 4) 'Prints 44
    Debug.Print MemLong(pvDataByRef + 4) 'Prints 55
    '
    arrByVal(1) = 77
    '
    Debug.Print MemLong(pvDataByVal + 4) 'Prints 77
    Debug.Print MemLong(pvDataByRef + 4) 'Prints 55
End Sub

So, we need to pass the wrapped array ByRef so that ArrPtr returns the correct address of the SAFEARRAY structure.