【问题标题】:Can I use the [] operator in C++ to create virtual arrays我可以在 C++ 中使用 [] 运算符来创建虚拟数组吗
【发布时间】:2010-05-11 09:05:49
【问题描述】:

我有一个大型代码库,最初是 C 多年前移植到 C++ 的,它正在对大量空间数据的大型数组进行操作。这些数组包含表示表示表面模型的点和三角形实体的结构。我需要重构代码,以便这些实体在内部存储的具体方式因特定场景而异。例如,如果点位于规则的平面网格上,我不需要存储 X 和 Y 坐标,因为它们可以即时计算,三角形也可以。同样,我想利用诸如STXXL 之类的核心工具进行存储。最简单的方法是用 put 和 get 类型函数替换数组访问,例如

point[i].x = XV;

变成

Point p = GetPoint(i);
p.x = XV;
PutPoint(i,p);

您可以想象,这是对大型代码库的非常繁琐的重构,在途中容易出现各种错误。我想做的是编写一个通过重载 [] 运算符来模仿数组的类。由于数组已经存在于堆上,并且随着 reallocs 移动,代码已经假定对数组的引用,例如

point *p = point + i;

可能无法使用。这个类可以写吗?例如用 [] 运算符编写下面的方法;

void MyClass::PutPoint(int Index, Point p)
{
   if (m_StorageStrategy == RegularGrid)
   {
      int xoffs,yoffs;
      ComputeGridFromIndex(Index,xoffs,yoffs);
      StoreGridPoint(xoffs,yoffs,p.z);
    } else
       m_PointArray[Index] = p;   
  }
}

Point MyClass::GetPoint(int Index)
{
   if (m_StorageStrategy == RegularGrid)
   {
      int xoffs,yoffs;
      ComputeGridFromIndex(Index,xoffs,yoffs);
      return GetGridPoint(xoffs,yoffs);   // GetGridPoint returns Point
    } else
       return m_PointArray[Index];   
  }
}

我担心的是我见过的所有数组类都倾向于通过引用传递,而我认为我必须通过值传递结构。我认为它应该在性能之外起作用,任何人都可以看到这种方法的任何主要缺陷。注意我必须按值传递的原因是为了得到 ​​p>

point[a].z = point[b].z + point[c].z

在底层存储类型不同的情况下正常工作。

【问题讨论】:

    标签: c++ dynamic-arrays


    【解决方案1】:

    您不需要按值传递数组。为了改变数组中的值,您需要两个版本的operator[],一个返回一个引用(用于变异),一个返回一个常量引用。

    原则上没有理由不使用operator[],只要您不需要在运行时更改存储类型 - 没有虚拟运算符,因此您需要一个命名函数运行时多态性。在这种情况下,您可以创建一个简单的struct,它将运算符调用调整为函数调用(尽管它取决于存储 API - 如果代码假定分配给点的成员变量会更改存储的数据,您可能必须使点类型也成为模板变量,以便可以覆盖)。

    查看您的示例代码,它对存储策略进行了测试。不要这样做。要么使用 OO 并让你的存储对象实现一个通用的虚拟接口,要么(可能更好)使用模板编程来改变存储机制。

    如果您查看std::vector(在最近的 C++ 标准中)所做的保证,那么可能会有一些具有动态存储并允许使用指针运算的东西,尽管这需要连续存储。鉴于您的某些值是动态创建的,可能不值得对您的实现进行限制,但约束本身并不会阻止使用operator[]

    【讨论】:

    • 返回对点的引用意味着对所有同时处于活动状态的潜在活动引用具有多个唯一点。这将需要转换数据的缓存,但应该是可行的。感谢您的回答。
    • WRT 使用存储策略而不是 OO 或模板,它不会工作,因为现有代码不是为了支持它而编写的,主要要求是尽量减少重写量。我想在后端加入一些运行时多态的东西,但在前端模仿了一个 C 数组。如果我从头开始编写,模板无疑是最佳选择。
    【解决方案2】:

    您想要的都是可能的,但是由于您也需要写入权限,因此有时结果会稍微复杂一些。您想要的是 setter 函数返回的不是直接的“点写入访问”,而是一个临时副本,一旦副本超出范围,它将执行写入。

    以下代码片段试图概述解决方案:

    class PointVector
    {
      MyClass container_;
    
      public:
      class PointExSet: public Point
      {
        MyClass &container_;
        int index_;
    
        public:
        PointExSet(MyClass &container, int index)
          :Point(container.GetVector(index)),container_(container),index_(index)
        {
        }
    
        ~PointExSet()
        {
          container_.PutVector(index_) = *this;
        }
      };
    
      PointExSet operator [] (int i)
      {
        return PointExSet(container_,i);
      }
    };
    

    它没有你希望的那么好,但恐怕你无法在 C++ 中找到更好的解决方案。

    【讨论】:

    • 是的,因此需要参考。我想我应该能够使用转换类型的缓存来做到这一点,索引与用于维护缓存的数组大小相同。
    【解决方案3】:

    要完全控制数组上的操作,operator[] 应该返回一个特殊的对象(很久以前发明并称为“光标”),它将为您处理操作。 举个例子:

    class Container
    {
      PointCursor operator [] (int i)
      {
        return PointCursor(this,i);
      }
    };
    class PointCursor
    {
    public:
        PointCursor(_container, _i)
           : container(_container), i(_i),
             //initialize subcursor
             x(container, i) {}     
    
        //subcursor
        XCursor x;
    private:
       Container* container;
       int i;
    };
    class XCursor
    {
    public:
        XCursor(_container, _i)
          : container(_container), i(_i) {}
    
         XCursor& operator = (const XCursor& xc)
         {
              container[i].x = xc.container[xc.i].x;
              //or do whatever you want over x
         }
    
         Container* container;
         int i; 
    }
    //usage
    my_container[i].x = their_container[j].x; //calls XCursor::operator = ()
    

    【讨论】:

      【解决方案4】:

      阅读上述答案后,我认为Pete 的两个版本operator[] 的答案是最好的前进方式。为了在运行时处理类型之间的变形,我创建了一个新的数组模板类,它采用如下四个参数;

      template<class TYPE, class ARG_TYPE,class BASE_TYPE, class BASE_ARG_TYPE>
      class CMorphArray 
      {
      int GetSize() { return m_BaseData.GetSize(); }
      BOOL IsEmpty() { return m_BaseData.IsEmpty(); }
      
      // Accessing elements
      const TYPE& GetAt(int nIndex) const;
      TYPE& GetAt(int nIndex);
      void SetAt(int nIndex, ARG_TYPE newElement);
      const TYPE& ElementAt(int nIndex) const;
      TYPE& ElementAt(int nIndex);
      
      // Potentially growing the array
      int Add(ARG_TYPE newElement);
      
      // overloaded operator helpers
      const TYPE& operator[](int nIndex) const;
      TYPE& operator[](int nIndex);
      
         CBigArray<BASE_TYPE, BASE_ARG_TYPE>  m_BaseData;
      private:
         CBigArray<TYPE, ARG_TYPE>    m_RefCache;
         CBigArray<int, int&> m_RefIndex;
         CBigArray<int, int&> m_CacheIndex;
      
         virtual void Convert(BASE_TYPE,ARG_TYPE) = 0;
         virtual void Convert(TYPE,BASE_ARG_TYPE) = 0;
      
         void InitCache();
         TYPE&    GetCachedElement(int nIndex);
      };
      

      主要数据存储在m_BaseData 中,这是其原始格式的数据,如所讨论的那样,其类型可能会有所不同。 m_RefCache 是以预期格式缓存元素的辅助数组,GetCachedElement 函数使用虚拟 Convert 函数在数据移入和移出缓存时转换数据。缓存需要至少与可以在任何时候处于活动状态的同时引用的数量一样大,但在我的情况下,可能会受益于更大的缓存,因为它减少了所需的转换次数。虽然 Alsk 的游标实现可能会运行良好,但给出的解决方案需要更少的对象副本和临时变量,并且应该提供稍微更好的性能,这在这种情况下很重要。

      对于旧版 MFC 的外观和感觉向所有 STL 粉丝致歉;项目的其余部分是 MFC,因此在这种情况下更有意义。 CBigArray 是related stack overflow question 的结果,它成为我处理大型数组的基础。我希望今天完成实现,明天测试。如果这一切都对我不利,我会相应地编辑这篇文章。

      【讨论】:

        猜你喜欢
        • 2014-08-15
        • 2011-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-15
        • 1970-01-01
        相关资源
        最近更新 更多