c++ - 通过放置 new 重用数据成员存储
问题描述
是否允许重用非静态数据成员的存储,如果允许,在什么条件下?
考虑程序
#include<new>
#include<type_traits>
using T = /*some type*/;
using U = /*some type*/;
static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));
struct A {
T t /*initializer*/;
U* u;
A() {
t.~T();
u = ::new(static_cast<void*>(&t)) U /*initializer*/;
}
~A() {
u->~U();
::new(static_cast<void*>(&t)) T /*initializer*/;
}
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};
int main() {
auto a = new A;
*(a->u) = /*some assignment*/;
delete a; /*optional*/
A b; /*alternative*/
*(b.u) = /*some assignment*/; /*alternative*/
}
除了s 之外,对象类型T
和需要满足哪些条件才能使程序具有已定义的行为(如果有)?U
static_assert
它是否取决于A
实际被调用的析构函数(例如是否存在/*optional*/
or/*alternative*/
行)?
它是否取决于 的存储持续时间A
,例如是否使用/*alternative*/
行 inmain
代替?
注意程序不使用t
placement-new之后的成员,除了在析构函数中。当然,当它的存储空间被不同的类型占用时使用它是不允许的。
另请注意,我不鼓励任何人编写这样的代码。我的目的是更好地理解语言的细节。特别是,至少只要不调用析构函数,我就没有发现任何禁止此类放置新闻的内容。
另请参阅我关于在封闭对象的构造/销毁期间不执行放置新闻的修改版本的另一个问题,因为根据一些评论,这似乎导致了并发症。
评论中要求的具体示例展示了我认为代表不同兴趣案例的类型子集的更广泛问题:
#include<new>
#include<type_traits>
struct non_trivial {
~non_trivial() {};
};
template<typename T, bool>
struct S {
T t{};
S& operator=(const S&) { return *this; }
};
template<bool B>
using Q = S<int, B>; // alternatively S<const int, B> or S<non_trivial, B>
using T = Q<true>;
using U = Q<false>;
static_assert(std::is_object_v<T>);
static_assert(std::is_object_v<U>);
static_assert(sizeof(U) <= sizeof(T));
static_assert(alignof(U) <= alignof(T));
struct A {
T t;
U* u;
A() {
t.~T();
u = ::new(static_cast<void*>(&t)) U;
}
~A() {
u->~U();
::new(static_cast<void*>(&t)) T;
}
A(const A&) = delete;
A(A&&) = delete;
A& operator=(const A&) = delete;
A& operator=(A&&) = delete;
};
int main() {
auto a = new A;
*(a->u) = {};
delete a; /*optional*/
// A b; /*alternative*/
// *(b.u) = {}; /*alternative*/
}
解决方案
看起来没问题,有一些问题取决于 or 的内容T
,U
或者 if T::T
throws。
如果在另一个对象占用的地址处创建新对象,则原始对象的所有指针、引用和名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以使用操作新对象,但前提是满足以下条件:
- 新对象的存储恰好覆盖了原始对象占用的存储位置
- 新对象与原始对象的类型相同(忽略顶级 cv 限定符)
- 原始对象的类型不是 const 限定的
- 如果原始对象具有类类型,则它不包含任何类型为 const 限定或引用类型的非静态数据成员
- 原始对象是 T 类型的最派生对象,而新对象是 T 类型的最派生对象(也就是说,它们不是基类子对象)。
并且您必须保证创建新对象,包括在异常中。
直接来自标准:
[基本生活] 6.8/8:
(8) 如果在对象的生命周期结束之后,在对象占用的存储空间被重用或释放之前,在原对象占用的存储位置创建一个新对象,一个指向原对象的指针,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,可用于操作新对象,如果:
- (8.1) 新对象的存储恰好覆盖了原始对象占用的存储位置,并且
- (8.2) 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且
- (8.3) 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何类型为 const 限定或引用类型的非静态数据成员,并且
- (8.4) 原始对象是类型 T 的最派生对象,而新对象是类型 T 的最派生对象(也就是说,它们不是基类子对象)。
T
基本上适用于U
。
至于使用T
不同的U
回填空间来重用空间:
[基本生活] 6.8/9:
(9) 如果程序以静态、线程或自动存储持续时间结束 T 类型对象的生命周期,并且如果 T 具有非平凡的析构函数,则程序必须确保原始类型的对象占用相同的存储位置当隐式析构函数调用发生时;否则程序的行为是不确定的。即使块因异常而退出也是如此。
并且T
(nor U
) 不能包含任何 non-static const
。
[基本生活] 6.8/10:
(10) 在具有静态、线程或自动存储持续时间的 const 完整对象占用的存储空间内,或在此类 const 对象在其生命周期结束之前曾经占用的存储空间内创建新对象,会导致未定义的行为。
推荐阅读
- snowflake-cloud-data-platform - 在雪花中重放未完成的雪管通知/消息
- beeline - 如何从直线中提取为 CSV 但带有文本限定符?
- python - Python .append() 方法行为怪异
- javascript - 如何使用 Discord API (rpc.activities.write) 设置用户的活动
- python - 寻找不存在的virtualenv的诗歌
- amazon-web-services - 如何只允许一个服务角色将对象放入 Amazon S3 存储桶
- javascript - 启动赛普拉斯时传递自定义 Chrome 配置文件目录
- snowflake-cloud-data-platform - 雪花撤销表/模式访问
- android - 在 Android 12 上的多活动应用程序中重复自定义 SplashScreen
- snowflake-sql - 雪花中的日期转换