逐个元素创建的数组的生命周期
这只是一个纯理论性的语言法理问题,因为它所描述的情景没有实际用途(这会是一个真的糟糕的设计)。
场景是在一个非字节类型的数组中创建存储,然后把与之无关的类型的对象连续放入其中:
#include <cstddef>
#include <memory>
#include <new>
// unrelated types
using store_t = double;
using data_t = short;
// some data_t object generator
template <typename T>
data_t make_data(T val) {
return data_t{static_cast<data_t>(val)};
}
int main() {
// creating a storage from store_t (not providing storage, not implicitly
// creating objects)
constexpr std::size_t N = 100;
auto* Storage = new store_t[N];
// retrieving a suitably aligned pointer in storage to hold a data_t object
std::size_t sz = sizeof(store_t) * N;
void* ptr = static_cast<void*>(Storage);
if (nullptr == std::align(alignof(data_t), sizeof(store_t) * N, ptr, sz)) {
throw std::bad_alloc();
}
// inplace construction of contiguous data_t object inside the original
// storage
// ends the array of store_t lifetime
constexpr std::size_t n = 10;
for (std::size_t i = 0; i < n; ++i) {
std::construct_at(reinterpret_cast<data_t*>(ptr) + i, make_data(i));
}
// is there a data_t array in its lifetime at ptr?
...
}
根据 https://eel.is/c++draft/dcl.array#6:
类型为 “N个 U的数组” 的对象由一个连续分配的非空的N 个U 型子对象集合组成,这些子对象被称为数组元素,编号从0 到N-1。编号为0 的元素被称为数组的第一个元素。
因此我理论上构造了一个具有 数组对象 布局的东西,可以与指针运算一起使用(例如,https://eel.is/c++draft/basic.compound#3):
data_t const* const array = std::launder(
reinterpret_cast<data_t*>(ptr)); // is this a living array?
for (std::size_t i = 0; i < n; ++i) {
std::cout << array[i]; // is this legal?
}
但据我所知,这个数组既没有显式也没有隐式地开始它的生存期。
operator new[] 隐式创建对象,而数组具有隐式生存期类型,但我不确定我们能否因为两个原因而依赖隐式对象创建。
首先,https://eel.is/c++draft/intro.object#14 与 https://eel.is/c++draft/intro.object#16。如果我对这些小节的理解正确,隐式创建对象的地址应当是 operator new[] 返回的那个地址,但由于对齐调整,尤其是对于非新对齐类型,情况可能并非如此。
此外,data_t 数组并未嵌套在 store_t 数组之内,因为后者不提供存储。因此我们将会有两个互不相关的对象共享同一个位置,这属于未定义行为(严格别名规则违规:https://eel.is/c++draft/basic.lval#11)。
那么,data_t 的数组在其生存期内吗,为什么?
这是完整示例:
#include <cstddef>
#include <iostream>
#include <memory>
#include <new>
// unrelated types
using store_t = double;
using data_t = short;
// some data_t object generator
template <typename T>
data_t make_data(T val) {
return data_t{static_cast<data_t>(val)};
}
int main() {
// creating a storage from store_t (not providing storage, not implicitly
// creating objects)
constexpr std::size_t N = 100;
auto* Storage = new store_t[N];
// retrieving a suitably aligned pointer in storage to hold a data_t object
std::size_t sz = sizeof(store_t) * N;
void* ptr = static_cast<void*>(Storage);
if (nullptr == std::align(alignof(data_t), sizeof(store_t) * N, ptr, sz)) {
throw std::bad_alloc();
}
// inplace construction of contiguous data_t object inside the original
// storage
// ends the array of store_t lifetime
constexpr std::size_t n = 10;
for (std::size_t i = 0; i < n; ++i) {
std::construct_at(reinterpret_cast<data_t*>(ptr) + i, make_data(i));
}
// is there a data_t array in its lifetime at ptr?
data_t const* const array = std::launder(
reinterpret_cast<data_t*>(ptr)); // is this a living array?
for (std::size_t i = 0; i < n; ++i) {
std::cout << array[i]; // is this legal?
}
// cleanly releasing resources
for (std::size_t i = 0; i < n; ++i) {
std::destroy_at(std::launder(reinterpret_cast<data_t*>(ptr) + i));
}
// releasing storage
operator delete[](Storage);
}
解决方案
用来为 Storage 指向的存储分配的 operator new[] 会隐式创建对象,但由于随后 new 运算符在那里创建了一个 store_t 的数组,因此没有这样的对象能存活到对 reinterpret_cast<data_t*>(ptr) 的指针运算。没有数组对象,这种运算将是未定义的。你可以尝试对数组底层的字节进行指针运算,但标准在这一点上仍不明确(尽管修订正在进行中,in progress),并且得到的对象也不会成为一个数组的成员(除非显式创建一个数组),即使它们具有相邻的地址。