C++ 子对象生命周期结束会影响外围对象和其他子对象吗?
在新对象的大小、对齐和存储范围均满足要求的前提下,第二次 std::construct_at 会结束 c1 和外围 A 对象的生命周期。没有被覆盖的 d、i、d1、d2 不会仅因 A 的生命周期结束而自动结束生命周期。
根据对象生命周期规则和 std::launder 的前置条件,题目中的两个 std::launder 表达式都可以有效:第一个取得新建的 std::uint16_t 对象,第二个取得仍然存活的 d1。
为什么 c1 和 A 的生命周期都会结束?
[basic.life]/2.5 规定:对象占用的存储被释放,或者被一个不 nested within 该对象的新对象复用时,该对象的生命周期结束。对应条文
对 c1 和 A 分别判断:
- 新建的
std::uint16_t与c1占用的存储重叠,且不是 nested withinc1的对象,因此c1的生命周期结束。 - 这部分存储也属于
A。新对象与原成员类型不同,不能作为原成员的同类型替代对象成为A的子对象;它也没有通过其他关系 nested withinA,因此A的生命周期同样结束。
触发这条规则不需要覆盖 A 的全部字节。决定因素是存储被复用,以及新旧对象之间是否存在标准规定的嵌套关系。子对象替换条件与 nested within 的定义
std::byte 数组为什么仍然存活?
provides storage 是标准定义的特定关系,不能简单理解为“地址落在某个对象内部”。满足条件的 unsigned char 数组或 std::byte 数组可以为新建的完整对象提供存储。
在这个例子中,storage 为最初的 A 提供存储,也为后来构造的 std::uint16_t 提供存储。因此,新对象 nested within storage,它的构造不会结束这个字节数组的生命周期。不过,新对象并不因此 nested within A。provides storage 与嵌套关系
为什么其他成员没有一起结束生命周期?
外围对象生命周期结束,与执行外围对象的析构过程是两回事。
这里通过存储复用结束了 A 的生命周期,没有调用 A 的析构函数。标准允许这样做,也不会因此隐式调用析构函数。跳过析构而结束生命周期的规则
再看 d、i、d1、d2:它们没有被销毁,各自占用的存储也没有被释放或复用。因此,不能仅凭外围 A 的生命周期已经结束,就认定这些成员的生命周期也结束了。
外围对象生命周期结束后,成员访问受到限制,不能再通过旧 A* 执行 a->d1 来访问成员。应当使用事先保存的成员指针,或者从底层字节数组地址取得指向仍存活成员的指针。生命周期结束后的指针使用限制
两个 std::launder 表达式如何判断?
std::launder 要求指定地址处存在符合类型和生命周期条件的对象,并满足存储可达性要求。它不会创建对象,也不会恢复已经结束的生命周期。std::launder 规定
对于:
*std::launder(reinterpret_cast<std::uint16_t*>(
storage + offsetof(A, c1)))
该地址处已经构造了一个存活的 std::uint16_t 对象,新对象的类型正好是 std::uint16_t。不能仅因为原来的 double c1 已经结束生命周期,就认定这个表达式是 UB。
对于:
*std::launder(reinterpret_cast<double*>(
storage + offsetof(A, d1)))
只要新对象没有覆盖 d1 的存储,该地址仍有存活的 double 对象。参数源于 storage 的数组元素指针,其存储可达范围足以覆盖目标对象。存储可达性的定义
如果在原 c1 地址请求取得存活的 double,则不能满足上述生命周期条件。
怎样写出便于核对的示例?
下面的示例保留构造函数返回的指针,并在复用前保存 d1 的指针。std::construct_at 的效果由标准规定为相应的 placement new 构造。construct_at 的规定
#include <cstddef>
#include <cstdint>
#include <memory>
struct A {
double d;
int i;
double c1;
double d1;
double d2;
};
int main() {
// 限定为新对象可以放入 c1,且地址满足其对齐要求的实现。
static_assert(sizeof(std::uint16_t) <= sizeof(double));
static_assert(alignof(double) % alignof(std::uint16_t) == 0);
alignas(A) std::byte storage[sizeof(A)];
A* a = std::construct_at(
reinterpret_cast<A*>(storage),
1., 2, 3., 4., 5.);
double* saved_d1 = &a->d1;
auto* replacement = std::construct_at(
reinterpret_cast<std::uint16_t*>(
storage + offsetof(A, c1)),
std::uint16_t{0});
// 此时原 A 和 c1 的生命周期已经结束。
std::uint16_t n = *replacement; // 0
double x = *saved_d1; // 4.0
auto* recovered_d1 = std::launder(
reinterpret_cast<double*>(storage + offsetof(A, d1)));
double y = *recovered_d1; // 4.0
// 不再通过 a->d1 访问成员。
}
这里没有假定 double 固定占 8 字节,也没有假定任意实现都提供 std::uint16_t。示例以该类型存在、且两个 static_assert 成立为适用条件。
仅显式销毁 c1,结论是否相同?
不同。若在另一次独立执行中,仅对仍存活的成员执行:
std::destroy_at(&a->c1);
这会结束 c1 的生命周期,但没有创建新对象来复用 A 的存储。因此,不能沿用前面的存储复用推理,认定 A 也结束生命周期;其他成员也不会仅因这次操作而结束生命周期。判断时应逐个检查对象的生命周期结束条件,区分销毁、释放存储和复用存储。生命周期结束条件
备注:内容仅供参考。
