PAYATHON 2026

C++ 中如何访问 std::byte 存储内隐式创建的数组?

数据小许

可以使用满足大小和对齐要求的 std::byte 数组实现这类存储。对于原代码,一种兼顾较早标准措辞的写法是先用 std::launder 取得指向整个 T[Capacity] 数组的指针,再通过数组到指针转换得到 T*。之后可以继续使用 std::construct_at 构造元素。

原代码直接对 T* 调用 std::launder 是否满足前置条件,需要区分标准措辞。C++26 已通过 LWG 4130 放宽相关限制,因此不能一概判定空容器中的这次调用违反前置条件。LWG 4130

数组生命周期与元素生命周期有什么区别?

T[Capacity] 是数组类型,属于 implicit-lifetime type。即使 T 本身需要执行非平凡构造函数,数组对象也可以被隐式创建,但这不代表所有元素都已完成构造。对象模型规则、类型分类规则

例如,对于需要显式构造的 T,可以存在这样的状态:

  • T[Capacity] 数组对象的生命周期已经开始。
  • 前 size_ 个元素已经构造,可以访问其值。
  • 剩余位置留给后续元素,尚不能读取其值或调用其成员函数。

数组对象为 T* 指针运算提供基础,各元素的生命周期则分别通过 std::construct_at 和 std::destroy_at 管理。单独在相邻地址构造若干对象,并不足以证明它们可以作为同一个数组进行指针运算。指针加减规则

reinterpret_cast 为什么不能直接解决访问问题?

下面的转换本身不会创建 T 对象:

reinterpret_cast<T*>(storage_)

“指针表示同一个地址”和“指针指向目标对象”是两回事。字节元素与存储中的 T 对象不会仅因地址相同就成为 pointer-interconvertible,因此不能只凭转换后的类型是 T*,就认定它是可用于数组访问的元素指针。指针与对象关系

这里使用 std::launder 是为了从表示存储地址的指针取得指向目标对象的指针。它不创建对象,也不调用构造函数。

如何修改 data()?

以下修改适用于运行期使用,并假设 Capacity > 0、T 是完整的非 cv 限定对象类型,存储大小的计算有效。

在原类中加入数组类型别名,并替换 data():

using array_type = T[Capacity];

T* data() noexcept {
    auto* array = std::launder(
        reinterpret_cast<array_type*>(storage_)
    );
    return *array;
}

这里的 std::launder 以整个数组为目标。根据隐式创建规则,storage_ 生命周期的开始可以隐式创建所需的 T[Capacity] 数组。数组本身处于生命周期内,不要求每个元素都已构造。return *array; 随后执行数组到指针转换,得到首元素位置的指针。C++20 对象模型措辞、C++20 std::launder 前置条件

std::launder 还要求结果指针可达的存储不能超出输入指针可达的存储。在此布局中,输入指针来自 storage_ 的首字节元素,可达整个字节数组,足以覆盖目标数组。

使用这样的 data() 后,原有的插入逻辑可以保留:

void push_back(T const& t) {
    if (size_ == Capacity) {
        throw std::bad_alloc();
    }

    std::construct_at(data() + size_, t);
    ++size_;
}

构造成功后才会增加 size_。如果复制构造函数抛出异常,容器记录的元素数量不会增加。

空容器中的 std::launder 是否一定有问题?

按照较早的 [ptr.launder] 措辞,目标对象必须处于生命周期内。如果 T 元素的生命周期尚未开始,直接调用原代码中的 std::launder<T> 确实存在前置条件问题。不能假定任意 T 都会随数组一起被隐式创建。

C++26 的 LWG 4130 增加了一种允许情况:目标可以是生命周期已经开始的数组中的元素子对象,即使该元素自身的生命周期尚未开始。因此,在所需数组已被隐式创建且满足可达性要求时,原来的 std::launder(reinterpret_cast<T*>(storage_)) 可以符合修订后的规则。LWG 4130 的修订内容

前面使用整个数组指针的写法不依赖这一放宽条件。

是否需要改用 placement-new?

不需要。std::construct_at 用于在指定位置构造对象;传入位置不要求预先存在一个已经构造完成、可读取的 T 对象。对于这里的非数组元素类型,其效果通过 placement-new 定义。std::construct_at 规则

如果直接使用 placement-new,也可以先在字节数组内计算槽位地址:

T* p = ::new (
    static_cast<void*>(storage_ + sizeof(T) * size_)
) T(t);

这段代码要求 size_ < Capacity。返回的 p 指向刚构造的对象,可以直接使用,无须再对它调用 std::launder。不过,仅替换构造语句,并不能自动解决 data() 和跨元素指针运算的依据问题。

std::launder 与迭代器失效有什么关系?

迭代器失效是容器接口的使用约定;std::launder 处理的是语言对象模型中的指针指向关系。即使容器约定某个旧迭代器已经失效,实现内部仍然需要正确取得新对象的指针。

在原位置重建对象时,也不一定每次都要调用 std::launder。满足 transparently replaceable 条件时,旧指针会自动指向替代对象;不满足这些条件时,才需要考虑通过构造操作的返回值或 std::launder 获取新指针。对象替换规则

实现还需要注意什么?

  • operator[] 只能访问已经构造的元素,调用者必须满足 n < size_。
  • 析构函数需要销毁剩余元素;字节数组成员不会自动调用其中 T 对象的析构函数。
  • 对任意 T,不能依赖默认复制、移动操作来正确复制这些元素。应逐元素实现,或先禁用相关操作。
  • Capacity == 0 需要单独处理,标准 C++ 不支持这里所需的零长度内建数组。
  • 检查 sizeof(T) * Capacity 的大小计算;上述写法也不能直接保证 constexpr 可用。

备注:内容仅供参考。