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