C++23改动后,动态unsigned char数组作存储的delete合法性及解读
先看基础示例代码:
unsigned char* storage = new unsigned char[sizeof(T)]; T* foo = new(storage) T; // 第2行 // 使用*foo foo->~T(); // 销毁foo delete[] storage; // C++23中是否属于未定义行为?还有其他做法吗?
问题背景
第2行执行后,数组本身仍处于存活状态(它是T的存储载体,T嵌套在数组的存储范围内),但数组的每个unsigned char元素此时已结束生命周期——因为它们的存储被T的对象复用,且T并非嵌套在单个数组元素中,而是占用了整个数组的存储空间。
核心疑问是:使用完毕后,通过指向已结束生命周期的第一个数组元素的指针storage来删除数组,这种操作在C++23中是否合法?
标准条款的变化
C++23之前:[basic.life]/6条款仅禁止以下情况的delete操作:
程序会产生未定义行为,如果:
(6.1) 对象是或将是带有非平凡析构函数的类类型,且该指针被用作delete表达式的操作数
由于unsigned char不是类类型,所以这条规则不适用,delete[] storage是合法的。C++23及之后:该条款被修改为:
程序会产生未定义行为,如果:(6.1) 该指针被用作delete表达式的操作数
这似乎把unsigned char也纳入了禁止范围,那是不是这种用法就不能用了?
三种解读的正确性分析
我们逐一分析你提出的三种解读:
解读1:指针指向T对象表示的unsigned char,仍存活
这种说法不成立。T的对象表示是对其存储内容的解释,并非实际存在的unsigned char对象。当T在数组存储中构造时,原数组的每个unsigned char元素的生命周期就已结束——存储被复用,原对象不复存在,不存在所谓“属于对象表示的新unsigned char”。
解读2:[basic.life]/6不适用于此场景,因为复用已结束
这是正确的解读。[basic.life]/6条款的适用前提是“在对象占用的存储被复用之前”使用指向已死对象的指针。而在这个场景中,我们是在T被析构、存储复用结束后才调用delete[]。此时数组对象本身的生命周期并未结束(它的存储只是被临时用来容纳T,没有被销毁或重新分配),delete[]操作的是数组对象本身,而非已死的数组元素——只要指针storage仍指向数组的起始地址,这个delete操作就是合法的。
解读3:因无法指定对齐,这种用法从一开始就不被允许
这是错误的。对齐问题是独立的另一个问题:new unsigned char[sizeof(T)]只保证unsigned char的对齐要求,不一定满足T的对齐需求,确实可能导致未定义行为,但这和delete阶段的生命周期问题无关。即便用alignas(T)解决对齐问题,原delete合法性的疑问依然存在。
更稳妥的替代写法
为了彻底规避数组元素生命周期带来的歧义,建议直接用operator new/operator delete分配和释放原始存储,同时显式指定对齐要求:
void* storage = operator new(sizeof(T), align_val_t(alignof(T))); T* foo = new(storage) T; // 使用foo foo->~T(); operator delete(storage);
内容的提问来源于stack exchange,提问作者JMC

