【问题标题】:C++ gcc extension for non-zero-based array pointer allocation?用于非基于零的数组指针分配的 C++ gcc 扩展?
【发布时间】:2019-03-01 20:40:59
【问题描述】:

我正在寻找支持 gcc 的 C++ 语言扩展来启用非从零开始的数组指针的分配。理想情况下,我可以简单地写:

#include<iostream>  
using namespace std;

// Allocate elements array[lo..hi-1], and return the new array.
template<typename Elem>
Elem* Create_Array(int lo, int hi)
{
  return new Elem[hi-lo] - lo;
  // FIXME what about [expr.add]/4.
  // How do we create a pointer outside the array bounds?
}

// Deallocate an array previously allocated via Create_Array.
template<typename Elem>
void Destroy_Array(Elem* array, int lo, int hi)
{
  delete[](array + lo);
}


int main() 
{  
  const int LO = 1000000000;
  const int HI = LO + 10;
  int* array = Create_Array<int>(LO, HI);
  for (int i=LO; i<HI; i++)
    array[i] = i;
  for (int i=LO; i<HI; i++)
    cout << array[i] << "\n";
  Destroy_Array(array, LO, HI);
} 

上面的代码似乎可以工作,但不是由 C++ 标准定义的。具体来说,问题是[expr.add]/4

当具有整数类型的表达式被添加或减去时 从一个指针,结果具有指针操作数的类型。如果 表达式 P 指向具有 n 的数组对象 x 的元素 x[i] 元素,表达式 P + J 和 J + P(其中 J 的值为 j) 如果 0 ≤ i + j ≤ 则指向(可能是假设的)元素 x[i + j] n; 否则,行为未定义。同样,表达式 P - J 指向(可能是假设的)元素 x[i - j] 如果 0 ≤ i - j ≤ n;否则,行为未定义。

换句话说,上面代码中标记为 FIXME 的行的行为是未定义的,因为它为基于 0 的数组 x 计算了一个超出范围 x[0..n] 的指针。

gcc 是否有一些 --std=... 选项来告诉它允许直接计算非零基数组指针?

如果没有,是否有一种合理的可移植方式来模拟return new Type[hi-lo] - lo; 语句,也许是通过转换为long 并返回? (但我会担心引入更多错误)

此外,是否可以像上面的代码那样只需要 1 个寄存器来跟踪每个数组?例如,如果我有array1[i], array2[i], array3[i],这只需要数组指针array1, array2, array3 的3 个寄存器,加上i 的一个寄存器? (类似地,如果冷取数组引用,我们应该能够直接获取非从零开始的指针,而不需要仅仅为了在寄存器中建立引用而进行计算)

【问题讨论】:

  • 只需创建一个数组/向量类,它在食物下使用 0 索引,但将用户索引范围作为其operator []。对于每一个问题,都有一个抽象可以解决它:)
  • @François Andrieux 好点。我已经删除了语言律师标签。
  • @NathanOlivier 好的,但是operator[] 不会包含像array[offset + i] 这样的表达式,这不是表示每个数组所需的寄存器数量的两倍吗?我在问题中添加了一句话来阐明效率目标。
  • @personal_cloud 我怀疑不是。只是没有用于规避每个不方便的 c++ 规则的扩展。这些规则的存在通常是有原因的,如果绕过它们很容易,那么它们可能一开始就不需要存在。
  • @personal_cloud 这个问题本身已经确定,它违反了语言规则。如果您要问的是为什么 或什么段落禁止这样做,那么这将是一个语言律师问题,但这将是一个独特的问题。这个问题的答案似乎只是“没有这样的扩展/保证”或“是的,使用[编译器标志]”。编辑:在您的最后评论之后,这可能是一个语言律师问题。例如,“指针是否需要有效才能使用运算符[]”。但这是一个不同的问题。

标签: c++ arrays gcc allocation


【解决方案1】:

假设您在 linux x86-64 上使用 gcc,它支持 intptr_tuintptr_t 类型,它们可以保存任何指针值(有效或无效)并且还支持整数运算。 uintptr_t更适合这个应用,因为它支持mod 2^64 semantics,而intptr_t有UB案例。

按照 cmets 的建议,我们可以使用它来构建一个重载 operator[] 并执行范围检查的类:

#include <iostream> 
#include <assert.h>
#include <sstream> // for ostringstream
#include <vector>  // out_of_range
#include <cstdint> // uintptr_t
using namespace std;


// Safe non-zero-based array. Includes bounds checking.
template<typename Elem>
class Array {
  uintptr_t array; // base value for non-zero-based access
  int       lo;    // lowest valid index
  int       hi;    // highest valid index plus 1

public:

  Array(int lo, int hi)
    : array(), lo(lo), hi(hi)
  {
    if (lo > hi)
      {
        ostringstream msg; msg<<"Array(): lo("<<lo<<") > hi("<<hi<< ")";
        throw range_error(msg.str());
      }
    static_assert(sizeof(uintptr_t) == sizeof(void*),
          "Array: uintptr_t size does not match ptr size");
    static_assert(sizeof(ptrdiff_t) == sizeof(uintptr_t),
          "Array: ptrdiff_t size does not match ptr (efficieny issue)");
    Elem* alloc = new Elem[hi-lo];
    assert(alloc); // this is redundant; alloc throws bad_alloc
    array = (uintptr_t)(alloc) - (uintptr_t)(lo * sizeof(Elem));
    // Convert offset to unsigned to avoid overflow UB.
  }


  //////////////////////////////////////////////////////////////////
  // UNCHECKED access utilities (these method names start with "_").

  uintptr_t _get_array(){return array;}
  // Provide direct access to the base pointer (be careful!)

  Elem& _at(ptrdiff_t i)
  {return *(Elem*)(array + (uintptr_t)(i * sizeof(Elem)));}
  // Return reference to element (no bounds checking)
  // On GCC 5.4.0 with -O3, this compiles to an 'lea' instruction

  Elem* _get_alloc(){return &_at(lo);}
  // Return zero-based array that was allocated

  ~Array() {delete[](_get_alloc());}


  //////////////////////////////
  // SAFE access utilities

  Elem& at(ptrdiff_t i)
  {
    if (i < lo || i >= hi)
      {
        ostringstream msg;
        msg << "Array.at(): " << i << " is not in range ["
            << lo << ", " << hi << "]";
        throw out_of_range(msg.str());
      }
    return _at(i);
  }

  int get_lo() const {return lo;}
  int get_hi() const {return hi;}
  int size()   const {return hi - lo;}

  Elem& operator[](ptrdiff_t i){return at(i);}
  // std::vector is wrong; operator[] is the typical use and should be safe.
  // It's good practice to fix mistakes as we go along.

};


// Test
int main() 
{  
  const int LO = 1000000000;
  const int HI = LO + 10;
  Array<int> array(LO, HI);
  for (int i=LO; i<HI; i++)
    array[i] = i;
  for (int i=LO; i<HI; i++)
    cout << array[i] << "\n";
}

请注意,由于GCC 4.7 Arrays and Pointers,仍然无法将intptr_t计算的无效“指针”转换为指针类型:

当从指针转换为整数并再次返回时,生成的指针必须引用与原始指针相同的对象,否则行为未定义。也就是说,不能使用整数运算来避免 C99 和 C11 6.5.6/8 中禁止的指针运算的未定义行为。

这就是为什么array 字段的类型必须是intptr_t 而不是Elem*。换句话说,只要在转换回Elem* 之前将intptr_t 调整为指向原始对象,就可以定义行为。

【讨论】:

  • 根据gcc.gnu.org/onlinedocs/gcc/…intptr_t 不避免UB。 结果指针必须引用与原始指针相同的对象,否则行为未定义。也就是说,人们可能不会使用整数算术来避免 C99 和 C11 6.5.6/8 中所禁止的指针算术的未定义行为。 在实践中它通常会起作用,因为通常没有依赖于的假设它。但这并不意味着它绝对安全或一个好主意。
  • 为什么在operator[] 中使用签名int 作为索引类型?这看起来很奇怪,除非你想允许负索引。
  • @Peter Cordes。是的,我绝对想允许负指数。回复:intptr_t 文档:糟糕,是的,我错过了。请注意,此限制仅在将整数 强制转换 回指针时适用。所以我相信如果我们将中间指针保存在intptr_t 变量中,我们会没事的。只要它在再次变成指针之前指向原来的对象。
  • 您可以使用intptr_t array_intbase 避免在任何对象之外存在指针。所以这个值在添加array_intbase + i*sizeof(Elem)后只作为指针类型存在。你不应该有Elem *array,因为它不是一个真正的指针。但是是的,存储已经偏移的数组基址比 array[i - lo] 更有效,并且应该在适当/可能的时候编译为 x86 / ARM 缩放索引寻址模式。
  • 建议改进:仅在编译时使用 C++11 static_assert 检查类型大小。还提供一个Elem* 函数,该函数撤消偏移并返回指向实际数组的原始指针。如果您使用-DNDEBUG 编译它以使断言成为无操作,则您已经获得了对数组的未经检查的访问权限。 (或者让它像 std::vector 一样:.at() 函数在取消引用之前会进行范围检查,而 operator[] 不会。这样仍然可以通过干净的包装器进行有效访问。)
猜你喜欢
  • 2015-11-08
  • 2014-02-26
  • 1970-01-01
  • 2010-10-31
  • 2014-07-16
  • 2021-10-14
  • 1970-01-01
  • 1970-01-01
  • 2017-06-20
相关资源
最近更新 更多