类型擦除上下文中的 C++ 内存分配(使用分配器)

bar*_*-md 6 c++ memory-management type-erasure allocator c++-standard-library

标准 C++ 库中有许多类可能会分配内存但不接受分配器。其中一些这样做是因为在类型擦除的上下文中分配内存是不可能的。

一个例子是 std::any 有一个构造函数,该构造函数在其设计的某个时刻接受了一个 Allocator 参数,但由于似乎无法实现而被丢弃。我确实对这种情况考虑了一段时间,我想知道阻止其实施的确切问题是什么不能满足标准的哪些要求

假设我们从any. 分配内存很简单:

struct any {

    struct type_interface;

    template <typename T>
    struct type_impl;

    type_interface* value;

    any(T&& value, const Allocator& allocator = Allocator()) {
        using actual_allocator_t
            = std::allocator_traits<Allocator>::rebind_alloc<type_impl<T>>;
        actual_allocator_t actual_allocator;
        // do allocate
        // do construct
        // assign obtained pointer
    }
};
Run Code Online (Sandbox Code Playgroud)

问题显然是我们失去了最初分配对象的分配器type_impl<T>。一个技巧可能是创建一个方法来声明一个静态变量来存储该分配器。

template <typename Allocator>
auto& get_allocator(const Allocator& allocator) {
    using actual_allocator_t = std::allocator_traits<Allocator>::rebind_alloc<type_impl<T>>;
    // static variable: initialized just on the first call
    static actual_allocator_t actual_allocator(allocator);
    return actual_allocator;
}
// and the constructor is now
any::any(T&& value, const Allocator& allocator = Allocator()) {
    auto actual_allocator = get_allocator(allocator);
    // do allocate
    // do construct
    // assign obtained pointer
}
Run Code Online (Sandbox Code Playgroud)

现在我们可以检索相同的对象来释放之前分配的对象。最后要解决的问题是解除分配。对象无法释放自身,因此可以使用相同的技巧来包装释放逻辑并通过接口使用它。

// Deallocator interface
struct deallocate_interface{
    virtual void deallocate(void*) {};
};

template <typename Allocator>
struct deallocate_wrapper{
    virtual void deallocate(void* ptr) {
        std::allocator_traits<Allocator>::deallocate(
            this->allocator,
            reinterpret_cast<typename Allocator::value_type*>(ptr),
            1u
        );
    }
};
Run Code Online (Sandbox Code Playgroud)

并且,以同样的方式,将它存储到一个静态方法中:

template <typename Allocator>
deallocate_interface& get_deallocator(Allocator& allocator) {
    auto& actual_allocator = get_allocator(allocator);
    // static variable: initialized just on the first call
    static deallocate_wrapper<std::decay_t<decltype(actual_allocator)>> deallocator(actual_allocator);
    return deallocator;
}
Run Code Online (Sandbox Code Playgroud)

我看到的唯一限制是这个实现对所有相同类型的对象使用相同的分配器,这意味着在复制/移动的情况下,分配器不会被复制/移动。但这不是比没有分配器更好吗?我在这里测试了代码(https://github.com/barsan-md/type-erasure-and-allocation)以查看它是否按预期工作。一个可能的输出是:

Begin
Allocator: 0x55ec6563a132, allocating:   0x55ec667e4280
Constructed: 0x55ec667e4280, print: Hello
Allocator: 0x55ec6563a132, allocating:   0x55ec667e42b0
Constructed: 0x55ec667e42b0, print: World
Allocator: 0x55ec6563a140, allocating:   0x55ec667e42e0
Constructed: 0x55ec667e42e0, print: 12345
Destroyed:   0x55ec667e42e0, print: 12345
Allocator: 0x55ec6563a140, deallocating: 0x55ec667e42e0
Destroyed:   0x55ec667e42b0, print: World
Allocator: 0x55ec6563a132, deallocating: 0x55ec667e42b0
Destroyed:   0x55ec667e4280, print: Hello
Allocator: 0x55ec6563a132, deallocating: 0x55ec667e4280
End
Run Code Online (Sandbox Code Playgroud)