C ++ Распределение памяти (с использованием allocator) в стертом контексте типа - PullRequest
0 голосов
/ 14 февраля 2019

В стандартной библиотеке 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
    }
};

Проблема, очевидно, в том, что мы теряем распределитель, который изначально выделил объект 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
}

Теперь мы можем получить тот же объект для освобождения ранее выделенного объекта.Последняя проблема, которую нужно решить, - это освобождение.Объект не может освободить себя, поэтому тот же трюк можно использовать, чтобы обернуть логику освобождения и использовать ее через интерфейс.

// 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
        );
    }
};

И, аналогично, сохранить его в статическом методе:

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;
}

Единственное ограничение, которое я вижу, состоит в том, что эта реализация использует один и тот же распределитель для всех объектов одного типа, что означает, что в случае копирования / перемещения распределитель не будет скопирован / перемещен. Но разве это не лучше, чем вообще никакой распределитель ?Я протестировал код здесь (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
...