How Do I define a Double Brackets/Double Iterator Operator, Similar to Vector of Vectors'?

Jason R. Mick picture Jason R. Mick · Sep 20, 2010 · Viewed 9k times · Source

I'm porting code that uses a very large array of floats, which may trigger malloc failures from c to c++. I asked a question about whether I should use vectors or deques and Niki Yoshiuchi generously offered me this example of a safely wrapped type:

template<typename T>
class VectorDeque
{
private:
  enum TYPE { NONE, DEQUE, VECTOR };
  std::deque<T> m_d;
  std::vector<T> m_v;
  TYPE m_type;
  ...
public:
  void resize(size_t n)
  {
    switch(m_type)
    {
      case NONE:
      try
      {
        m_v.resize(n);
        m_type = VECTOR;
      }
      catch(std::bad_alloc &ba)
      {
        m_d.resize(n);
        m_type = DEQUE;
      }
      break;
    }
  }
};

I needed a 2D vector of vectors/deque of deques, so I modified it to the following code:

template<typename T>
class VectorDeque
{
private:
  enum STORAGE_CONTAINER { NONE, DEQUE, VECTOR };
  std::deque<std::deque<T> > x_d,y_d,z_d;
  std::vector<std::vector<T> > x_v,y_v,z_v;
  TYPE my_container;
public:
  void resize(size_t num_atoms, size_t num_frames)
  {
    switch(m_type)
    {
      case NONE:
      try
      {
        x_v.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   x_v[counter].resize(num_frames);
        y_v.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   y_v[counter].resize(num_frames);
        z_v.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   z_v[counter].resize(num_frames);
        my_container = VECTOR;
      }
      catch(std::bad_alloc &e)
      {
        x_d.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   x_d[counter].resize(num_frames);
        y_d.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   y_d[counter].resize(num_frames);
        z_d.resize(num_atoms);
 for (unsigned int couter=0;couter < num_frames; counter++)
   z_d[counter].resize(num_frames);
        my_container = DEQUE;
      }
      break;
    }
  }
};

I now want to be able to define my bracket operators so that I can have a statement like x[1][2] directly access whichever is the real memory container I'm using (given by the value of my enumerated variable.

I've seen a couple of tutorials floating around about overriding the brackets operator, but have positively no idea to override double brackets.

How can you overload double brackets?

Additionally, how would you overload double iterators (in case I want to use an iterator, as opposed to direct indexing)?

EDIT 1:

Based on the solution from Martin York/Matteo Italia I devised the following class:

template<typename T>
class VectorDeque2D
{
public:

  class VectorDeque2D_Inner_Set
  {
    VectorDeque2D& parent;
    int   first_index;
  public:
    // Just init the temp object
    VectorDeque2D_Inner_Set(My2D& p, int first_Index) : 
      parent(p), 
      first_Index(first_index) {} 
    // Here we get the value.
    T& operator[](int second_index)  const 
    { return parent.get(first_index,second_index);}   
  };

  // Return an object that defines its own operator[] that will access the data.
  // The temp object is very trivial and just allows access to the data via 
  // operator[]
  VectorDeque2D_Inner_Set operator[](unsigned int first_index) { 
    return (*this, x);
  }


  void resize_first_index(unsigned int first_index) {
    try {
      my_vector.resize(first_index);
      my_container = VECTOR;
    }
    catch(std::bad_alloc &e) {
      my_deque.resize(first_index);
      my_container = DEQUE;
    }
  }

  void resize_second_index(unsigned int second_index) {
    try {
      for (unsigned int couter=0;couter < my_vector.size(); counter++) {
    my_vector[counter].resize(second_index);
      }
      my_container = VECTOR;
    }
    catch(std::bad_alloc &e) {
      for (unsigned int couter=0;couter < my_deque.size(); counter++) {
    my_deque[counter].resize(second_index);
      }
      my_container = DEQUE;
    }
  }
  void resize(unsigned int first_index,
          unsigned int second_index) {
    try {
      my_vector.resize(first_index);
      for (unsigned int couter=0;couter < my_vector.size(); counter++) {
    my_vector[counter].resize(second_index);
      }
      my_container = VECTOR;
    }
    catch(std::bad_alloc &e) {
      my_deque.resize(first_index);
      for (unsigned int couter=0;couter < my_deque.size(); counter++) {
    my_deque[counter].resize(second_index);
      }
      my_container = DEQUE;
    }    
  }
private:
  enum STORAGE_CONTAINER { NONE, DEQUE, VECTOR };

  friend class VectorDeque2D_Inner_Set;

  std::vector<std::vector<T> > my_vector;
  std::deque<std::deque<T> > my_deque;
  STORAGE_CONTAINER my_container;

  T& get(int x,int y) { 
    T temp_val;
    if(my_container == VECTOR) {
      temp_val = my_vector[first_index][second_index];
    }
    else if(my_container == DEQUE) {
      temp_val = my_deque[first_index][second_index];
    }

    return temp_val;
  }

};

Finally a size-safe 2D container!! Thanks guys!

Answer

Martin York picture Martin York · Sep 20, 2010

There are two main techniques:

1) Use operator() rather than operator[].
This is because the operator() allows multiple parameters.

class My2D
{
    public:
       int&   operator()(int x,int y)  { return pget(x,y);}
    private:
       int&   pget(int x,int y) { /* retrieve data from 2D storage */ }
};

2) Use operator[] but return an intermediate object.
You can then apply the second operator[] to the intermediate object.

class My2D
{
    public:
       class My2DRow
       {
           My2D& parent;
           int   x;
           public:
               My2DRow(My2D& p, int theX) : parent(p), x(theX) {}     // Just init the temp object
               int& operator[](int y)  const { return parent.pget(x,y);}   // Here we get the value.
       };

       // Return an object that defines its own operator[] that will access the data.
       // The temp object is very trivial and just allows access to the data via operator[]
       My2DRow operator[](int x)        { return My2DRow(*this, x);}
    private:
       friend class My2DRow;
       int&   pget(int x,int y) { /* retrieve data from 2D storage */ }
};

int main()
{
    My2D   data;
    int&   val = data[1][2]; // works fine.

    // This is the same as
    My2D::My2DRow row  = data[1];
    int&          val2 = row[2]; 
}

I prefer the second technique.
This is because it leaves the original code untouched and more natural to read (in an array context). Of course you pay for the simplicity at the high level with slightly more complex code implementing your 2D array.