提问人:Oersted 提问时间:8/25/2023 最后编辑:Jan SchultkeOersted 更新时间:9/14/2023 访问量:122
正确使用对象数组的类型调整和擦除
Correctly using type-punning and erasure for array of objects
问:
我的目标是拥有一个用于存储对象数组的内存池非模板类。 同一内存池对象必须可重用于不同的数组(不同大小、不同类型和/或对齐方式)。
我已经发布了一系列问题,但它们可能过于关注有关可能实现的技术细节,而此实现可能不是正确的:
带着这个问题,我将重点关注“什么”。
我想有一个带有这个伪代码 API 的内存池类(和一个使用示例):
// type-punning reusable buffer for arrays
// holds a non-typed buffer (actually a char*) that can be used to store any
// types, according to user needs
struct Buffer {
// start of storage address
char* p = nullptr;
// adding whatever method and variable required to make it work
// ...
// Creates an adequate storage (if needed) to store an array of N object of
// type T and default-construct them returns a pointer to the first element
// of this array
template <typename T>
T* DefaultAllocate(const size_t N);
// Ends lifetime of the currently stored array of objects, if any, leaving
// the storage reusable for another array of possibly different type and
// size
// Make it non-template if possible
// Make it optional if possible (by calling it automatically in
// DefaultAllocate if needed)
template <typename T>
void Deallocate() {}
// Releasing all ressources (storage and objects)
~Buffer() {}
};
int main() {
constexpr std::size_t N0 = 7;
constexpr std::size_t N1 = 3;
Buffer B;
std::cout << "Test on SomeClass\n";
SomeClass* psc = B.DefaultAllocate<SomeClass>(N0);
psc[0] = somevalue0;
*(psc + 1) = somevalue1;
psc[2] = somevalue2;
std::cout << psc[0] << '\n';
std::cout << psc[1] << '\n';
std::cout << *(psc + 2) << '\n';
std::cout << "Test on SomeOtherClass\n";
// reallocating, possibly using existing storage, for a different type and
// size
SomeOtherClass* posc = B.DefaultAllocate<SomeOtherClass>(N1);
std::cout << posc[0] << '\n';
std::cout << posc[1] << '\n';
std::cout << posc[2] << '\n';
return 0;
}
应该如何实现此类以避免 UB、内存泄漏,让指针算术在类型化指针(由“DefaultAllocate”返回的指针)上有效并正确对齐?
我期待 C++14 个带有技术参考和解释的答案(什么确保了 UB 的缺失、指针算术的有效性,...)。
但我也对如何在更现代的版本中做到这一点感兴趣(特别是因为已经发生了一些根本性的变化,导致在某些特定情况下需要)。std::launder
注意:在使用 std::aligned_alloc 对对象数组进行类型双关时,一种非常有趣的技术(为了帮助数据擦除,已经提出了使用 和 lambda)。std::function
答:
首先:
如果只想通过解除分配内存块来清理池,则只能接受 的类型。
std::is_trivially_destructible
确保正确对齐是微不足道的,这是留给读者的练习。
创建单个对象
T *MakeOne()
{
return ::new(static_cast<void *>(address)) T{};
}
添加会使它零一些原本未初始化的类型:标量和具有隐式生成的默认构造函数的类,或直接在类体中标记的默认构造函数(对于类,只有未初始化的成员才会归零)。{}
=default
添加并确保这始终选择内置的 placement-new,而不是一些用户提供的重载。::
static_cast<void *>(...)
在 C++20 中创建数组
T *MakeArray(std::size_t n)
{
return ::new(static_cast<void *>(address)) T[n]{};
}
在 C++20 之前创建数组
(唯一需要此解决方法的编译器是 MSVC,请参阅 Microsoft C/C++ 语言一致性,并参阅CWG2382 非分配放置的数组分配开销 new)
T *MakeArray(std::size_t n)
{
for (std::size_t i = 0; i < n; i++)
::new(static_cast<void *>(address + i * sizeof(T))) T{};
// Just remove `launder` if your language standard version doesn't have it.
return std::launder(reinterpret_cast<T *>(address));
}
std::launder
是必需的,因为您有一个指向包含对象的内存位置的指针,但该指针是以非法方式获取的(标准说它“不指向”您的对象,尽管它具有正确的值),例如当您没有存储 placement-new 返回的值,并且只知道最初传递给它的指针时。
在实践中,缺乏很少会破坏事情(在添加 C++17 之前,如果没有 UB 就无法实现,并且没有人遇到问题)。因此,如果您使用的是 C++14 或更早版本,则可以省略它,事情应该可以正常工作。std::launder
std::vector
launder
更多的 UB 可能隐藏在这里,例如在指向原始存储的指针上使用(请参阅此相关问题),但这可以说是标准中的一个缺陷,并且没有编译器强制执行这一点。+
评论
std::vector
”。事实上,指针算术 () (将)使 UB 成为 UB(但作为 std 库的一部分,规则是不同的)。如果没有,我们可能有一个额外的指针数组来存储放置 new 返回的指针(效率低下但合法)......data()
std::vector
std::launder
new
sizeof(T)*N
您可以创建“模板类”并执行如下操作:
[[nodiscard]] T* allocate(std::size_t n) noexcept
{
if (auto p = static_cast<T*>malloc(n * sizeof(T))))
{
return p;
}
return nullptr;
}
void deallocate(T* p, std::size_t n) noexcept
{
if( nullptr != p )
free(p);
}
评论
Buffer
std::vector
any