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

Posted

技术标签:

【中文标题】将 operator new(sizeof(T) * N) 返回的内存视为数组【英文标题】:treating memory returned by operator new(sizeof(T) * N) as an array 【发布时间】:2019-04-26 08:47:15 【问题描述】:

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

在 C++ 中,可以使用 operator new() 以与 malloc() 相同的方式执行类似操作,然后放置新的(例如,可以在“异常 C++:47 个工程难题、编程问题、和解决方案”由 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];

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

然后我被指向如下链接:http://eel.is/c++draft/expr.add#4 或 http://eel.is/c++draft/intro.object#def:object 并声称在 C++ 中 operator 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 字节读取处理器上)。

但这是不同的——我已经从堆/空闲存储中请求了内存,特别是请求 operator new 返回内存,该内存将保持与sizeof(T)对齐的元素。

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

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

对不起,如果这是愚蠢的。

【问题讨论】:

是否可以在 C 中将 malloc() 用于动态数组? - 你想写 C++? 你能把整个代码放在这里吗?因为您提供的代码甚至无法编译(在第一行,至少有一个丢失的演员表)。 @Galik 它在eel.is/c++draft/expr.add#footnote-85 中说。我不认为指针被认为指向单个对象。在分配的内存中没有构造对象。 是的,如我所见,该代码确实有 UB。但是,在我看来,需要修复的是标准,而不是 Herb 的代码。有趣的是,为什么我们对指针算法有这样一个限制规则。 @Galik 我不太清楚,但在我的业余阅读中,possibly-hypothetical 指的是实际数组之后的hypothetical x[n],而 4.2 的排位赛if 也没有使用它。 【参考方案1】:

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

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。

编辑: 正如 cmets 中所指出的,我应该指出,当我说 storage + i 将在 P0593 下得到良好定义时,我假设元素 storage[0]、@987654327 @, ..., storage[i-1] 已经构建好了。虽然我不确定我对 P0593 的理解是否足够好,无法得出结论,它也不会涵盖那些元素尚未已经被构造的情况。

【讨论】:

嗯,为什么 P0593 在这里相关? T 可以是任何类型。我认为该提案不会解决这个问题。 @PeteBecker 这就是我的意思。 std::vector 并不意味着无法在用户代码中实现。 但是UB的原因是什么?这不是指针算术,而是这个 std::bless 是什么?安置新?你能把它归结为基本块吗? @geza 它说 any 元素类型的数组类型是隐式生命周期类型(无论元素类型是否是隐式生命周期类型)。因为如果你已经有一堆T 类型的对象在内存中排列,那么创建T 的数组不需要额外的代码。 谢谢,这确实有道理!现在是第 35 次重新阅读该提案的时候了:)【参考方案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-multiplied 偏移量来保持对齐是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++ 内存模型。

【讨论】:

CWG 1701 与问题中的问题没有任何关系。 CWG 1701 是关于对象表示的。分配函数的问题是它们不创建对象。问题的解决应该如何帮助? 每个人都把它当成一个数组(这是有意的) 这个“这是有意的”从何而来?该问题的注意事项:关于将对象的组成字节称为“对象”本身是否合适,已经提出了一个额外的关注点。当它的元素不应该是对象时,它如何成为一个数组? @LanguageLawyer,分配函数不创建对象是的。见the standard。由该语言的作者所设计,这取决于他们(以及其他所有人)如何使用此类结构;如有疑问,您可以直接问他们,他们的电子邮件不是秘密。 @Kit。 参见标准。 它并没有说对象被创建,它说它的生命周期已经开始。 See the standard, when object is created. @LanguageLawyer,那么“创建对象时”就是稻草人。从语义上讲,分配函数“创建”或“引用”字节数组没有区别。【参考方案3】:

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

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

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


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

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


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

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


Sutter 是否建议在某些古董机器上可能会破坏的代码?

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

【讨论】:

但是为什么在@Brian 回答链接中与自制向量的 Sutters 完全相同的示例(除了实现细节)它指出:“实际上,此代码适用于一个范围现有实现,但根据 C++ 对象模型,未定义的行为发生在点 #a、#b、#c、#d 和 #e,因为它们试图在不包含的已分配存储区域上执行指针算术一个数组对象。”? (c)open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0593r2.html widely used posix-based systems 到底是什么意思?您的答案是基于某种标准化或实施的保证吗? @eukaryota 没有一个,但纯粹的经验。至少适用于最新的 Windows、Linux(和 Android ofc)和 Mac OSX 版本。 @xor256 这根本不是真的...每次分配最终都会导致malloc 调用,而 malloc 实现由平台的 C 库提供,必须解决这些问题本身,否则您将无法在 C 中使用结构数组... 天哪,人们……你没见过任何汇编代码吗?指针是简单的无符号整数,在算术中使用它们的恐惧是疯狂的......【参考方案4】:

您可以使用“老式”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);

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

【讨论】:

但是我可以在你的例子中使用 operator new() 代替 malloc() 吗? 据我了解,新数组放置可能需要未指定的内存开销。因此,此代码是否已定义行为取决于实现。见***.com/questions/8720425/… @eukaryota 请注意,Sutter 和 open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0593r2.html 示例都没有使用新的数组放置。他们只对他们在内存块中创建的每个元素使用新的位置。 @xor256 是的,我特指此答案中的代码示例。非数组placement-new不允许有这个开销。

以上是关于将 operator new(sizeof(T) * N) 返回的内存视为数组的主要内容,如果未能解决你的问题,请参考以下文章

operator new & operator delete

operator new & operator delete

new和delete

在从放置 new 获得的指针上使用 operator delete 的合法性

这是啥语法 - new (this) T(); [复制]

new-Operator 大幅增加了 Arduino 草图的大小 - 为啥?