可以用 realloc 安全地重新分配普通可复制对象的存储吗?

Posted

技术标签:

【中文标题】可以用 realloc 安全地重新分配普通可复制对象的存储吗?【英文标题】:Can the storage of trivially copyable objects be safely reallocated with realloc? 【发布时间】:2018-04-01 00:18:46 【问题描述】:

我知道trivially copyable 对象可以安全地复制到我的malloc 到适当的存储位置1,并且目标对象将具有与源相同的值。

realloc 也可以这样做吗?也就是说,如果realloc某个存储包含一些T类型的对象,而realloc决定移动和复制该块,那么新分配的存储中的对象是否完好无损并且已经开始了它们的生命周期,并且生命周期将旧存储中的对象是否安全结束?


1 在问这个问题时,我假设“适当的存储位置”包括适当对齐和大小的未初始化存储,但正如下面的 M.M's answer 所说,这实际上并没有得到很好的支持标准。这会使realloc 产生问题,因为它总是复制到未初始化的存储中。

【问题讨论】:

与其问这个问题,不如花时间确保你的 C++ 代码在首先 - 只是避免不得不问这个问题。 C++ 在所有情况下都有更好的选择——使用它们。 我不确定你所说的“你的 C++ 代码”是什么意思,或者为什么你会假设“我的”代码使用这些方法中的任何一种。如果我们都能幸运地只处理“我们的”代码... “目标对象将具有与源对象相同的值” 这不是标准所说的。 "[basic.types]/2 对于任何可简单复制类型T 的对象...,构成该对象的底层字节(1.7) 可以复制到char 数组中“强调我的。 realloc 的情况并非如此 一些可以简单复制的东西并不能保证这一点。如果写得不好,它可能在技术上满足要求,同时实际拥有指针,或拥有非拥有的内部指针,或者让它如此简单地复制它是一个坏主意,即使语言没有意识到这一点。 @IgorTandetnik - 对,但通常理解为提供复制保证的是 [basic.types]/3 而不是 /2。除非你的主张是memcpy 不能复制普通可复制类型不能?众所周知,他们可以并且该功能被大量使用。 【参考方案1】:

3.8 对象生命周期明确:

类型 T 的对象的生命周期开始于:

1.1 获得类型 T 具有适当对齐和大小的存储,并且

1.2 如果对象有非空初始化,则其初始化完成。

同样的生命周期结束。您可以忽略 std 其他部分的垃圾,例如 [intro]!

【讨论】:

当没有对象时,它的生命周期何时开始的问题没有意义。 所以要么生命周期实际上从创建的那一刻开始,而“已获得存储”部分只是一个红鲱鱼或非常混乱的措辞,或者对象的“生命周期”实际上向后延伸及时,在对象创建之前,因此获得了最终创建对象的存储的时刻。后者看起来很疯狂,但前者并没有解释为什么1.1 存在。现在对于非空构造的对象,初始化将在创建结束时完成,对吧?除非你不用初始化就可以创建对象? @BeeOnRope:考虑new(ptr) SomeType。这涉及两个不同的操作:调用::operator new(size_t, void*),并在该存储中初始化SomeType 对象。那么,在该过程中,SomeType 对象的生命周期何时开始?好吧,如果SomeType 有空初始化,那么它会在::operator new 调用返回时开始。如果一定要调用默认构造函数,那就是默认构造函数返回的时候。 @NicolBolas - 当然,“新表达式”可能涉及这两个组件,但[intro.objects] 只谈论新表达式本身,这就是全部。所以对象是由 new 表达式创建的,它也被 new 表达式初始化。这足以简单地说生命周期从创建点开始,至少对于[info.objects] @NicolBolas - 当我说它总是涉及初始化时,我的意思是对于具有非空初始化的对象。 “为什么必须这样?” - 因为关于现有条件似乎毫无意义:在调用 new 之前甚至不能存在,并且 显然 在那时(或作为新表达式本身的一部分)已经获得了合适的存储.【参考方案2】:

realloc 不能用于安全地移动对象,即使是可简单复制的类型,因为realloc 无法在未初始化的存储中创建新对象。

特别是根据C++14 [basic.life]/1:

类型 T 的对象的生命周期结束于:

如果 T 是具有非平凡析构函数 (12.4) 的类类型,则析构函数调用开始,或者

对象占用的存储空间被重用或释放。

调用realloc 释放或重用存储(即使没有发生重新分配,我认为,尽管这对您的问题没有实际意义)。这样对象的生命周期就结束了。

[intro.objects]/1 涵盖了创建对象的情况:

对象由定义(3.1)、新表达式(5.3.4)创建 或在需要时通过实施 (12.2)。

这不包括realloc;所以 realloc 调用结束了旧对象的生命周期并且不会创建新对象。

这不仅意味着realloc 不适合复制一般可复制的对象,还意味着使用mallocoperator new(size_t) 来获取未初始化的存储,然后将memcpy 从现有对象复制到该存储不会创建对象的可用副本,因为在这种情况下,目标对象也没有创建


另请参阅:reinterpret_cast creating a trivially-default-constructible object 或 constructing a trivially copyable object with memcpy,进一步讨论将字节复制到新位置不会在该位置创建对象这一事实。

【讨论】:

以上是关于可以用 realloc 安全地重新分配普通可复制对象的存储吗?的主要内容,如果未能解决你的问题,请参考以下文章

使用 realloc 是不是安全?

是否可以在不重新分配的情况下实现动态数组?

如果分配器提供 realloc 语义,std::vector 可以避免复制吗?

在 C++ 中使用 realloc

释放分配的内存:realloc() 与 free()

重新分配新分配的内存是不是安全?