我的目标是拥有一个内存池非模板类,用于存储对象数组。 相同的内存池对象必须可重用于不同的数组(不同大小、不同类型和/或对齐方式)。
我已经发布了一系列问题,但他们可能过于关注有关可能实现的技术细节,而这个实现可能不是正确的:
我会带着这个问题,关注“什么”。
我想要一个带有这个伪代码 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
)。
NB 在使用 std::aligned_alloc 进行对象数组的类型双关中,一种非常有趣的技术(已提出使用
std::function
和 lambda 来帮助数据擦除)。
首先:
如果您想简单地通过释放内存块来清理池,则只能接受
std::is_trivially_destructible
的类型。
确保正确对齐很简单,留给读者作为练习。
创建单个对象:
T *MakeOne()
{
return ::new((void *)address) T{};
}
添加
{}
会使某些类型归零,否则这些类型将无法初始化:标量和具有隐式生成的默认构造函数的类,或者直接在类主体中标记为 =default
的默认构造函数(对于类,仅是其他类型的成员)未初始化的被清零)。
添加
::
和 (void *)
确保始终选择内置的新放置,而不是某些用户提供的重载。
在 C++20 中创建数组:
T *MakeArray(std::size_t n)
{
return ::new((void *)address) T[n]{};
}
在 C++20 之前创建数组:(需要此解决方法的唯一编译器是 MSVC,请参阅 link,查找
CWG 2382
)
T *MakeArray(std::size_t n)
{
for (std::size_t i = 0; i < n; i++)
::new((void *)(address + i * sizeof(T))) T{};
return std::launder((T *)address); // Just remove `launder` if your language standard version doesn't have it.
}
当您有一个指向包含对象的内存位置的指针时,需要 std::launder
,但该指针是以非法方式获得的(标准说它“不指向”您的对象,尽管具有正确的值),比如当你没有存储placement-new返回的值,而只知道最初传递给它的指针时。
缺乏
std::launder
在实践中很少会破坏事情(在 C++17 添加 std::vector
之前,没有 UB 就无法实现 launder
,并且没有人遇到问题)。因此,如果您使用的是 C++14 或更早版本,则可以忽略它,事情应该可以正常工作。
更多的 UB 可能隐藏在这里,例如在指向原始存储的指针上使用
+
(link),但这可以说是标准中的缺陷,并且没有编译器强制执行这一点。