在初始化的内存上使用放置新位置是否合法
Is it legal to use placement new on initialised memory?
我正在探索在C++中实现真正的(部分)不可变数据结构的可能性。由于C++似乎没有区分变量和变量存储的对象,因此真正替换对象(无需赋值操作!)的唯一方法是使用放置 new:
auto var = Immutable(state0);
// the following is illegal as it requires assignment to
// an immutable object
var = Immutable(state1);
// however, the following would work as it constructs a new object
// in place of the old one
new (&var) Immutable(state1);
假设没有非平凡的析构函数要运行,这在C++中是合法的,还是我应该期待未定义的行为?如果它依赖于标准,哪个是我可以期望它工作的最小/最大标准版本?
附录:由于人们似乎在 2019 年仍然阅读本文,因此快速说明一下 - 这种模式实际上在现代(17 后)C++中使用std::launder()
在法律上是可能的。
你写的东西在技术上是合法的,但几乎可以肯定是无用的。
假设
struct Immutable {
const int x;
Immutable(int val):x(val) {}
};
对于我们非常简单的不可变类型。
auto var = Immutable(0);
::new (&var) Immutable(1);
这是完全合法的。
而且没用,因为您不能使用var
来引用放置后存储在其中的Immutable(1)
的状态new
. 任何此类访问都是未定义的行为。
您可以这样做:
auto var = Immutable(0);
auto* pvar1 = ::new (&var) Immutable(1);
获得*pvar1
是合法的。 您甚至可以执行以下操作:
auto var = Immutable(0);
auto& var1 = *(::new (&var) Immutable(1));
但在任何情况下,你都不能在你放置新的之后提到var
。
C++中的实际const
数据是对编译器的承诺,即您永远不会更改该值。 这是与对 const 的引用或对 const 的指针进行比较,这只是建议您不会修改数据的建议。
被宣布为const
的结构的成员"实际上是恒常的"。 编译器会假设它们从未被修改过,并且不会费心去证明这一点。
您在旧实例实际上所在的位置创建新实例违反了此假设。
您可以这样做,但不能使用旧名称或指针来引用它。 C++让你搬起石头砸自己的脚。 去吧,我们敢。
这就是为什么这种技术是合法的,但几乎完全无用。 一个具有静态单一赋值的优秀优化器已经知道您将在此时停止使用var
,并创建
auto var1 = Immutable(1);
它很可能重用存储。
在另一个变量之上放置新位置通常是定义的行为。 这通常是一个坏主意,而且很脆弱。
这样做会结束旧对象的生存期,而无需调用析构函数。 如果某些特定的假设成立,则引用和指针以及旧对象的名称会引用新对象(完全相同的类型,没有常量问题)。
修改声明为 const 的数据或包含const
字段的类会导致在引脚掉落时出现未定义的行为。 这包括结束声明为 const 的自动存储字段的生存期,并在该位置创建新对象。 旧名称、指针和引用使用起来不安全。
[Basic.life 3.8]/8:
如果在对象的生存期结束后,在对象占用的存储被重复使用之前,或者 释放后,在原始对象占用的存储位置创建一个新对象,一个指针 指向原始对象、引用原始对象的引用或原始对象的名称 对象将自动引用新对象,并且一旦新对象的生存期开始,就可以 用于操作新对象,如果:
(8.1) 新对象的存储恰好覆盖原始对象占用的存储位置, 和
(8.2) 新对象与原始对象的类型相同(忽略顶级 CV 限定符),并且
(8.3) 原始对象的类型不是常量限定的,如果是类类型,则不包含任何非静态 类型为常量限定或引用类型的数据成员,以及
(8.4) 原始对象是派生最多的对象 (1.8) 类型 T 而新对象是最派生的 类型的对象 T (也就是说,它们不是基类子对象)。
简而言之,如果您的不可变性是通过const
成员编码的,则使用旧名称或指向旧内容的指针是未定义的行为。
您可以使用放置 new 的返回值来引用新对象,而不能使用其他任何内容。
异常可能性使得极难防止执行未定义行为或必须立即退出的代码。
如果需要引用语义,请使用指向 const 对象的智能指针或可选的 const 对象。 两者都处理对象生存期。 第一个需要堆分配,但允许移动(可能还有共享引用),第二个允许自动存储。 两者都将手动对象生存期管理移出业务逻辑。 现在,两者都可以为空,但无论如何,手动避免这种情况是很困难的。
还要考虑写入指针上的复制,这些指针允许逻辑上包含突变的数据,以提高效率。
来自 C++ 标准草案 N4296:
3.8 对象生存
期[...]
类型 T 对象的生存期在以下时间结束:
(1.3) — 如果 T 是一个类 使用非平凡析构函数 (12.4) 的类型,析构函数调用开始, or
(1.4) — 对象占用的存储被重用,或者 释放。
[...]
4 程序可以通过以下方式结束任何对象的生存期 重用对象占用的存储或通过显式调用 具有非平凡的类类型的对象的析构函数 破坏者。对于具有非平凡的类类型的对象 析构函数,程序不需要调用析构函数 在对象占用的存储被重用之前显式或 释放;但是,如果没有显式调用析构函数或 如果未使用删除表达式 (5.3.5) 释放存储,则 不得隐式调用析构函数,并且任何依赖于 析构函数产生的副作用具有未定义的行为。
所以,是的,你可以通过重用对象的内存来结束对象的生存期,即使是具有非平凡析构函数的内存,只要你不依赖于析构函数调用的副作用。
这适用于具有对象的非常量实例,例如struct ImmutableBounds { const void* start; const void* end; }
您实际上已经问了 3 个不同的问题:)
1. 不变性契约
只是 - 一个契约,而不是一个语言结构。
例如,在Java中,String类的实例是不可变的。但这意味着类的所有方法都旨在返回类的新实例,而不是修改实例。
因此,如果你想将Java的字符串变成一个可变的对象,你不能,而不访问它的源代码。
这同样适用于用C++或任何其他语言编写的课程。您可以选择创建包装器(或使用代理模式),但仅此而已。
2. 使用放置构造函数并分配到初始化的内存中。
这实际上就是他们最初被创造的目的。 放置构造函数最常见的用例是内存池- 预先分配一个大型内存缓冲区,然后将内容分配到其中。
所以是的 - 这是合法的,没有人不会介意。
3. 使用放置分配器覆盖类实例的内容。
别这样。
有一个特殊的构造来处理这种类型的操作,它称为复制构造函数。
- GL_SHADERSTORAGE_BUFFER位置是否与其他着色器位置冲突
- 内存中类位置的成员是否取决于类成员在类定义中的位置?
- 标记为 std::memory_order_seq_cst 的单个原子操作是否会在所有位置触发顺序一致性?
- 是否有任何优雅的方式来遍历元素位置可以更改的列表?
- 是否可以将值分配给移出位置?
- 如果着色器中未使用绑定属性位置,是否会对其进行惩罚
- C++每个变量的存储位置是否正确?
- 在初始化的内存上使用放置新位置是否合法
- 在C++中声明使用的位置是否有良好做法
- C++的最后一个位置是否有计算单元的标准功能
- libvlc 检查媒体位置是否有效
- 更改描述函数行为的位置是否会导致错误?
- 重用内存位置是否安全
- LPCSTR-如何检查第一个位置是否为空格" "
- 检查阵列位置是否为空/空
- 星号的位置是否会影响指针
- 检查数组的X位置是否为char
- 将相同的值写入相同的内存位置是否会导致数据争用?
- 确定一个位置是否是特定列表的成员
- 在"this"指针上使用放置新位置是否安全