• Welcome to Jose's Read Only Forum 2023.
 

ProgEx 45: C++ Verses PowerBASIC Dynamic Multi-Dimensional Arrays

Started by Frederick J. Harris, December 09, 2016, 09:53:29 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

Comparison Between C/C++ And PowerBASIC Arrays
And
A C++ Template Based Array Class

     Let's begin by attempting to create an integral four dimensional dynamic array in C++ like so...


// MultiDim01.cpp
#include <cstdio>

int main()
{
int iD1,iD2,iD3,iD4;

iD1=3, iD2=4, iD3=5, iD4=6;        // 3 Rows, 4 Cols, 5 Deep, 6 ??? (don't know what to call 4th Dim!)
int iArray[iD1][iD2][iD3][iD4];    // Create Array
iArray[2][3][4][5]=123456789;      // Set some random element
printf("iArray[2][3][4][5] = %d\n",iArray[2][3][4][5]);
getchar();

return 0;   
}


In the code above, four 32 bit int variables are declared to contain the four dimensions of our array named iD1, iD2, iD3, and iD4.  They are set equal to 3, 4, 5, and 6 respectively.  This line allocates the array on the stack...


int iArray[iD1][iD2][iD3][iD4];


There will be 3 X 4 X 5 X 6 = 360 elements in that array, and since ints are 4 bytes each the array will be stored linearly in an address space comprising 1440 bytes.

If the above program is named MultiDim01.cpp the following command line will build it with GCC...


g++ MultiDim01.cpp -oMultiDim01.exe -mconsole -m64 -Os -s


If one attempts to build the above program with any Microsoft compiler these are the errors generated...

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>cl MultiDim01.cpp /O1 /Os
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.


MultiDim01.cpp
MultiDim01.cpp(9) : error C2057: expected constant expression
MultiDim01.cpp(9) : error C2466: cannot allocate an array of constant size 0
MultiDim01.cpp(9) : error C2057: expected constant expression
MultiDim01.cpp(9) : error C2466: cannot allocate an array of constant size 0
MultiDim01.cpp(9) : error C2087: 'iArray' : missing subscript
MultiDim01.cpp(9) : error C2057: expected constant expression
MultiDim01.cpp(9) : error C2466: cannot allocate an array of constant size 0
MultiDim01.cpp(9) : error C2087: 'iArray' : missing subscript
MultiDim01.cpp(9) : error C2057: expected constant expression
MultiDim01.cpp(9) : error C2466: cannot allocate an array of constant size 0
MultiDim01.cpp(9) : error C2087: 'iArray' : missing subscript
MultiDim01.cpp(9) : error C2133: 'iArray' : unknown size


The nature of the problem is that the C++ Standard does not provide for dynamic array allocation.  Even attempting to allocate a relatively simple one dimensional array of 5 ints will fail with Microsoft compilers...


// MultiDim02.cpp
#include <cstdio>

int main()
{
int iD1=5;

int iArray[iD1];            // Create Array
getchar();

return 0;   
}


Build Chain Output...

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>cl MultiDim02.cpp /O1 /Os
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

MultiDim02.cpp
MultiDim02.cpp(8) : error C2057: expected constant expression
MultiDim02.cpp(8) : error C2466: cannot allocate an array of constant size 0
MultiDim02.cpp(8) : error C2133: 'iArray' : unknown size


To get our MultiDim01 or MultiDim02 programs to compile with Microsoft compilers one would need to specify static rather than dynamic arrays....


// g++ MultiDim03.cpp -oMultiDim03.exe -mconsole -m64 -Os -s
// cl  MultiDim03.cpp /O1 /Os
#include <cstdio>

int main()
{
int iArray[3][4][5][6];            // Create Array
iArray[2][3][4][5]=123456789;      // Set some random element
printf("iArray[2][3][4][5] = %d\n",iArray[2][3][4][5]);
getchar();

return 0;   
}


In the above code the iD1, iD2, iD3, and iD4 variables have been replaced with numeric literals, and the change seems to placate the compiler enough to allow the program to build and run satisfactorily.

All of the above might lead one to suspect that GCC is somehow better in that it seems to allow for dynamic array allocation.  Well, we haven't quite seen the full story.

In many applications using dynamic arrays its likely that the arrays would need to be passed as function parameters.  To put it most simply, neither C nor C++ contain any syntax which specifies that a function parameter is an array of any rank (number of dimensions) whatsoever.  Let's think about that, fiddle with some code, and try to work out what the difficulties are. 

Back in C days, if one wanted to pass a single dimension array to a function, one would pass the base address of where the memory allocation for the array started, and in another or second parameter one would pass the number of elements or the size of the array.  Here is MultiDim04.cpp showing that design pattern...


// g++ MultiDim04.cpp -oMultiDim04.exe -mconsole -m64 -Os -s
#include <cstdio>

void foo(int* pInts, int iNumElements)  // function receives base address of int array and number of elements from main()
{
for(size_t i=0; i<iNumElements; i++)
     printf("%d\t%d\n",i,pInts[i]);
}

int main()
{
int iAr[]={0,1,2,3,4,5,6,7,8,9};       // allocate 10 element int array on stack
foo(iAr, sizeof(iAr)/sizeof(iAr[0]));  // pass array base address and number of elements to function foo()
getchar();

return 0;
}

#if 0

0       0
1       1
2       2
3       3
4       4
5       5
6       6
7       7
8       8
9       9

#endif


As you can see, that works, and we get the expected output.  But what have we really done?  If you think about it, what we've done is 'added' something to the array to make passing it as a parameter work.  In other words, the data comprising the array itself contains nothing but data.  It contains no 'intelligence' whatsoever as to the limits of the data or anything else about it.  The 'intelligence' I'm alluding to that makes it possible to pass the array to function foo() is contained within the second parameter of the function, and that would be the information as to the number of elements contained within the array.  Let's attack this idea from another angle, and look at how another and completely different programming language attacks this problem.  Let's go back to our original four dimensional array from MultiDim01.cpp and look at that from the perspective of the PowerBASIC programming language.  Here would be that plus some extra output statements to show what's going on...   


#Compile Exe   ' Compile code into an executable
#Dim All       ' Specify through compiler directive that all variables must be declared
%RETRIEVE_NUMBER_DIMENSIONS = 3   ' Equate/#define used in built in function ArrayAttr()

Function Foo(iArray() As Long) As Long
  Register i As Long

  Con.Print "Entering Foo()"
  Con.Print "  iArray(2,3,4,5) = " iArray(2,3,4,5)
  Con.Print
  Print "  LBound(iArray(), i)        UBound(iArray(), i)"
  Print "  ============================================="
  For i=1 To ArrayAttr(iArray(), %RETRIEVE_NUMBER_DIMENSIONS)
    Con.Print "  " LBound(iArray(),i),, UBound(iArray(), i)
  Next i
  Print "Leaving Foo()"

  Function=0
End Function

Function PBMain() As Long
  Local iD1,iD2,iD3,iD4 As Long        ' Declare four 32 bit Longs to contain size of each dimension

  iD1=3 : iD2=4 : iD3=5 : iD4=6        ' Set D1 to 3, D2 to 4, D3 to 5, and D4 to 6
  Dim iAr(iD1, iD2, iD3, iD4) As Long  ' Allocate array
  iAr(2, 3, 4, 5) = 123456789          ' Set a random element
  Console.Print "iAr(2,3,4,5)      = " iAr(2,3,4,5)
  Print
  Foo(iAr())
  Erase iAr()
  Waitkey$

  PBMain=0
End Function
                   
Output:

iAr(2,3,4,5)      =  123456789

Entering Foo()
  iArray(2,3,4,5) =  123456789

  LBound(iArray(), i)        UBound(iArray(), i)
  =============================================
   0                         3
   0                         4
   0                         5
   0                         6
Leaving Foo()



Frederick J. Harris

Above you'll find the code plus the output.  Let me start at the beginning explaining it.  These two lines contain compiler directives plus PowerBASIC comments...


#Compile Exe   ' Compile code into an executable
#Dim All       ' Specify through compiler directive that all variables must be declared


The first, i.e., #Compile Exe, tells the compiler to generate a 32 bit executable file as opposed to a dll or lib file.  The " ' " symbol can be used to specify a comment, or the more verbose REM statement.  The second directive tells the compiler's parser to flag any encountered undeclared variable as an error.  Basic family languages had a sordid past where undeclared variables were allowed.  In such cases the language would make a 'best guess' as to the variable's type, and this is a poor way to go about the programming endeavor.  You won't find any undeclared variables in any basic code I write.  OK, we have a program quite similar to MultiDim01.cpp, so let's look within PBMain().  That would be analogous to main() in C or C++.  Find it above.  Here are the lines which allocate our four dimensional array, set a random element, and output it to the console...


Dim iAr(iD1, iD2, iD3, iD4) As Long  ' Allocate array
iAr(2, 3, 4, 5) = 123456789          ' Set a random element
Console.Print "iAr(2,3,4,5)      = " iAr(2,3,4,5)


In the program's output as seen above right after the program those lines generated this line of output...


iAr(2,3,4,5)      =  123456789


Nothing too spectacular or noteworthy there, as the C or C++ code could do that - if it compiles that is.  Our Microsoft compiler couldn't even quite manage that, although the GCC compiler could.  But next in PBMain() comes this line....


Foo(iAr())


...which is a function call with a single argument, that is, our iAr() four dimensional array.  Several things to note, first the syntax, that is...


iAr()


Basic family languages use rounded parentheses to contain array subscripts and also to contain function parameters.  The usage or meaning is inferred by the language parser by the context or usage.  So when the Basic Dim statement (which is an executable statement which allocates memory) was used to declare and allocate iAr() like so...


Dim iAr(iD1, iD2, iD3, iD4) As Long  ' Allocate array
   

...we didn't use four sets of square brackets like in C, i.e., not this...


int iArray[iD1][iD2][iD3][iD4];    // Create Array


Next, the symbols, iAr() containing the empty parentheses are the language construct or syntax used in PowerBASIC to specify that an array is being passed as a function parameter - not a scalar variable.

Lastly, and related to my point above, there is only one parameter to the function foo().  Recall above in MultiDim04.cpp our foo () function required a pointer to the base address of the array as the first parameter and the number of elements contained within the array as the second parameter...


void foo(int* pInts, int iNumElements)  // function receives base address of int array and number of elements from main()
{
for(size_t i=0; i<iNumElements; i++)
     printf("%d\t%d\n",i,pInts[i]);
}
   

In contrast, our PowerBASIC foo function stripped of all the excess baggage to show what's going on, and reduced to its bare essentials which simply outputs the random value set in PBMain(), would be simply this...


Function Foo(iArray() As Long) As Long
  Con.Print "  iArray(2,3,4,5) = " iArray(2,3,4,5)
  Function=0
End Function


And that would output the correct value set in PBMain().  So what's going on here?  How is this possible? 

In attempting to come up with an answer think back to MultiDim04.cpp where I stated that our C function foo needed extra intelligence to be able to deal with the pointer to the array which was passed to it.  That extra intelligence was the second parameter iNumElements which we provided the function.  Clearly, somehow or other, PowerBASIC must be acquiring this extra 'intelligence' to be able to correctly deal with the much more complicated situation of resolving the four dimensional array being passed to its foo function.  And that's exactly what its doing.  The language is internally generating 'metadata' when an array is declared, and when an array is passed through a function parameter, that metadata is 'riding along' with the parameter itself.  Here is our full PowerBASIC foo() function again...


Function Foo(iArray() As Long) As Long
  Register i As Long

  Con.Print "Entering Foo()"
  Con.Print "  iArray(2,3,4,5) = " iArray(2,3,4,5)
  Con.Print
  Print "  LBound(iArray(), i)        UBound(iArray(), i)"
  Print "  ============================================="
  For i=1 To ArrayAttr(iArray(), %RETRIEVE_NUMBER_DIMENSIONS)
    Con.Print "  " LBound(iArray(),i),, UBound(iArray(), i)
  Next i
  Print "Leaving Foo()"

  Function=0
End Function


Let's dissect that in detail.  To begin with, the function returns a Long which is the same as a C or C++ int.  Next, the syntax...


iArray() As Long


...contained within the parameter list specifies the parameter as being a Long parameter, but rather than being a single Long as with the Long return value, it is an array of Longs, i.e., an array of C/C++ ints.  Also, the way this array of Longs is passed to foo is By Reference.  That is the standard way PowerBASIC passes parameters.  The function could have been specified this more verbose way...


Function Foo StdCall Alias "Foo" (Byref iArray() As Long) As Long


The StdCall symbol specifies the __stdcall calling and parameter passing convention, but can be omitted because it is the default with PowerBASIC.  If one wanted to use the C calling convention one would specify CDecl, which is equivalent to __cdecl in C/C++.  The optional Alias clause can also be used when naming conflicts exist when importing functions from Dlls.  The Byref symbol can be omitted because, as I've said, that is the default parameter passing mechanism in PowerBASIC.  If one wanted to use the C passing convention one would specify the 'Byval' override. 

Next, we have this...


Register i As Long


That's a variable declaration for an iterator variable i to be used in a For Loop to iterate through some of the 'metadata' passed to foo through the array parameter.  That declaration tells the compiler to use a processor register for the variable i, rather than memory.  This is extremely fast.  The PowerBASIC language is a very high performance language just like C and C++, in that all statements are compiled to optimized machine code. 

Where we use Register variable i is in the next several lines of code...


Print "  LBound(iArray(), i)        UBound(iArray(), i)"
  Print "  ============================================="
  For i=1 To ArrayAttr(iArray(), %RETRIEVE_NUMBER_DIMENSIONS)
    Con.Print "  " LBound(iArray(),i),, UBound(iArray(), i)
  Next i


...which produces this output...


LBound(iArray(), i)        UBound(iArray(), i)
=============================================
0                         3
0                         4
0                         5
0                         6


There are actually three built in PowerBASIC functions involved in that.  By 'built in' I mean something like C or C++'s sizeof() function or operator.  Its a built in part of the language, and one doesn't need to include any include files to access the functionality.  Its functionality will always be available to you.  Those built in PowerBASIC functions are...


ArrayAttr(...)   ' Array Attribute
LBound(...)      ' Lower Bound of Array
UBound(...)      ' Upper Bound of Array


The '...' between the parentheses is my symbolism for indicating that these functions have variable argument lists.  In the case of ArrayAttr(), calling it like so...


ArrayAttr(iArray(), %RETRIEVE_NUMBER_DIMENSIONS)


...as is done in the For Loop, returns the number of dimensions in the array specified as the first parameter.  The equate/define %RETRIEVE_NUMBER_DIMENSIONS was set equal to 3 at the top of my PowerBASIC code.  The PowerBASIC Help File on ArrayAttr specifies that an argument of '3' will retrieve the number of dimensions of the array from its store of vital metadata stored about the array.  So rather than using the 'mystery number' '3' I created that equate/define so as to make the code clearer in terms of the information being requested of ArrayAttr().  And since the array was dimensioned back in PBMain() as a four dimensional array the number 4 is returned by my usage as above.  So that For Loop will run from 1 through 4.  As an aside and point of difference between PowerBASIC and C/C++ looping constructs, that call to ArrayAttr() within the For Loop will only be executed once - unlike with C\C++ where it will be re-executed for every iteration of the loop. 

Next in line for discussion are the built in functions LBound() and UBound().  These built in functions return the lower and upper bounds of the specified array.  In our case above LBound returned zero for all of the four dimensions.  In PowerBASIC one can allocate arrays with any arbitrary starting subscripts such as...


Dim April(20 To 25) As Long


The above would create a Long (C int) array containing 6 elements, and a LBound call on it would return 20 rather than 0 and a UBound call on it would return 25.  But returning to our case above the LBound() and UBound() functions correctly return and output the dimensions of the array set back in PBMain(), i.e., the first dimension is 3; the second dimension is 4; the third dimension 5; and the fourth 6.   And of course the function outputs the correct random value set back in PBMain() for iArray(2,3,4,5), which is 123456789.  OK, this is all kind of magical but how can we accomplish something like this in C++?  That is the question!

In ruminating about it, that is, thinking back on MultiDim04.cpp where we provided our C version of foo() with that iNumElements count of the number of elements of the passed array, and how my PowerBASIC program stored all that metadata associated with the four dimensional array created in PBMain() and passed it along with the array to the function (behind the scenes, of course), we need to come up with some way of creating and storing this metadata about arrays in C++.  What are the language mechanisms available to us for associating data together?  The answer to that question is the C++ class.  We'll make an array class!

Let's start with the easiest task first, that is, a one dimensional array class.  Perhaps in developing that we'll get some ideas as to what's involved, so that we'll be in a better position for expanding its ideas to more dimensions.

We'll call the class CArray1 because it only has one dimension.  We'll want to make the array the class creates dynamic, so we'll do a memory allocation with C++'s new operator to acquire memory for instantiated objects.  So to start, let's assume we have this...


class CArray1
{
public:
int*   pInts;       // pointer to the base memory allocation for array
size_t iNumInts;    // Number of integers the array will store
size_t d1;          // Dimension #1 (its only a one dimensional array)

};


Those three variables listed above are fine, but we'll need some way to acquire the memory for the array.  We haven't provided a Constructor for the class yet, and that would be the most obvious way of providing memory for the array.  For a constructor we could use this...


public:   
CArray1(size_t i) : pInts(0)   // One Dimensional Array Constructor
{
  this->iNumInts=i;
  this->pInts=new int[this->iNumInts];
  d1=i;
}


Next, we need some way of assigning numbers to our array.  None of the above will do any of that.  We need an accessor or mutator in OOP speak.  What will really be almost magical is this...


int& operator()(size_t i)      // One Dimensional Accessor
{
return pInts[i];
}
 

I'll take this slow, as its complicated and tricky, and I don't want to lose my readers.  Recall in my PowerBASIC program we used parentheses after array names to contain comma delimited subscripts like this...


iAr(2, 3, 4, 5) = 123456789          ' Set a random element             

And in C or C++ we were using this syntax instead...


iArray[2][3][4][5]=123456789;      // Set some random element


Well, because of a really cool feature of C++ known as operator overloading we can redefine in our code what the meaning of certain symbols is when used in the context of a specific class.   This needs some elaboration I know.  Assume we've instantiated a CArray1 object like so which contains 10 elements...


CArray1 ar1(10);


That code will invoke the CArray1 Constructor which will perform a memory allocation for 10 ints.  If we then do this...

ar1(5) = 123456789;

...that line will invoke our operator() overload as defined just above...


int& operator()(size_t i)      // One Dimensional Accessor
{
return pInts[i];
}
 

...and cause the number 123456789 to be assigned to this->pInts[5], i.e., the 5th zero based int slot in our array controlled by our class.  The tricky reason for this is because C++ member functions which return a reference can be used on the left hand side of an equal sign to assign something to the entity which they reference, which in the case above is some i indexed 'slot' in the underlying int array controlled by the object.  Another possible way of looking at it is that a function which returns a reference is actually an lvalue, i.e., it refers to an allocated memory position somewhere.  And in this case that memory position is an index into the underlying array memory referred to by this->pInts.  I realize this will take some time to fully digest.  Perhaps if you look at all of MultiDim05.cpp and build/run it that may help...

Frederick J. Harris

#2

// g++ MultiDim05.cpp -oMultiDim05.exe -mconsole -m64 -Os -s
// cl  MultiDim05.cpp /O1 /Os /MT
#include <cstdio>

class CArray1
{
public:   
CArray1(size_t i) : pInts(0)   // One Dimensional Array Constructor
{
  this->iNumInts=i;
  this->pInts=new int[this->iNumInts];
  d1=i;
}

int& operator()(size_t i)      // One Dimensional Accessor
{
  return pInts[i];
}

~CArray1()
{
  if(this->pInts)
     delete [] this->pInts;
}

public:
int*   pInts;       // pointer to the base memory allocation for array
size_t iNumInts;    // We'll need this to zero memory for non-class types
size_t d1;          // Dimension #1
};

void foo(CArray1& ar)
{
printf("ar.iNumInts = %Lu\n\n",ar.iNumInts);
for(size_t i=0; i<ar.iNumInts; i++)
     printf("%Lu\t%d\n",i,ar(i));     
}   

int main()
{
size_t iSize;

iSize=10;
CArray1 Ar1(iSize);
for(size_t i=0; i<iSize; i++)
     Ar1(i)=(int)i;
foo(Ar1);
getchar();

return 0;
}


The only thing added to our CArray1 Class I haven't shown you yet is the Destructor, which is needed to free the allocated memory when a CArray1 object goes out of scope.  Running that program should produce this output...


ar.iNumInts = 10

0       0
1       1
2       2
3       3
4       4
5       5
6       6
7       7
8       8
9       9


Next in line is creating capability to deal with two dimensional dynamic arrays.  For that I need to broach a topic I haven't covered yet, and that would be how data is actually stored in memory and accessed when two or more array subscripts are involved.  The fundamental consideration or fact that must be dealt with is that memory allocation functions return a base address into a linear address space which continues on in memory for howsoever many bytes were requested.  If the memory allocation failed zero is always returned by low level memory allocation operating system specific functions.  In the case of multi-dimensional arrays the multiple subscripts are simply mental models, as it were, which we superimpose on the underlying linear address space to organize it into various kinds of row and column orders.  And we accomplish this through indexing schemes.  For example, suppose we wish to create a two dimensional array or table as it were consisting of four rows by five columns, e.g., something like this...


X   X   X   X   X      Row 0           0     1     2     3     4          (0,0)    (0,1)    (0,2)    (0,3)    (0,4)
X   X   X   X   X      Row 1           5     6     7     8     9          (1,0)    (1,1)    (1,2)    (1,3)    (1,4)
X   X   X   X   X      Row 2           10    11    12    13    14         (2,0)    (2,1)    (2,2)    (2,3)    (2,4)
X   X   X   X   X      Row 3           15    16    17    18    19         (3,0)    (3,1)    (3,2)    (3,3)    (3,4)


Clearly, we have 4 X 5 = 20 entries in that table.  If we're talking about an array of 20 ints in computer memory where each int requires 4 bytes, then we need to make a memory allocation for 4 X 5 X 4 = 80 bytes.  Any one of those 20 ints could be accessed through pointer notation like so if pInt is a pointer to valid memory...


1st  >>> pInt[0]
2nd  >>> pInt[1]
3rd  >>> pInt[2]
....
....
20th >>> pInt[19]


Only thing is, we don't want to do it that way.  The requirements or logic of some code we are working on require the  memory to be accessed through a row by column subscripted notation.  So we form a little index equation to relate the numbers 0 through 19 to a row by column access like this...

actual memory index = row * number_of_columns + column

For example, to arrive at the actual linear based index for the first X in zero based Row 1 above, we would have this arithmetic...

actual memory index = 1 * 5 + 0 = 5

So if our zero based array is Ar(), we want Ar(1,0).  The last X in our 4 by 5 matrix above would of course be pInt[19], but in terms of our indexing arithmetic for that linear address space organized into 4 rows and 5 columns it would be...

Ar(3,4)

...which is derived like so...

actual memory index = 3 * 5 + 4 = 19.

Got it?

OK, let's create a class for dealing with dynamic two dimensional arrays, and we'll name this class CArray2, and we'll name the program MultiDim06.cpp...


// g++ MultiDim06.cpp -oMultiDim06.exe -mconsole -m64 -Os -s
// cl  MultiDim06.cpp /O1 /Os /MT
#include <cstdio>

class CArray2
{
public:   
CArray2(size_t i, size_t j) : pInts(0)        // Two Dimensional Array Constructor
{
  this->iNumInts=i*j;
  this->pInts=new int[this->iNumInts];
  d1=i, d2=j;
}

int& operator()(size_t i, size_t j)           // Two Dimensional Accessor
{
  return pInts[i*d2 + j];
}

~CArray2()
{
  if(this->pInts)
     delete [] this->pInts;
}

public:
int*   pInts;       // pointer to the base memory allocation for array
size_t iNumInts;    // We'll need this to zero memory for non-class types
size_t d1;          // Dimension #1
size_t d2;          // Dimension #2
};

void foo(CArray2& ar)
{
printf("ar.iNumInts = %Lu\n\n",ar.iNumInts);
for(size_t i=0; i<ar.d1; i++)
{
     for(size_t j=0; j<ar.d2; j++)     
         printf("%d\t",ar(i,j));
     printf("\n");     

}     
}   

int main()
{
size_t iCtr  = 0;   
size_t iRows = 4;
size_t iCols = 5;
CArray2 Ar2(iRows, iCols);
for(size_t i=0; i<iRows; i++)
{
     for(size_t j=0; j<iCols; j++)
         Ar2(i,j)=(int)iCtr++;
}
foo(Ar2);
getchar();

return 0;
}


Compare Class CArray2 with Class CArray1 to see what we've done.  In the CArray1 Constructor for our last program we wanted a single dimension array for 10 ints.  Now we want a two dimensional array containing 20 ints, but we want it organized into four rows and five columns.  So the CArray2 Constructor needs two parameters - one for the number of rows desired, and one for the number of columns desired.  It does its multiplication of rows times columns to come up with the needed number of ints which is 20, and it does a memory allocation for 20 ints.  Finally, it stores the number of rows in member variable d1, and the number of columns in d2. Executing the above code should produce this result...


C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>g++ MultiDim06.cpp -oMultiDim06.exe -mconsole -m64 -Os -s

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>MultiDim06
ar.iNumInts = 20

0       1       2       3       4
5       6       7       8       9
10      11      12      13      14
15      16      17      18      19


Well, so far so good!  We have so far a CArray1 Class for one dimensional arrays and a CArray2 Class for two dimensional arrays.  I guess you're thinking now that we need a CArray3 Class for three dimensional arrays, and a CArray4 Class for four dimensional arrays, etc., etc?  Well, not actually.  If you compare the Constructors for our CArray1 Class and CArray2 Class you'll find they have, of course, different parameter lists...


CArray(size_t i) : pInts(0)   // One Dimensional Array Constructor
{
this->iNumInts=i;
this->pInts=new int[this->iNumInts];
d1=i, d2=0;
}

CArray(size_t i, size_t j) : pInts(0)        // Two Dimensional Array Constructor
{
this->iNumInts=i*j;
this->pInts=new int[this->iNumInts];
d1=i, d2=j;
}


Our one dimensional constructor just has a single i parameter for the row count, but our two dimensional constructor needs both an i and j parameter for the row and column size needed.  Looking at our overloaded operator() functions you'll see a similar pattern...


int& operator()(size_t i)      // One Dimensional Accessor
{
return pInts[i];
}

int& operator()(size_t i, size_t j)           // Two Dimensional Accessor
{
return pInts[i*d2 + j];
}


Each of these member functions have different parameter lists.  So they can all go in the same array class!  Rather than CArray1, CArray2, CArray3, etc, we can have just a single CArray Class that will handle as many dimensions in arrays as we care to code for.  Here would be a single CArray Class coded to handle both one and two dimensional arrays, plus a main(), foo1(), and foo2() function to exercise all the functionality...


// g++ MultiDim07.cpp -oMultiDim07.exe -mconsole -m64 -Os -s
// cl  MultiDim07.cpp /O1 /Os /MT
#include <cstdio>

class CArray
{
public:   
CArray(size_t i) : pInts(0)                  // One Dimensional Array Constructor
{
  this->iNumInts=i;
  this->pInts=new int[this->iNumInts];
  d1=i, d2=0;
}

CArray(size_t i, size_t j) : pInts(0)        // Two Dimensional Array Constructor
{
  this->iNumInts=i*j;
  this->pInts=new int[this->iNumInts];
  d1=i, d2=j;
}

int& operator()(size_t i)                    // One Dimensional Accessor
{
  return pInts[i];
}

int& operator()(size_t i, size_t j)          // Two Dimensional Accessor
{
  return pInts[i*d2 + j];
}

~CArray()
{
  if(this->pInts)
     delete [] this->pInts;
}

public:
int*   pInts;       // pointer to the base memory allocation for array
size_t iNumInts;    // We'll need this to zero memory for non-class types
size_t d1;          // Dimension #1
size_t d2;          // Dimension #2
};


void foo1(CArray& ar)
{
printf("ar.iNumInts = %Lu\n\n",ar.iNumInts);
for(size_t i=0; i<ar.iNumInts; i++)
     printf("%Lu\t%d\n",i,ar(i));     
printf("\n\n");
}


void foo2(CArray& ar)
{
printf("ar.iNumInts = %Lu\n\n",ar.iNumInts);
for(size_t i=0; i<ar.d1; i++)
{
     for(size_t j=0; j<ar.d2; j++)     
         printf("%d\t",ar(i,j));
     printf("\n");     

}     
}       

int main()
{
size_t iCtr = 0;   
size_t D1   = 10;
size_t D2   = 5;
CArray Ar1(D1);
for(size_t i=0; i<10; i++)
     Ar1(i)=(int)i;
foo1(Ar1);
D1=4;
CArray Ar2(D1, D2);
for(size_t i=0; i<D1; i++)
{
     for(size_t j=0; j<D2; j++)
         Ar2(i,j)=(int)iCtr++;
}
foo2(Ar2);
getchar();

return 0;
}

#if 0

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>cl  MultiDim07.cpp /O1 /Os /MT
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

MultiDim07.cpp
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:MultiDim07.exe
MultiDim07.obj

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>MultiDim07.exe
ar.iNumInts = 10

0       0
1       1
2       2
3       3
4       4
5       5
6       6
7       7
8       8
9       9


ar.iNumInts = 20

0       1       2       3       4
5       6       7       8       9
10      11      12      13      14
15      16      17      18      19

#endif



Frederick J. Harris

Having gotten this far the next issue I must point out is that all our work so far only pertains to the 32 bit int data type!  What about short ints, floats, doubles, etc?  Well, following and extending the logic of what we've done so far we would have to code separate classes for each of these.  And if we did that we would find that all the code is extremely similar, differing only in a couple spots, which involve just the variable type involved.  C++ does indeed have an interesting and sometimes useful feature to deal with this and that feature is known as templates.  With those we can actually just have one CArray Class which can be used to create dynamic multi-dimensional arrays not only for all the data type primitives such as ints, floats, etc., but for objects of any C++ Class whatsoever.  Here is my templated CArray Class which I keep in a file named CArray.cpp...


// CArray.cpp
#ifndef CArray_cpp                                // This entity is a templated class for creating dynamic
#define CArray_cpp                                // multi-dimensional arrays of from one to four dimensions.
                                                  // It allows for a basic language type syntax for doing
#include <new>                                    // the above.  GNU C++ compilers implement dynamic array
#define  NEW new(std::nothrow)                    // allocation (to some extent) but MS VC++ compilers don't. 
                                                 
template <class datatype> class CArray           
{                                                 
public:                                         
CArray() : pObjs(0)                              // Uninitialized Constructor (allocates no memory)
{                                               
  this->iNumObjects=0;                           
  d1=d2=d3=d4=0;
}

CArray(int i) : pObjs(0)                         // One Dimensional Array Constructor
{
  this->iNumObjects=i;
  this->pObjs = NEW datatype[this->iNumObjects]();
  d1=i, d2=0, d3=0, d4=0;
}

CArray(int i, int j) : pObjs(0)                  // Two Dimensional Array Constructor
{
  this->iNumObjects=i*j;
  this->pObjs = NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=0, d4=0;
}

CArray(int i, int j, int k) : pObjs(0)           // Three Dimensional Array Constructor
{
  this->iNumObjects=i*j*k;
  this->pObjs = NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=k, d4=0;
}

CArray(int i, int j, int k, int l) : pObjs(0)    // Four Dimensional Array Constructor
{
  this->iNumObjects=i*j*k*l;
  this->pObjs = NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=k, d4=l;
}

bool Allocate(int i)                             // Allocates A Single Dimension Dynamic Array Of Anything
{
  if(this->pObjs)
     delete [] this->pObjs;
  this->iNumObjects=i;
  this->pObjs=NEW datatype[this->iNumObjects]();
  d1=i, d2=0, d3=0, d4=0;
  if(this->pObjs)
     return true;
  else
     return false;
}

bool Allocate(int i, int j)                      // Allocates A Two Dimensional Dynamic Array Of Anything
{
  if(this->pObjs)
     delete [] this->pObjs;
  this->iNumObjects=i*j;
  this->pObjs=NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=0, d4=0;
  if(this->pObjs)
     return true;
  else
     return false;
}

bool Allocate(int i, int j, int k)               // Allocates A Three Dimensional Dynamic Array Of Anything
{
  if(this->pObjs)
     delete [] this->pObjs;
  this->iNumObjects=i*j*k;
  this->pObjs=NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=k, d4=0;
  if(this->pObjs)
     return true;
  else
     return false;
}

bool Allocate(int i, int j, int k, int l)        // Allocates A Four Dimensional Dynamic Array Of Anything
{
  if(this->pObjs)
     delete [] this->pObjs;
  this->iNumObjects=i*j*k*l;
  this->pObjs=NEW datatype[this->iNumObjects]();
  d1=i, d2=j, d3=k, d4=l;
  if(this->pObjs)
     return true;
  else
     return false;
}

datatype& operator()(int i)                      // One Dimensional Accessor
{
  return pObjs[i];
}

datatype& operator()(int i, int j)               // Two Dimensional Accessor
{
  return pObjs[i*d2 + j];
}

datatype& operator()(int i, int j, int k)        // Three Dimensional Accessor
{
  return pObjs[i*d2 + j + k*d1*d2];
}

datatype& operator()(int i, int j, int k, int l) // Four Dimensional Accessor
{
  return pObjs[i*d2 + j + k*d1*d2 + l*d1*d2*d3];
}

bool blnMemoryIsGood()
{
  return !!pObjs;
}

int UBound(int iDim)
{
  if(iDim==1)
     return d1-1;
  if(iDim==2)
     return d2-1;
  if(iDim==3)
     return d3-1;
  if(iDim==4)
     return d4-1;
  else
     return 0;
}

~CArray()
{
  if(this->pObjs)
     delete [] this->pObjs;
}

private:
datatype* pObjs;       // pointer to the base memory allocation for array
int       iNumObjects; // We'll need this to zero memory for non-class types
int       d1;          // Dimension #1
int       d2;          // Dimension #2
int       d3;          // Dimension #3
int       d4;          // Dimension #4
};

#endif
   

The above will take you pretty far.  To use it simply #include CArray.cpp after your other includes.  I might point out that this is a fairly simple and basic use of templates.  If you are unfamiliar with templates in C++ a couple hours of reading and study of them should allow you to relatively easily grasp everything or most of everything in my CArray Class. 

Before I provide a couple examples of its use, I'd like to make a few comments.  First, using templates like I've done to eliminate the need to create a separate class for each data type, i.e., one for ints, one for doubles, etc., is a pretty neat and concise thing to do.  It works out well to say the least.  But there is one somewhat unfortunate downside, which simply involves some really awkward, ugly, confusing syntax that there simply is nothing that one can do anything about.   Let me illustrate.  Recall from our earlier examples that to create a dynamic array one would use a syntax something like this   

CArray Ar1(10);

That's pretty clean, straightforward, and easy to understand.  It looks about like any construction code in C++ to create any object.  And once such an object was constructed passing it to a function was also straightforward ... 

foo(Ar1);

...and foo() might look like so if we're passing Ar1 by reference...


void foo1(CArray& ar)
{
printf("ar.iNumInts = %Lu\n\n",ar.iNumInts);
for(size_t i=0; i<ar.iNumInts; i++)
     printf("%Lu\t%d\n",i,ar(i));     
printf("\n\n");
}


Well, it gets nasty with templates.  Here is what a declaration would look like to create an array of five doubles...

CArray<double> Ar1(5);

And I suppose even that isn't terribly bad, as the only difference between a non-templated version would be the addition of the <double> term.  But the syntax for passing templated objects through functions is truly wicked.  Here would be that...


template<typename t1> void Output(CArray<t1>& Ar)
{
for(size_t i=0; i<=Ar.UBound(1); i++)
{
     printf("%8.2f\n",Ar(i));
}
}


About the only good news is that at the point of call nothing changes.  Our foo() above for Ar1 is still just...

foo(Ar1);

Second, in my templated CArray Class you'll see this near the top...


#include <new>                                   
#define  NEW new(std::nothrow)


Unlike most C++ coders I don't make use of C++ Exception Handling in my work.  If I use new to allocate memory for objects I want it to simply return NULL on memory allocation failures rather than throwing an exception.  So my macro above inserts new(std::nothrow) into my new syntax.  To use that one must include <new>.  Further, this somewhat obscure syntax ...


this->pObjs = NEW datatype[this->iNumObjects]();


...with the '()' symbols at the end causes allocated memory to be zeroed out. 

Third, you'll see a bunch of CArray::Allocate(...) member functions in my CArray Class.  What those allow are declarations of CArray objects that don't allocate memory or do anything.  For example, this...

CArray<double> Ar;

Note the above has no parameter list specifying rows, columns, or anything else.  To be able to use that one also needs to provide an uninitialized Constructor for the class, which you'll find if you examine myCArray.cpp file.  Of course, one can't make much use of that by itself, so that's where the CArray::Allocate() members come into play.  I'll provide an example in a bit.

Fourth, if you recall back when I was discussing PowerBASIC code we were working with some useful built in PowerBASIC functions named LBound() and UBound() which returned he lower and upper bounds of  the allocated array.  Well, I added a UBound() member to my CArray Class simply because its so useful. 
That's about all I can think of so lets look at some examples.  Lets now look at MultiDim08.cpp which includes the previously posted CArray.cpp  templated Multi-Dimensional Array Class...


// g++ MultiDim08.cpp -oMultiDim08.exe -mconsole -m64 -Os -s
// cl  MultiDim08.cpp /O1 /Os /EHsc /MT
#include <cstdio>
#include <string>
#include "CArray.cpp"

template<typename t1> void Output(CArray<t1>& Ar3)
{
for(size_t h=0; h<=Ar3.UBound(3); h++)
{
     for(size_t i=0; i<=Ar3.UBound(1); i++)
     {
         for(size_t j=0; j<=Ar3.UBound(2); j++)
             printf("%s\t",Ar3(i,j,h).c_str());
         printf("\n");
     }
     printf("\n");
}
}

int main()
{
size_t rows=4,cols=5,levels=6;
char szBuffer[16];

CArray<std::string> Ar3(rows,cols,levels);
for(size_t i=0; i<=Ar3.UBound(3); i++)
{
     for(size_t j=0; j<=Ar3.UBound(1); j++)
     {
         for(size_t k=0; k<=Ar3.UBound(2); k++)
         {
             Ar3(j,k,i)="(";
             sprintf(szBuffer,"%d",j);
             Ar3(j,k,i)=Ar3(j,k,i)+szBuffer+",";
             sprintf(szBuffer,"%d",k);
             Ar3(j,k,i)=Ar3(j,k,i)+szBuffer+",";
             sprintf(szBuffer,"%d",i);
             Ar3(j,k,i)=Ar3(j,k,i)+szBuffer+")";
         }
     }
}
Output(Ar3);
getchar();

return 0;   
}

#if 0

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>g++ MultiDim08.cpp -oMultiDim08.exe -mconsole -m64 -Os -s

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>MultiDim08.exe
(0,0,0) (0,1,0) (0,2,0) (0,3,0) (0,4,0)
(1,0,0) (1,1,0) (1,2,0) (1,3,0) (1,4,0)
(2,0,0) (2,1,0) (2,2,0) (2,3,0) (2,4,0)
(3,0,0) (3,1,0) (3,2,0) (3,3,0) (3,4,0)

(0,0,1) (0,1,1) (0,2,1) (0,3,1) (0,4,1)
(1,0,1) (1,1,1) (1,2,1) (1,3,1) (1,4,1)
(2,0,1) (2,1,1) (2,2,1) (2,3,1) (2,4,1)
(3,0,1) (3,1,1) (3,2,1) (3,3,1) (3,4,1)

(0,0,2) (0,1,2) (0,2,2) (0,3,2) (0,4,2)
(1,0,2) (1,1,2) (1,2,2) (1,3,2) (1,4,2)
(2,0,2) (2,1,2) (2,2,2) (2,3,2) (2,4,2)
(3,0,2) (3,1,2) (3,2,2) (3,3,2) (3,4,2)

(0,0,3) (0,1,3) (0,2,3) (0,3,3) (0,4,3)
(1,0,3) (1,1,3) (1,2,3) (1,3,3) (1,4,3)
(2,0,3) (2,1,3) (2,2,3) (2,3,3) (2,4,3)
(3,0,3) (3,1,3) (3,2,3) (3,3,3) (3,4,3)

(0,0,4) (0,1,4) (0,2,4) (0,3,4) (0,4,4)
(1,0,4) (1,1,4) (1,2,4) (1,3,4) (1,4,4)
(2,0,4) (2,1,4) (2,2,4) (2,3,4) (2,4,4)
(3,0,4) (3,1,4) (3,2,4) (3,3,4) (3,4,4)

(0,0,5) (0,1,5) (0,2,5) (0,3,5) (0,4,5)
(1,0,5) (1,1,5) (1,2,5) (1,3,5) (1,4,5)
(2,0,5) (2,1,5) (2,2,5) (2,3,5) (2,4,5)
(3,0,5) (3,1,5) (3,2,5) (3,3,5) (3,4,5)



C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>
#endif


What we're doing above is creating a three dimensional array containing std::string objects.  The array will comprise 4 rows, 5 columns and 6 what might be called 'levels', or at least some kind of height or 3rd dimension, kind of like x, y, z space in mathematics.  And the code outputs all the array positions showing all the x, y, z or i, j, k coordinate subscripts.  And you can come face to face with some of the nasty template syntax.  As you can see and like I said its not too bad at the point of call...

Output(Ar3);

...its just the function declaration syntax that's a bit trying to one's sensibilities.  Next I'll show a program where we allocate five doubles and test for allocation failures, and we'll use my CArray::Allocate() syntax and also the cool CArray::UBound() member...


// g++ MultiDim09.cpp -oMultiDim09.exe -mconsole -m64 -Os -s
// cl  MultiDim09.cpp /O1 /Os /EHsc /MT
#include <cstdio>
#include "CArray.cpp"

template<typename t1> void Output(CArray<t1>& Ar)
{
for(size_t i=0; i<=Ar.UBound(1); i++)
{
  printf("%8.2f\n",Ar(i));
}
}

int main()
{
CArray<double> Ar1;
size_t iNumDoubles;   

iNumDoubles = 5;
if(Ar1.Allocate(iNumDoubles))
{   
    Ar1(0)=0.1111111;
    Ar1(1)=1.2222222;
    Ar1(2)=2.3333333;
    Ar1(3)=3.4444444;
    Ar1(4)=4.5555555;
    Output(Ar1);
}
getchar();

return 0;   
}

#if 0
C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>cl  MultiDim09.cpp /O1 /Os /MT
Microsoft (R) C/C++ Optimizing Compiler Version 15.00.21022.08 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

MultiDim09.cpp
Microsoft (R) Incremental Linker Version 9.00.21022.08
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:MultiDim09.exe
MultiDim09.obj

C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>MultiDim09.exe
    0.11
    1.22
    2.33
    3.44
    4.56


C:\Code\CodeBlks\Multi_Dimensional_Arrays\Array01>
#endif


And this one does the above in a more concise manner...


// g++ MultiDim10.cpp -oMultiDim10.exe -mconsole -m64 -Os -s
// cl  MultiDim10.cpp /O1 /Os /EHsc /MT
#include <cstdio>
#include "CArray.cpp"

template<typename t1> void Output(CArray<t1>& Ar)
{
for(size_t i=0; i<=Ar.UBound(1); i++)
     printf("%8.2f\n",Ar(i));
}

int main()
{
CArray<double> Ar1(5);
Ar1(0)=0.1111111;
Ar1(1)=1.2222222;
Ar1(2)=2.3333333;
Ar1(3)=3.4444444;
Ar1(4)=4.5555555;
Output(Ar1);
getchar();

return 0;   
}


James C. Fuller

Fred,
  I wrote a couple array scanners for your String (fstring for me).
I modified your latest CArray and made the datatype* pObjs public so I could test them .
It is probably a better idea to add a function to retrieve it but I was not sure how to do that.

James


typedef String fstring;
int     fsAryScan (fstring* , fstring&, bool = false);
int     fsAryScan (fstring* , const _TCHAR* , bool = false);

int fsAryScan (fstring*  fsary, fstring&  fsFind, bool  bCase)
{
    int     idx = {0};
    while(fsary[idx].Len())
    {
        if(fsary[idx].InStr(fsFind, bCase, 0))
        {
            return idx;
        }
        idx++;
    }

    return -1;
}


int fsAryScan (fstring*  fsary, const _TCHAR*  sfind, bool  bCase)
{
    int     idx = {0};
    while(fsary[idx].Len())
    {
        if(fsary[idx].InStr(sfind, bCase, 0))
        {
            return idx;
        }
        idx++;
    }

    return -1;
}


Frederick J. Harris

I imagine just...


datatype* getptr()
{
return pObjs;  // Or, return this->pObjs;
}


I believe your AedDynArray() had a getptr() member too, so I used that name.

Perhaps I ought to update this code to use my new version with the changed UBound() stuff and the Redim Preserve. 

The reason I made this post is that I feel so terrible when I see new C++ coders trying to learn to code and struggling so horribly bad with multi-dimensional dynamic arrays.  Of course, there are various solutions to this in various libraries, but the bare bones C++ language itself, i.e., the core language without including and headers, has no solution for it. 

I'd been thinking of our discussion about static/global class instantiations and TCLib.  You know, Matt Pietrek's code to handle that, which does seem to work I found last Friday night in my little test, only really adds something like 512 or 1024 bytes to base TCLib sizes.  Considering the extra capability that adds, I'm beginning to feel it might be worthwhile to add it, or at least support two versions of TCLib - one with and one without.  Like I said, we're talking 512 to 1024 bytes.  I can't do it now - got lots of irons in the fire, but possibly this summer.

Frederick J. Harris

Forgive me and ignore this comment if this is common knowledge that everybody knows, but it strikes me as possible that everybody isn't aware of tghis so I'll take a chance and mention it.  With a construct such as this in C++...


while(fsary[idx].Len())


...that constitutes a seperate member function call for every iteration of the loop.  This is different behavior from PowerBASIC.  For example - and using a For Loop, If you have this...


Function SomeFunction() As Long
....
End Function

For i=1 To SomeFunction()
....
Next i


...PowerBASIC will call the function to determine the terminating condition once - not for every iteration of the loop.  With C++, the function is called for every iteration.  For that reason, I seldom put function calls in the terminating condition of a loop.  I mean, it might be meaningless for non-critical things, but for serious data processing work where performance is important I feel it could be an issue. 

I'm pretty sure of all the above.  Haven't checked in a long time though.  Maybe somebody wants to test it and prove me wrong?