与此处std::aligned_storage
页面的示例类似,假设我有以下内容:
template <typename T, std::size_t N>
class SlotArray {
static_assert(!std::is_array_v<T>);
static_assert(!std::is_void_v<T>);
using StorageType = std::aligned_storage_t<sizeof(T), alignof(T)>;
public:
template<typename ...Args>
T* alloc(Args... args) {
std::size_t index = claimIndex();
return ::new(&slots[index]) T(std::forward<Args>(args)...);
}
void free(T* value) {
static_assert(sizeof(std::ptrdiff_t) <= sizeof(std::size_t));
StorageType* storagePtr = reinterpret_cast<StorageType*>(value);
free(storagePtr - slots);
}
void free(std::size_t index) {
std::destroy_at(std::launder(reinterpret_cast<T*>(&slots[index])));
freeIndex(index);
}
private:
std::size_t claimIndex() {
// Get a unique index, doesn't matter here how (say, bitset + throw if empty)
...
}
void freeIndex(std::size_t index) {
...
}
class alignas(T) Storage {
std::byte buffer[sizeof(T)];
};
StorageType slots[N];
};
这里有UB 吗?
成员函数alloc
是标准的placement-new,free
带有索引参数的成员函数重载是教科书上的std::launder
东西。指针算法定义明确。更难验证的部分是reinterpret_cast
底层存储类型。除了数组的一些极端情况外,有很多迹象表明placement new的指针类型应该是相同的地址。此外,强制转换的结果显然具有相同的对齐方式,并且实际上指向数组中的某个对象。
当然,这一切都假设用户不会传递一些随机数T*
,而只传递从返回的指针alloc
。
正如@AhmedAEK在评论中指出的那样,
std::aligned_storage
已被弃用,并且很可能会导致UB(请参阅P1413R3)。此外,该评论包含指向Jonathan Müller的“C++对象生命周期(不完全)指南”演示文稿的链接,这使我在这里找到了更长的版本。它对于理解适用于此问题的标准相关部分非常有帮助。首先,让我们摆脱使用
std::aligned_storage
。对于替换,P1413R3 建议使用正确对齐的字节数组。这是明确定义的行为,其细节在[intro.object]中介绍(它甚至有一个以与下文完全相同的方式使用 placement-new 的示例)。这样
alloc
就变成:void*
使用强制类型转换来确保避免任何 placement-new 的过载。接下来,
free
带size_t
参数的重载变为:std::launder
请注意,至少从 C++23 开始,需要使用。有关详细信息,请参阅P3006R0。最后,我们来到了问题的核心。答案在于指针可相互转换的概念,其细节在[basic.compound]中介绍。指向对象的指针和指向存储的指针(目前)还不能相互转换,但可以安全地从一个转换为另一个,如 P3006R0 中所示。但是,指向
storage
成员Slot
的指针可以与指向包含的指针相互转换,Slot
因为“一个是标准布局类对象,另一个是该对象的第一个非静态数据成员”。