将运算符new(sizeof(T)* N)返回的内存视为数组

时间:2018-11-23 18:59:18

标签: c++ arrays language-lawyer new-operator alignas

在C语言中,可以使用malloc(sizeof(T)* N)分配动态数组,然后使用指针算法在该动态数组的i偏移处获取元素。

在C ++中,可以使用运算符new()与malloc()相同的方式进行类似的操作,然后放置new(例如,可以在《 Exceptional C ++:47个工程难题,编程问题》一书中看到第13项的解决方案。和解决方案”。如果您没有,则此问题的解决方案摘要为:

T* storage = operator new(sizeof(T)*size);

// insert element    
T* p = storage + i;
new (p) T(element);

// get element
T* element = storage[i];

对我来说,这看起来很合法,因为我要的是一个内存块,要有足够的内存来容纳N个对齐的size = sizeof(T)元素。由于sizeof(T)应该返回对齐的元素的大小,并且将它们一个接一个地放置在一块内存中,因此可以使用指针算法。

然后我却指向诸如http://eel.is/c++draft/expr.add#4http://eel.is/c++draft/intro.object#def:object之类的链接,并声称在C ++运算符中new()不会返回数组对象,因此对返回的对象和使用它进行指针算术运算与ANSI C相反,数组是未定义的行为。

我在这样低级的课程上并不擅长,我确实想通过阅读以下内容来理解:https://www.ibm.com/developerworks/library/pa-dalign/或以下内容:http://jrruethe.github.io/blog/2015/08/23/placement-new/,但我仍然无法理解Sutter是否很简单错误吗?

我确实知道alignas在诸如以下的结构中是有意义的:

alignas(double) char array[sizeof(double)];

(c)http://georgeflanagin.com/alignas.php

如果数组似乎不在double的边界内(也许结构中的char跟随2字节读取处理器运行)。

但这是不同的-我从堆/空闲存储中请求了内存,特别是要求new的运算符返回内存,该内存将保存与sizeof(T)对齐的元素。

总结一下,如果这是TL; DR:

  • 是否可以将malloc()用于C ++中的动态数组?
  • 在没有alignas关键字的旧C ++中,是否可以对动态数组使用运算符new()和new放置?
  • 在运算符new()返回的内存上使用时,插针式算术行为是否未定义?
  • Sutter是否建议某些古董机器可能会破解的代码?

抱歉,这很愚蠢。

4 个答案:

答案 0 :(得分:7)

分配的内存上的指针算术问题,如您的示例:

T* storage = operator new(sizeof(T)*size);
// ...
T* p = storage + i;  // precondition: 0 <= i < size
new (p) T(element);

在技术上不确定的行为已经很久了。这意味着std::vector不能仅以库的形式定义良好的行为来实现,而是需要实现中提供的标准之外的其他保证。

标准委员会绝对无意使std::vector无法实施。萨特当然是正确的,这样的代码应该是打算。该标准的措辞需要反映这一点。

P0593是一项建议,如果被标准接受,则可以解决此问题。同时,可以像上面那样继续编写代码;没有主要的编译器会将其视为UB。

编辑:如评论中所指出,我应该说,当我说storage + i在P0593下会得到很好的定义时,我假设元素{{1} },storage[0],...,storage[1]已被构造。尽管我不确定我对P0593的理解是否足够,可以得出结论,它也不会涵盖那些尚未尚未构建的元素的情况。

答案 1 :(得分:2)

C ++标准包含一个开放的issue,对象的基础表示形式不是unsigned char对象的“数组”而是“序列”。尽管如此,每个人都将其视为一个数组(这是预期的),因此可以安全地编写如下代码:

char* storage = static_cast<char*>(operator new(sizeof(T)*size));
// ...
char* p = storage + sizeof(T)*i;  // precondition: 0 <= i < size
new (p) T(element);

,只要void* operator new(size_t)返回正确对齐的值。使用sizeof乘以偏移量来保持对齐方式是safe

在C ++ 17中,有一个宏STDCPP_DEFAULT_NEW_ALIGNMENT,用于指定“正常” void* operator new(size_t)的最大安全对齐方式;如果需要更大的对齐方式,则应使用void* operator new(std::size_t size, std::align_val_t alignment)

在早期版本的C ++中,没有这种区别,这意味着void* operator new(size_t)的实现方式必须与任何对象的对齐方式兼容。

关于能够直接在T*上进行指针算术,我不确定该标准是否需要 。但是,很难以无法使用的方式实现C ++内存模型。

答案 2 :(得分:0)

您可以使用“老式” malloc来完成此操作,这将为您提供一块内存,可以满足各个平台上最严格的对齐方式(例如long long double的对齐方式)。因此,您将能够将任何对象放入这样的缓冲区中,而不会违反任何对齐要求。

鉴于此,您可以基于这样的内存块对类型的数组使用new放置:

struct MyType {
    MyType() {
        cout << "in constructor of MyType" << endl;
    }
    ~MyType() {
        cout << "in destructor of MyType" << endl;
    }
    int x;
    int y;
};

int main() {

    char* buffer = (char*)malloc(sizeof(MyType)*3);
    MyType *mt = new (buffer)MyType[3];

    for (int i=0; i<3; i++)  {
        mt[i].~MyType();
    }
    free(mt);
}

请注意,与放置new一样,您必须小心地显式调用析构函数,并在不同的步骤中释放内存;您不能使用deletedelete[]函数,它们将这两个步骤结合在一起,从而释放了它们不拥有的内存。

答案 3 :(得分:0)

以下所有适用于最近广泛使用的posix兼容系统,即Windows,Linux(和Android ofc。)和MacOSX

  

是否可以在C ++中将malloc()用于动态数组?

是的。最佳做法是使用reinterpret_cast将结果void*转换为所需的指针类型,它会产生如下所示的动态分配数组:type *array = reinterpret_cast<type*>(malloc(sizeof(type)*array_size); 请注意,在这种情况下,不要在数组元素上调用构造函数,因此无论type是什么,它都仍是未初始化的存储。当free用于释放时,也不会调用析构函数。


  

在没有alignas关键字的旧C ++中,是否可以对动态数组使用运算符new()和new放置?

是的,但是如果您向其放置自定义位置(即,并非来自malloc / new的位置),则在放置新位置时需要注意对齐。普通运算符new和malloc将提供本机字对齐的内存区域(至少在分配大小> = wordize时)。这个事实以及确定结构布局和大小以便正确考虑对齐方式的事实,如果使用malloc或new,则无需担心dyn数组的对齐方式。 可能会注意到,字长有时会比最大的内置数据类型(通常为long double)小很多,但由于对齐方式与数据大小无关,因此必须以相同的方式对齐,但是内存总线上不同访问大小的地址的位宽。


  

在运算符new()返回的内存上使用指针算术是未定义的行为吗?

不,只要您尊重进程的内存边界,从这个角度来看,new的工作方式基本上与malloc相同,而且,new实际上在绝大多数实现都是为了获得所需的区域。 实际上,指针算法永远不会无效。但是,算术表达式的计算结果可能会指向一个指针,该结果可能指向允许区域之外的位置,但这不是指针算术的错误,而是错误表达式的错误。


  

萨特建议代码在某些古董机器上可能会中断吗?

我不这么认为,只要使用正确的编译器。 (请勿将AVR指令或128位宽的内存mov编译为旨在在80386上运行的二进制文件) 当然,在具有不同内存大小和布局的不同机器上,相同的文字地址可能会访问目的/状态/存在性不同的区域,但是为什么要使用文字地址,除非您将驱动程序代码写入特定的硬件?...:)< / p>