PAYATHON 2026

C++ 中 std::launder 能否获取运行时长度数组并进行指针运算?

数据阿明

第一种写法中,只要数组构造成功,n > 0,且存储容量和对齐满足要求,就可以通过 std::launder 取得数组首元素的有效指针,并在该数组范围内进行指针运算。

第二种写法涉及 C++20 的隐式对象创建规则,还需要解决构造循环中如何取得指针的问题。最后才调用 std::launder,无法补救此前已经发生的无效指针运算。

首元素指针可以用于遍历整个数组

表达式:

std::launder(reinterpret_cast<double*>(static_storage))

的返回类型是 double*,指向数组的第一个 double 元素。它不是携带运行时长度的数组指针类型,也不会把首元素变成独立对象。

只要这个元素属于一个包含 n 个元素的数组,就可以进行以下操作:

// 前提:arr 指向该数组的首元素,且 n > 0。
arr + n;       // 可以形成尾后指针,不能解引用
arr[n - 1] = 3.14;

arr + i 允许的范围是 0 <= i <= n;解引用则要求 0 <= i < n。这些权限来自目标对象所属的数组,与指针类型是否记录长度无关。指针运算规则

第一种写法为什么满足 std::launder 的要求

std::launder 不负责创建数组。调用它之前,目标地址必须已经存在符合要求的对象,而且返回指针能到达的存储字节,也必须全部能通过输入指针到达。std::launder 的前置条件

对于第一段代码:

  1. new (location) double[n] 显式创建了数组;当 n > 0 时,起始地址处存在首元素。
  2. static_storage 衰变得到指向字节数组首元素的指针,其可达范围包含直接包围该元素的整个字节数组。
  3. 返回的 double* 指向 double 数组首元素,其可达范围包含直接包围该元素的 double 数组。
  4. 只要 double 数组完整地放在字节数组内,返回指针就没有扩大可达范围。

因此,这次 std::launder 调用在上述前提下成立。可达性规则明确包含指针所指元素的直接外围数组。可达存储的定义

这个结论依赖输入指针的来源。地址相同的指针,其可达范围不一定相同。

更直接的做法是返回 placement new 的结果

数组 new-expression 本身返回指向首元素的指针,通常可以直接保留它,省去后续转换和 std::launder。数组 new-expression 的返回值

下面沿用问题中的 is_aligned_to,由调用方检查容量,并显式保证存储对齐:

#include <cstddef>
#include <memory>
#include <new>
#include <stdexcept>

bool is_aligned_to(void* ptr, std::size_t alignment) noexcept {
    void* p{ptr};
    std::size_t sz{1};
    return std::align(alignment, sz, p, sz) == ptr;
}

double* MakeDouble(void* location, std::size_t n) {
    if (n == 0) {
        return nullptr;
    }
    if (!is_aligned_to(location, alignof(double))) {
        throw std::invalid_argument("bad alignment");
    }
    return ::new (location) double[n]{};
}

int main(int argc, const char**) {
    constexpr std::size_t capacity = 1000;
    alignas(double) std::byte storage[capacity * sizeof(double)];

    const auto n = static_cast<std::size_t>(argc);
    if (n > capacity) {
        throw std::length_error("insufficient storage");
    }

    double* arr = MakeDouble(storage, n);
    for (std::size_t i = 0; i < n; ++i) {
        arr[i] = 3.14;
    }
}

这里使用 double[n]{} 将元素初始化为零;原来的 double[n] 不会把元素初始化为可直接读取的数值。

逐个 construct_at 的版本还差在哪里

对隐式数组的理解有依据:在 C++20 的隐式对象创建规则下,开始 std::byte 数组的生命周期,可以在其存储中隐式创建使后续程序具有定义行为所需的 implicit-lifetime 对象,包括 double 数组及其元素。具体创建哪些对象受程序行为约束,不能仅凭字节容量就断言一定创建了某个指定长度的数组。隐式对象创建规则、P0593R6

不过,数组存在和取得正确指向其元素的指针,是两个需要分别满足的条件。原循环中的表达式:

reinterpret_cast<double*>(location) + i

不能仅凭转换后的静态类型是 double*,就认定它已经指向隐式创建的 double 元素。这里的 location 由字节元素指针转换而来,reinterpret_cast 不会自动完成这种对象指向关系的切换。指针运算还要求实际所指对象与元素类型满足规则。指针转换规则、指针运算的类型要求

在题目这类字节数组提供存储的场景下,如果依赖隐式创建的 double 数组及元素,可以先取得首元素指针,再进行运算:

// 前提:n > 0,容量和对齐足够;字节数组的生命周期开始操作
// 已依据隐式对象创建规则提供所需的 double 数组及元素。
auto* p = std::launder(reinterpret_cast<double*>(location));

for (std::size_t i = 0; i < n; ++i) {
    std::construct_at(p + i, 3.14);
}

这个写法依赖上述存储来源及对象生命周期前提,不能直接推广为适用于任意 void* 的构造函数。对于当前需求,显式构造数组并返回 placement new 的结果更容易说明和维护。

原示例还需要处理的边界

  • std::byte static_storage[N] 本身不保证满足 double 的对齐要求,应使用 alignas(double)。
  • is_aligned_to 只检查对齐,不检查容量。原存储最多容纳 1000 个 double,argc 不能直接当作已验证的长度。
  • n == 0 时没有首个 double 元素,不能按取得首元素的方式调用 std::launder。
  • 使用标准非分配形式的 ::operator new[](std::size_t, void*) 时,现行规则不允许额外数组分配开销;其他自定义 placement 重载需要单独分析。数组分配开销规则
  • 这些对象使用调用方提供的存储,不能对返回指针执行普通 delete[]。数组长度也需要由调用方另行保存。

备注:内容仅供参考。