正确使用类型双关和擦除对象数组

问题描述 投票:0回答:1

我的目标是拥有一个内存池非模板类,用于存储对象数组。 相同的内存池对象必须可重用于不同的数组(不同大小、不同类型和/或对齐方式)。

我已经发布了一系列问题,但他们可能过于关注有关可能实现的技术细节,而这个实现可能不是正确的:

我会带着这个问题,关注“什么”。
我想要一个带有这个伪代码 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 来帮助数据擦除)。

c++ arrays pointer-arithmetic strict-aliasing type-punning
1个回答
0
投票

首先:

  • 如果您想简单地通过释放内存块来清理池,则只能接受

    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),但这可以说是标准中的缺陷,并且没有编译器强制执行这一点。

© www.soinside.com 2019 - 2024. All rights reserved.