Multidimensional arrays and operator[]

Hello,

I am currently wondering why operator[] cannot be used to index multidimensional arrays. My simple class definition is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class classDeclarations
{
private:
        int array1[5][5];
public:
        classDeclarations(int startValue = 0)
        {
                for (int i = 0; i < 5; i++)
                {
                        for (int j = 0; j < 5; j++)
                        {
                                array1[i][j] = startValue;
                        }
                }
        }
 
        int* operator[](int index) { return &(array1[index]); }
}


When I try to set a class's [0][0] index, it does not work. I was just wondering why something like this would not work. I was thinking that the compiler would do array1[0] first, return its array, and then do array1[0][0].

Any reasons why multidimensional arrays will not work with operator[]? Any fixes?

If you would like to see my full code, here it is http://www.ideone.com/xTSMT
Last edited on
In a given array such a T someArray [5][8] (where T meany type - for example int) -
then the result of someArray[index] is an array of T[8]
which would mean that & someArray[index] is
in this case T (*)[8] - which is a pointer to array[8] of int.

We understand what you are trying to do - but a compiler will not implicitly
bind a T* to T (*)[] - so but will implicitly bind T* to T[].

So you have choice :
1....
1
2
3
4
int* operator[](int index) 
{ 
    return (int*)(&array1[index]); //make a cast - this is the long winded and uneccessary way 
 }


2.
1
2
3
4
int* operator[](int index) 
{ 
    return (array1[index]);//implicit conversion by the compiler - no need to take address and then cast it. 
 }


There is an article somewhere on the articles section about arrays and pointer
conversions
@guestgulkan,

Very nice explanation. LearnCpp was telling me that the operator[] only works for 1-dimensional arrays, but I didn't believe it.

However, I do have two questions.

which would mean that & someArray[index] is
in this case T (*)[8] - which is a pointer to array[8] of int.


Does that simply mean I am trying to dereference an array? Mind explaining more about this?


So you have choice :
1....
1
2
3
4
int* operator[](int index) 
{ 
    return (int*)(&array1[index]); //make a cast - this is the long winded and uneccessary way 
 }



Does this simply dereference the index into an anonymous pointer to an integer?

Thanks though, I think it is becoming more clear!

flurite wrote:
Very nice explanation. LearnCpp was telling me that the operator[] only works for 1-dimensional arrays, but I didn't believe it.


What they mean is that you can only make an operator[] overload.
You can't have operator[][] (if you look at the list of C++ operators, there is
an operator [] but no single operator [][])

1
2
3
4
5
6
class SomeClass
{

    public:
    int operator[][] (int x, inty); //Error - there is no such thing as a [][]  operator.
}
;


(When you derefence a two dimensional array using [][] - you are not
calling a single operator [][] - you are calling [] twice.
This means that each pre-ceding [] must return a pointer type (or something that can take a [] operation) or the next [] will fail).

You might have to think hard a little while about that.
@guestgulkan,

Okay, so the problem with doing

& someArray[index]

is that I am trying to get the address of a pointer, but this is not the proper way of doing that?
&arrayOfPointers[index] will give you the address of the pointer. But this means you have a pointer to a pointer to an int (int **), when what you really want is an int *. Simply return array[index], and you will have your int *, which in this case is cast from int [], but the two types are so similar that they can often be used interchangeably.

@guestulkan, I can't say I like your long winded answer return (int*)(&array1[index]);. It works for arrays of type 'int [][]', but will NOT work for a dynamically allocated 'int **' array, and if someArray is changed to an int** at some point, the resulting error could be difficult to locate.
Last edited on
1
2
3
4
5
6
7
8
9
10
   
    char CharArray [5];

   char * pc1 = CharArray;  //OK (1)

   char *pc2 = &CharArray; //Error (2)

   char (*pc3)[5] = &CharArray; //OK (3)

    char* pc4 = (char*)&CharArray; //OK (4) 


(1) - This is ok - C++ does an implicit conversion between an array and a pointer to its type. Notice we do not use the & (address) operator on the array name.

(2) When we take the address of array we are creating a pointer to the array - in this case a pointer to an array of 5 chars char(*)[5] .
On the left side of the = sign we have char*
The compiler will not implicitly do char* = char(*)[5] - the pointer types are different.

(3) OK - we change the pointer types on the left of the = to match what we explained in (2) above.

(4) WE KNOW that the address of the array is the start of the array which is
an array of char - so the address of the array can be cast to a char* - but
we have to do that our self because as we explained in (2) above, the compiler does not do this implicitly.
Last edited on
@rollie,

I see what you are saying. So the first pointer is the pointer to arrayOfPointers[index], which is a value in an array. Is the second pointer anonymously made, because of the address-to operator?
@rollie.
I was really taking about the C/C++ builtin arrays.
(but we know that the usual method of doing dynamic arrays, and also vector of vectors follow the sematics of array deferencing but the actual layout of the data are not the same)
You can get the syntax array[row][col] to work with user defined operator[]s so it sort of works like the non-existent operator[][] by using a "row accessor" helper class (I can't remember the proper name for right at this moment; maybe a smart array reference?). For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

static const int row_count = 5;
static const int col_count = 5;
	
class MyArray
{
private:
	friend class RowAccessor;

        int m_data[row_count][col_count];

public:
        MyArray(int startValue = 0, int increment = 0) {
                for (int row = 0; row < row_count; ++row) {
                        for (int col = 0; col < col_count; ++col) {
                                m_data[row][col] = startValue;
				startValue += increment;
                        }
                }
        }
	
	int RowCount() const {
		return row_count;
	}
	
	int ColCount() const {
		return col_count;
	}	
	
	class RowAccessor {
	private:
		int* m_row_data;
		
	public:
		RowAccessor(int* row_data) : m_row_data(row_data) {
		}

		int& operator[](int col) {
			// should really check that col is in range here
			return m_row_data[col];
		}
		
		int operator[](int col) const {
			// should really check that col is in range here
			return m_row_data[col];
		}		
	};
 
        RowAccessor operator[](int row) {
		// should really check that row is in range here		
		return RowAccessor(m_data[row]);
	}
	
        const RowAccessor operator[](int row) const {
		// should really check that row is in range here
		return RowAccessor(m_data[row]);
	}	
};

int main() {
	MyArray example(2, 3);
	
	int row_count = example.RowCount();
	int col_count = example.ColCount();
	
	for(int row = 0; row_count > row; ++row) {
		for(int col = 0; col_count > col; ++col) {
			cout << "(" << row << ", " << col << ") = " << example[row][col] << endl;
		}
	}
	
	return 0;
}


Andy

PS I think I got the rows and cols the right way around?
Last edited on
You can do simpler. For example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class classDeclarations
{
private:
        int array1[5][5];
public:
        classDeclarations(int startValue = 0)
        {
                for (int i = 0; i < 5; i++)
                {
                        for (int j = 0; j < 5; j++)
                        {
                                array1[i][j] = startValue;
                        }
                }
        }
 
	int ( & operator []( int i ) )[5]
	{
		return ( a[i] );
	}
};


However if you will change int i to size_t i this code will not be compiled with MS VC++ 2010, but will be compiled with GCC. It seems that MS VC++ 2010 contains a bug.
I highly recommend against overloading the [] operator for multidimentional arrays. It forces you to break encapsulation and is very ugly.

The simpler approach would be to overload the () operator and use that instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class foo
{
  int array1[5][5];

public:
  int& operator () (int a, int b)
  {
    return array1[a][b];
  }
};

//...
foo myarray;
myarray(x,y) = 10;
> I highly recommend against overloading the [] operator for multidimentional arrays.
> It forces you to break encapsulation

It deasn't force anyone to break encapsulation - depends on how the overloaded the operator is implemented.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <initializer_list>
#include <type_traits>
#include <stdexcept>
#include <iostream>

template< typename T, std::size_t ROWS, std::size_t COLS > struct array2
{
     array2( const T& v = T() ) { for( auto& r : a ) for( auto& x : r ) x = v ; }
     array2( std::initializer_list< std::initializer_list<T> > il ) ; // TODO
     template< typename ITERATOR > array2( ITERATOR begin, ITERATOR end ) ; // TODO

     template < bool CONST = false > struct row ;

     row<> operator[] ( std::size_t n )
     {
         if( n >= ROWS ) throw std::out_of_range("invalid row") ;
         return row<false>( a[n] ) ;
     }

     row<true> operator[] ( std::size_t n ) const ; // TODO

     // iterators etc. // TODO

     template < bool CONST > struct row
     {
         typedef typename std::conditional< CONST,
                        typename std::add_const<T>::type, T >::type type ;
         row( const row& ) = default ;

         type& operator[] ( std::size_t pos )
         {
             if( pos >= COLS ) throw std::out_of_range("invalid col") ;
             return r[pos] ;
         }

         const T& operator[] ( std::size_t pos ) const
         {
             if( pos >= COLS ) throw std::out_of_range() ;
             return r[pos] ;
         }
         
         type* begin() { return std::begin(r) ; }
         type* end() { return std::end(r) ; }
         const T* begin() const { return std::begin(r) ; }
         const T* end() const { return std::end(r) ; }

         private:
             T (&r) [COLS] ;
             row( T (&rr) [COLS] ) : r(rr) {}
             friend class array2<T,ROWS,COLS> ;
     };

     private: T a [ROWS] [COLS] ;
};




> and is very ugly.
> The simpler approach would be to overload the () operator

That is at best a matter of personal opinion.
IMNSHO, overloading the function call operator is extremely ugly - for starers, it violates the principle of least surprise. Overloading the subscript operator is far less ugly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template< typename T, std::size_t ROWS, std::size_t COLS >
void do_try( array2<T,ROWS,COLS>& a, std::size_t r, std::size_t c, const T& v )
{
    std::cout << "row: " << r << ", col: " << c << " => " ;
    try { a[r][c] = v ; std::cout << "ok: " << a[r][c] << '\n' ; }
    catch( const std::exception& e ) { std::cerr << "error: " << e.what() << '\n' ; }
}

int main()
{
    array2<int,5,6> a ;

    do_try( a, 7, 0, 99 ) ;
    do_try( a, 3, 9, 99 ) ;
    do_try( a, 3, 2, 99 ) ;

    array2<int,5,6>::row<> slice[] = { a[2], a[3] } ;
    slice[1][1] = 77 ;

    for( int& v : a[3] ) v += 106 ;
    for( int v : a[3] ) std::cout << v << ' ' ;
}

Output:
row: 7, col: 0 => error: invalid row
row: 3, col: 9 => error: invalid col
row: 3, col: 2 => ok: 99
106 183 205 106 106 106

JLBorges wrote:
overloading the function call operator is extremely ugly - for starters, it violates the principle of least surprise


Not to the readers of C++FAQ or the users of boost.ublas or Eigen.
> Not to the readers of C++FAQ or the users of boost.ublas or Eigen.

There are more; for instance Blitz++, Lapack++
Topic archived. No new replies allowed.