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

treating memory returned by operator new(sizeof(T) * N) as an array

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

在 C++ 中,可以使用 operator new() 以与 malloc() 相同的方式进行类似的操作,然后放置新的(例如,您可以在一本书中查看第 13 项的解决方案 "Exceptional C++: 47 engineering puzzles, programming problems, and solutions"赫伯·萨特 (Herb Sutter) 着)。如果您没有,此问题的解决方案摘要将是:

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

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

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

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

然而,我随后被指向如下链接:http://eel.is/c++draft/expr.add#4 or http://eel.is/c++draft/intro.object#def:object 并声称在 C++ 中 operator new() 不是 return 数组对象,因此指针算术它具有的内容 returned 并将其用作数组是未定义的行为,与 ANSI C 不同。

我不太擅长这么低级的东西,我真的想通过阅读这篇文章来理解:https://www.ibm.com/developerworks/library/pa-dalign/ or this: http://jrruethe.github.io/blog/2015/08/23/placement-new/但我仍然不明白萨特是否完全错了?

我明白 alignas 在以下结构中有意义:

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

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

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

但这是不同的 - 我已经从 heap/free 存储请求内存,特别是请求 return 内存的新操作符,它将保存与 sizeof(T).[=29= 对齐的元素]

总结以防万一这是 TL;DR:

对不起,如果这很愚蠢。

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

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

技术上未定义的行为早已为人所知。这意味着 std::vector 不能纯粹作为一个库以定义明确的行为来实现,而是需要来自实现的额外保证,而不是标准中的保证。

标准委员会的意图绝对不是让 std::vector 无法实施。当然,Sutter 是正确的,打算 明确定义此类代码。标准的措辞需要反映这一点。

P0593 是一个建议,如果被接受到标准中,也许能够解决这个问题。同时,像上面这样继续写代码就可以了;没有主要的编译器会将其视为 UB。

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

您可以使用 "old fashioned" malloc 来实现,这会为您提供一块内存,以满足相应平台上最严格的对齐方式(例如 long long double)。因此,您将能够在不违反任何对齐要求的情况下将任何对象放入此类缓冲区。

鉴于此,您可以根据这样的内存块对您类型的数组使用 placement 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);
}

请注意,与 placement new 一样,您必须注意显式调用析构函数并在不同的步骤中释放内存;您不得使用 deletedelete[] 函数,它们结合了这两个步骤,因此会释放它们不拥有的内存。

对于最近广泛使用的所有 posix 兼容系统,即 Windows、Linux(& Android ofc.)和 MacOSX 以下内容申请

Is it possible to use malloc() for dynamic arrays in C++?

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

时,也不会调用析构函数

Is it possible to use operator new() and placement new for dynamic arrays in older C++ which has no alignas keyword?

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


Is pointer arithmetic undefined behavior when used over memory returned by operator new()?

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


Is Sutter advising code which might break on some antique machine?

我不这么认为,前提是使用了正确的编译器。 (不要将 avr 指令或 128 位宽的内存 mov 编译成旨在 运行 在 80386 上的二进制文件) 当然,在具有不同内存大小和布局的不同机器上,相同的文字地址可能会访问不同 purpose/status/existence 的区域,但是除非您将驱动程序代码写入特定硬件,否则为什么要使用文字地址?... :)

C++ 标准包含一个开放的 issue,即对象的底层表示不是 "array",而是 unsigned char 个对象的 "sequence"。尽管如此,每个人都将其视为一个数组(这是有意的),因此编写如下代码是安全的:

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) returns 正确对齐的值。使用 sizeof 乘以偏移量来保持对齐是 safe.

在C++17中,有一个宏STDCPP_DEFAULT_NEW_ALIGNMENT,它指定了"normal"void* operator new(size_t)的最大安全对齐,如果是void* operator new(std::size_t size, std::align_val_t alignment)则应该使用void* operator new(std::size_t size, std::align_val_t alignment)需要更大的对齐方式。

在早期版本的C++中,没有这种区分,也就是说void* operator new(size_t)需要以兼容任何对象对齐方式的方式实现。

至于能够直接在 T* 上进行指针运算,我不确定 是否需要 标准。但是,C++ 内存模型很难实现,以至于无法正常工作。