- Читается в процессе изучения
Memory new- это абстракция над выделением памяти в C и работать с ней не очень удобно. Например: хотим для типаMyTypeчтобыstd::vectorвыделял память одним способом, аstd::setдругим. С помощью перегрузок оператораnewэтого не добиться- Поэтому в C++ появилась более высокоуровневая абстракция: аллокаторы. Это способ переопределить выделение памяти до момента обращения к оператору
new
template <typename T>
struct Allocator {
T* allocate(size_t n) {
return ::operator new(n * sizeof(T), static_cast<std::align_val_t>(alignof(T))); // see alignment
}
void deallocate(T* ptr, size_t) {
::operator delete(ptr);
}
template <typename... Args>
void construct(T* ptr, Args&&... args) { // Args&&
new(ptr) T(args...); // std::forward(args)
}
void destroy(T* ptr) noexcept {
ptr->~T();
}
}- See more
- Большинство методов у всех аллокаторов будут одинаковые (например
constructиdestroy), а еще внутри есть кучаusing, поэтому в C++ была добавлена специальная оберткаallocator_traits allocator_traits- это структура, которая шаблонным параметром принимает класс вашего аллокатора- Почти все методы и внутренние юзинги работают по прицнипу "взять у аллокатора если есть, если нет сгенерировать автоматически"
#include <memory>
#include <stdexcept>
template <typename T, typename Alloc = std::allocator<T>>
class Vector {
public:
Vector(size_t n, const T& value = T(), const Alloc& allocator = Alloc());
T& operator[](size_t i) { return arr_[i]; }
const T& operator[](size_t i) const { return arr_[i]; }
T& at(size_t i) {
if (i >= size_) {
throw std::out_of_range("...");
}
return arr_[i];
}
const T& at(size_t i) const {
if (i >= size_) {
throw std::out_of_range("...");
}
return arr_[i];
}
size_t size() { return size_; }
size_t capacity() { return capacity_; }
void resize(size_t n, const T& value = T());
void reserve(size_t n);
private:
using AllocTraits =
std::allocator_traits<Alloc>; // это не обязательно, но так будет удобней
T* arr_;
size_t size_;
size_t capacity_;
Alloc alloc_; // Сохраняем к себе!
};
template <typename T, typename Alloc>
void Vector<T, Alloc>::reserve(size_t n) {
if (n <= capacity_) {
return;
}
// T* new_arr = new T[n]; // На неуд
// T* new_arr = reinterpret_cast<T*>(new int8_t[n * sizeof(T)]); На удос
// T* new_arr = alloc_.allocate(n); На хор
T* new_arr = AllocTraits::allocate(alloc_, n); // На отл
size_t i = 0;
try {
for (; i < size_; ++i) {
AllocTraits::construct(alloc_, new_arr + i,
arr_[i]); // здесь std::move(arr_[i]) (а точнее move_if_noexcept)
}
} catch (...) {
for (size_t j = 0; j < i; ++j) {
AllocTraits::destroy(alloc_, new_arr + j);
}
AllocTraits::deallocate(alloc_, new_arr, n);
throw;
}
for (size_t i = 0; i < size_; ++i) {
AllocTraits::destroy(alloc_, arr_ + i);
}
AllocTraits::deallocate(alloc_, arr_, capacity_);
arr_ = new_arr;
capacity_ = n;
}- Example: in
List<T>allocator isAllocator<T>, but we need to allocateNote<T> - Можно реализовать в аллокаторе и обращаться к
Alloc::rebind<Node<T>>::other
class Allocator {
template <typename U>
struct rebind {using other = Allocator<U>; }
}- Но это неудобно, да и реализация почти всегда будет одинаковой у всех аллокаторов, поэтому это вынесли в
allocator_traits - Realized in
std::allocator_traits
template <typename T, typename Alloc = std::allocator<T>>
class List {
public:
// ...
private:
using AllocTraits = std::allocator_traits<Alloc>;
using NodeAlloc = typename AllocTraits::template rebind_alloc<Node>;
using NodeAllocTraits = typename AllocTraits::template rebind_traits<Node>; // same as std::allocator_trais<NodeAlloc>
NodeAlloc alloc_;
};PoolAlloc alloc1;
PoolAlloc alloc2 = alloc1;-
Не знаем, что требуется:
- Что
alloc2просто должен работать какPoolAllocи скопировал в себя настройки (размер пула и тд) изalloc1 - Или чтобы эти два аллокатора отвечали за один и тот же пул
- Что
-
Или еще:
std::vector<int, PoolAlloc> v1;
std::vector<int, PoolAlloc> v2 = v1;
// Нужно ли копировать аллокатор, если да, то как?- Для это есть несколько юзингов
| Type | Definition | Desc |
|---|---|---|
value_type |
T |
|
pointer |
T* |
- deprecated in C++17 - removed in C++20 |
const_pointer |
const T* |
- deprecated in C++17 - removed in C++20 |
reference |
T& |
- deprecated in C++17 - removed in C++20 |
const_reference |
const T& |
- deprecated in C++17 - removed in C++20 |
size_type |
std::size_t |
|
difference_type |
std::ptrdiff_t |
|
propagate_on_container_*_assignment* - copy|move|swap(no assignment) |
std::true_type |
C++11 |
rebind |
template <class U> struct rebind { typedef allocator<U> other; }; |
- deprecated in C++17 - removed in C++20 |
is_always_equal |
std::true_type |
C++11 - deprecated in C++23 - removed in C++26 |
is_always_equal : true_type- Всегда равен любому другому
- about
true_typesee
propagate_on_container_copy/move/_assignment/swap : true_type- Говорит, надо ли при копировании/перемещении/swap'а объекта перетягивать и аллокатор (pool allocator for example)
- Но все равно аллокатор перетягивается, если аллокаторы не равны:
S& operator=(other) {
if(p_o_c_c_a || (!is_always_equal && alloc != other.alloc)) {
alloc = other.alloc;
}
...
} // this cringe + million of try-catch| Member | Type |
|---|---|
allocator_type |
Alloc |
value_type |
Alloc::value_type |
pointer |
Alloc::pointer if present, otherwise value_type* |
propagate_on_container_copy_assignment |
Alloc::propagate_on_container_copy_assignment if present, otherwise std::false_type |
propagate_on_container_move_assignment |
Alloc::propagate_on_container_move_assignment if present, otherwise std::false_type |
propagate_on_container_swap |
Alloc::propagate_on_container_swap if present, otherwise std::false_type |
is_always_equal |
Alloc::is_always_equal if present, otherwise std::is_empty<Alloc>::type |
| Function | Desc |
|---|---|
static pointer allocate(Alloc& a, size_type n); |
allocates uninitialized storage using the allocator |
static void deallocate( Alloc& a, pointer p, size_type n ); |
deallocates storage using the allocator |
template< class T, class... Args >static void construct( Alloc& a, T* p, Args&&... args ); |
constructs an object in the allocated storage |
template< class T > static void destroy( Alloc& a, T* p ); |
destructs an object stored in the allocated storage |
static Alloc select_on_container_copy_construction( const Alloc& a ); |
obtains the allocator to use after copying a standard container |
Поведение select_on_container_copy_construction следующее: |
- Если в аллокаторе определен метод
select_on_container_copy_construction, то вызывается он - Если метод не определен, то возвращается тот же аллокатор
| Type | Definition |
|---|---|
rebind_alloc<T> |
Alloc::rebind<T>::other if present, otherwise SomeAllocator<T, Args> if this Alloc is of the form SomeAllocator<U, Args>, where Args is zero or more type arguments |
rebind_traits<T> |
std::allocator_traits<rebind_alloc<T>> |
- Вообще раньше аллокаторы были stateless (без состояния => без полей)
- Было сложно работать с разными типами:
vector<int, Alloc1> =???= vector<int, Alloc2>
template <typename T>
struct polymorphic_allocator : std::scoped_allocator_adapter<...> {
std::memory_resource* resource_;
};
struct memory_resource {
virtual void* allocate(size_t s) = 0;
virtual void deallocate(void* p) = 0;
virtual bool is_equal(other) = 0;
};memory_resourceбез шаблонов => можно сделатьvirtual allocate/deallocate/... methodsи делать наследников:struct std::null_resource : std::memory_resource { ... }- Чистая заглушка
struct std::new_delete_resource : std::memory_resource { ... }- Честный
new+delete - Вообще это не наследник, а отдельная функция (все равно оно stateless)
- Честный
get/set_default_resource()- функции управления ресурсов по умолчанию (далее пригодится)- Изначально -
new_delete_resource
- Изначально -
struct monotonic_buffer_resource : std::memory_resource { ... }- Есть большой пулл, он вытаскивает память из него
- Когда закончился буффер, обращается к
upstream_resource- указателю на еще одинmemory resource. По умолчанию -get_default_resource()
struct std::(un)syncronized_pool_resource : std::memory_resource { ... }- Выделяет несколько буфферов (пуллов) (вроде 4)
- Первый пулл - для 1 аллокации 1 байта
- Второй - 2-х
- Третий - 4-х
- Четвертый - для аллокации памяти размерности более 4-х байт
syncronizedрасчитан для многопотока, аunsyncronizedбыстрее (не тратит много времени на синхронизацию), в остальном они одинаковы
struct std::memory_resource {
void* allocate(size_t) {
return do_allocate(s);
}
void* do_allocate(size_t) = 0;
};
struct my_res : public std::memory_resource {
void* do_allocate(size_t) { // Uses NVI
// ...
}
};- Пропагейты -
on_container_copy/move/swap - У полиморфика все выставлены в
false
namespace std::pmr {
template <class T>
using vector = std::vector<T, std::pmr::polymorphic_allocator>; // c++17
}pmr::vector<pmr::vector<int>> a(3);
[v1] -> A1
[v2] -> A2
[v3] -> A3
[-] -> -
insert(begin(), pmr::vector());
[v4] -> A1
[v1] -> A2
[v2] -> A3
[v3] -> A3
insert(begin(), pmr::vector());
// realloc (first of all, will insert in new data ptr new item (for safety exceptions))
[v5] -> A5
[v4] -> A4
[v1] -> A2
[v2] -> A3
[v3] -> A3
// cringepmr::vector<int> v = f(); // assign f() allocator quickly
pmr::vector<int> u; // creates own allocator
u = f(); // ?! - different behaviour