如何定义一个类似于向量嵌套的双括号/双迭代器运算符?

11

我正在将使用非常大的浮点数数组的代码从c移植到c ++,这可能会触发malloc故障。 我问了一个关于是否应该使用向量或deque的问题,Niki Yoshiuchi 慷慨地为我提供了这个安全包装类型的示例:

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;
    }
  }
};

我需要一个二维的向量/双端队列的向量,因此我将其修改为以下代码:

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;
    }
  }
};

我现在希望能够定义我的括号运算符,以便可以使用类似于 x[1][2] 这样的语句直接访问我正在使用的“真实”内存容器(由枚举变量的值给出)。

我看到了一些关于重载括号运算符的教程,但完全不知道如何重载双括号。

你如何重载双括号?

此外,如果我想使用迭代器而不是直接索引,你如何重载双迭代器?

编辑 1:

根据 Martin York/Matteo Italia 的解决方案,我设计了以下类:

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;
  }

};

终于有了一个大小安全的 2D 容器!感谢大家!

5个回答

20

有两种主要的技术:

1)使用operator()而不是operator[]。
这是因为operator()允许使用多个参数。

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) 使用 operator[] 返回一个中间对象。
然后,您可以将第二个 operator[] 应用于中间对象。

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]; 
}

我更喜欢第二种技术。
这是因为它不会改变原始代码,更自然地在数组上下文中阅读。当然,在较高层次上,你需要用稍微复杂一些的代码来实现你的二维数组,但这是为了追求简单而付出的代价。


2
不错!再次感谢你和Matteo提供的解决方案,我认为这是一个“真正”的解决方案。那些抱怨没有办法做到这一点的人只是被卡在没有重载单一[][]运算符这一事实上。 - Jason R. Mick

4
你如何重载双括号?
我不完全理解你的问题,但你需要重载括号,并使其返回一个重载了自己的括号运算符的对象。
例如,如果你有一个向量的向量,那么工作已经完成:vector < vector < something > > 重载了 operator[],它返回一个 vector< something >;这个向量又重载了它自己的括号运算符(并返回一个 something 对象),所以你可以简单地这样做:
vector<vector<something> > vec;
// ...
something s = vec[2][3];


使用代理对象的示例:

template <typename T>
class Container
{
private:
    // ...


public:

    // Proxy object used to provide the second brackets
    template <typename T>
    class OperatorBracketHelper
    {
        Container<T> & parent;
        size_t firstIndex;
    public:
        OperatorBracketHelper(Container<T> & Parent, size_t FirstIndex) : parent(Parent), firstIndex(FirstIndex) {}

        // This is the method called for the "second brackets"
        T & operator[](size_t SecondIndex)
        {
            // Call the parent GetElement method which will actually retrieve the element
            return parent.GetElement(firstIndex, SecondIndex);
        }

    }

    // This is the method called for the "first brackets"
    OperatorBracketHelper<T> operator[](size_t FirstIndex)
    {
        // Return a proxy object that "knows" to which container it has to ask the element
        // and which is the first index (specified in this call)
        return OperatorBracketHelper<T>(*this, FirstIndex);
    }

    T & GetElement(size_t FirstIndex, size_t SecondIndex)
    {
        // Here the actual element retrieval is done
        // ...
    }
}

(在适当的地方添加重载的const方法:)

请注意,使用此方法几乎不会失去与operator()实现相关的任何内容,因为检索仍然在一个单一的位置进行,没有对两个索引的使用限制,在执行检索时同时具有两个索引,并且不返回“fat”临时对象(OperatorBracketHelper只有两个指针那么大,并且可以轻松地被编译器优化掉)。


我想能够使用双括号引用我的类型VectorDeque中包含的数据。你的示例与我尝试做的不同,因为我不是要为VectorDeque定义[],而是要为VectorDeque定义[][],因为我的声明只是VectorDeque(); 而不是VectorDeque < VectorDeque <... - Jason R. Mick
2
好的,这只是一个例子。请记住不存在[][]运算符,只有operator[],它可能返回重载operator[]的类型。在这种情况下,您应该创建一个临时代理对象来返回VectorDeque的operator[],该代理对象将保留对其父对象的引用,并在调用其operator[]时向其请求“正确”的结果。我会尝试在一分钟内添加一个示例。 - Matteo Italia
不要,看一下我刚刚添加的例子(希望我做得对 :))。 - Matteo Italia
Heck,@Martin York更快 :) - Matteo Italia
1
@ Matteo Italia:非常好的例子。只需通过引用返回值(使它们成为lvalue),从而允许您在原地修改它们。x [3] [4] = 5; - Martin York
不错...我会简单测试一下。我认为你可能有解决方案!这就是我一直在寻找的...像普通的2D向量/双端队列一样访问它,在我的代码的其余部分中,同时保持在调整大小时所有更改都转换到适当的容器类。 - Jason R. Mick

2
在C++中不存在“双方括号”运算符。您需要做的是定义单个[]运算符,并使其返回对另一个对象的引用,该对象可以依次响应其自己的[]运算符。这可以嵌套多层深度,以满足您的需求。
例如,当您创建一个向量的向量时,外部向量上的[]运算符返回内部向量之一的引用;该向量上的[]运算符返回向量的单个元素的引用。
std::vector<std::vector<float> > example;
std::vector<float> & first = example[0];  // the first level returns a reference to a vector
float & second = example[0][0];  // the same as first[0]

1
@Jason,我认为你没有理解我的回答。我一开始并没有说你不能这样做;我是从最明显的答案是错误的角度入手,然后详细说明了获取你想要的内容的正确方法。我认为我的解释非常简单明了,请告诉我我哪里做错了。 - Mark Ransom
好吧,你似乎一直在强调不能重载[][],因为它不是一个真正的运算符。我并不关心重载[][],我只是想能够使用[][]对自定义类型进行索引,而不需要嵌套。Martin York和Matteo Italia提供了这个功能。你的答案在技术上是完全正确的,但你需要更多地跳出思维定势,这样你的答案会更有帮助。 - Jason R. Mick
@Jason,我能理解我以这种方式打开答案可能会给你留下错误的印象。当然,我并没有“固执己见”,但是那个关键事实似乎对于理解其余答案至关重要。很抱歉你没觉得有帮助,也许下一个发现这个问题的人会持相反意见。 - Mark Ransom

1

不要重载[]运算符,而是重载()运算符。

请参阅此链接:重载下标运算符。

我强烈建议在发布到Stack Overflow之前至少阅读一遍C++ FAQ Lite。此外,在Stack Overflow上搜索可能也会得到一些有用的信息。


使用()代替+1是更好的选择。在实现多维索引时,这个细节经常被忽视。 (天知道我做过不止一次... :) - Macke
浏览了那一节和他的解决方案。它提到可以使用[][]. 我读了他为什么通常不想这样做的理由,但我想保留它向量/双端队列的特性,所以在这种情况下我认为它值得承担缺点。但是你怎么做呢?他没有提供太多细节。我想我需要嵌套类或其他东西,但如何在这样的嵌套结构中保留try/catch处理呢? - Jason R. Mick
2
@Jason R. Mick:和C++FAQ中的许多内容一样,这个问题非常片面且带有批判性。我不同意。为了使代码更易于阅读和维护,我建议提供使用[][]的能力,因为这样做可以使代码更易于阅读(尽管会使矩阵类稍微难以阅读)。下面我提供一个简单的模板,演示如何在不暴露实现细节的情况下提供灵活性(PS:这是一个相对标准的模式(不是我发明的))。 - Martin York
@LokiAstari 尽管有点特殊,但我想要补充的是,除了更易读之外,使用[][]还可以让括号运算符用于多维函数对象。我曾经使用它来创建绑定,以便向OpenCL的n维范围内核空间发出参数。 - That Realty Programmer Guy

0

我在以前的问题的答案中介绍了如何重载多维数组的operator[]。

对于迭代器,我可能会采用类似的方式处理:有一个迭代器表示多维数组的“切片”(行或列),然后另一个迭代器表示该切片中的元素。


你的答案似乎来自这里:http://www.parashift.com/c++-faq-lite/operator-overloading.html#faq-13.10。然而,这仍然没有回答我的问题。有没有一种方法可以使用[][]?如果将我的类转换为嵌套类,我会接受一个答案。但是,在出现错误的malloc情况下,所有的VectorDeque实例都需要更改为Deque,而不仅仅是其中一个... - Jason R. Mick
1
@Jason:实际上不是这样的。FAQ中的答案来自古老的Usenet帖子,其中一些是我写的。例如,Robert Martin和我在1996年的一个主题中提供了两个答案,我相信这早于将其包含在FAQ中:http://groups.google.com/group/comp.lang.c++.moderated/browse_frm/thread/0a52d34d173dd27d/298b8a42e13f4986 - Jerry Coffin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接