Обоснование
По сути, этот вопрос задает вопрос о том, как использовать пользовательский распределитель с многоуровневым контейнером.Есть и другие условия, но подумав об этом, я решил проигнорировать некоторые из этих условий.Кажется, они мешают решениям без веской причины.Это оставляет открытой возможность ответа из стандартной библиотеки: std::scoped_allocator_adaptor
и std::vector
.
Возможно, самым большим изменением в этом подходе является отказ от идеи, что распределитель контейнера долженбыть модифицируемым после постройки (бросить член setAllocator
).Эта идея кажется сомнительной в целом и неверной в данном конкретном случае.Посмотрите на критерии для определения, какой распределитель использовать:
- Для выделения одного кадра требуется, чтобы объект был уничтожен к концу цикла по
timeStep
. - Распределение кучи должно бытьиспользуется, когда не может быть выделено однокадровое распределение.
То есть, вы можете сказать, какую стратегию размещения использовать, посмотрев на область действия рассматриваемого объекта / переменной.(Это внутри или снаружи тела цикла?) Область видимости известна во время построения и не изменяется (если вы не злоупотребляете std::move
).Таким образом, желаемый распределитель известен во время строительства и не изменяется.Однако текущие конструкторы не позволяют указывать распределитель.Это что-то изменить.К счастью, такое изменение является довольно естественным продолжением введения scoped_allocator_adaptor
.
Другим большим изменением является отбрасывание класса MyArray
.Существуют стандартные контейнеры для облегчения программирования.По сравнению с написанием собственной версии стандартные контейнеры быстрее внедряются (как уже было сделано) и менее подвержены ошибкам (стандарт стремится к более высокому качеству, чем «работает для меня в этот раз»).Итак, с шаблоном MyArray
и с std::vector
.
Как это сделать
Фрагменты кода в этом разделе можно объединить в один исходный файл, который компилируется,Просто пропустите мой комментарий между ними.(Вот почему только первый фрагмент содержит заголовки.)
Ваш текущий класс Allocator
является разумной отправной точкой.Ему просто нужна пара методов, которые указывают, когда два экземпляра являются взаимозаменяемыми (т.е. когда оба способны освободить память, выделенную одним из них).Я также позволил себе сменить amountByte
на тип без знака, поскольку выделение отрицательного объема памяти не имеет смысла.(Я оставил только тип align
, поскольку нет никаких указаний на то, какие значения это будет принимать. Возможно, это должно быть unsigned
или перечисление.)
#include <cstdlib>
#include <functional>
#include <scoped_allocator>
#include <vector>
class Allocator {
public:
virtual void * allocate(std::size_t amountByte, int align)=0;
virtual void deallocate(void * v)=0;
//some complex field and algorithm
// **** Addition ****
// Two objects are considered equal when they are interchangeable at deallocation time.
// There might be a more refined way to define this relation, but without the internals
// of Allocator, I'll go with simply being the same object.
bool operator== (const Allocator & other) const { return this == &other; }
bool operator!= (const Allocator & other) const { return this != &other; }
};
Далее следуют дваспециализаций.Однако их детали выходят за рамки вопроса.Поэтому я просто макет что-то, что будет компилироваться (необходимо, так как нельзя напрямую создать экземпляр абстрактного базового класса).
// Mock-up to allow defining the two allocators.
class DerivedAllocator : public Allocator {
public:
void * allocate(std::size_t amountByte, int) override { return std::malloc(amountByte); }
void deallocate(void * v) override { std::free(v); }
};
DerivedAllocator oneFrameAllocator;
DerivedAllocator heapAllocator;
Теперь мы переходим к первому мясному блоку - адаптации Allocator
к ожиданиям стандарта,Он состоит из шаблона оболочки, параметром которого является тип создаваемого объекта.Если вы можете проанализировать требования Allocator , этот шаг прост.Надо отметить, что анализ требований не прост, поскольку они предназначены для охвата «причудливых указателей».
// Standard interface for the allocator
template <class T>
struct AllocatorOf {
// Some basic definitions:
//Allocator & alloc; // A plain reference is an option if you don't support swapping.
std::reference_wrapper<Allocator> alloc; // Or a pointer if you want to add null checks.
AllocatorOf(Allocator & a) : alloc(a) {} // Note: Implicit conversion allowed
// Maybe this value would come from a helper template? Tough to say, but as long as
// the value depends solely on T, the value can be a static class constant.
static constexpr int ALIGN = 0;
// The things required by the Allocator requirements:
using value_type = T;
// Rebind from other types:
template <class U>
AllocatorOf(const AllocatorOf<U> & other) : alloc(other.alloc) {}
// Pass through to Allocator:
T * allocate (std::size_t n) { return static_cast<T *>(alloc.get().allocate(n * sizeof(T), ALIGN)); }
void deallocate(T * ptr, std::size_t) { alloc.get().deallocate(ptr); }
// Support swapping (helps ease writing a constructor)
using propagate_on_container_swap = std::true_type;
};
// Also need the interchangeability test at this level.
template<class T, class U>
bool operator== (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc == a_u.get().alloc; }
template<class T, class U>
bool operator!= (const AllocatorOf<T> & a_t, const AllocatorOf<U> & a_u)
{ return a_t.get().alloc != a_u.get().alloc; }
Далее следуют классы многообразия.Самый низкий уровень (M1) не нуждается в каких-либо изменениях.
Средним уровням (M2) требуется два дополнения для получения желаемых результатов.
- Тип элемента
allocator_type
нуждается вбыть определенным.Его существование указывает на то, что класс распознает распределитель. - Должен существовать конструктор, который принимает в качестве параметров объект для копирования и распределитель для использования.Это делает класс фактически осведомленным о распределителях. (Потенциально могут потребоваться другие конструкторы с параметром allocator, в зависимости от того, что вы на самом деле делаете с этими классами.
scoped_allocator
работает путем автоматического добавления распределителя к предоставленным параметрам конструкции. Так как пример кода делает копии внутри векторовтребуется конструктор «copy-plus-allocator».)
Кроме того, для общего использования средние уровни должны получить конструктор, у которого единственный параметр является распределителем.Для удобства чтения я также верну имя MyArray
(но не шаблон).
Для самого высокого уровня (M3) просто нужен конструктор, принимающий распределитель.Тем не менее, псевдонимы двух типов полезны для удобочитаемости и согласованности, поэтому я добавлю их также.
class M1{}; //e.g. a single-point collision site
class M2{ //e.g. analysed many-point collision site
public:
using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M1>>;
using MyArray = std::vector<M1, allocator_type>;
// Default construction still uses oneFrameAllocator, but this can be overridden.
explicit M2(const allocator_type & alloc = oneFrameAllocator) : m1s(alloc) {}
// "Copy" constructor used via scoped_allocator_adaptor
//M2(const M2 & other, const allocator_type & alloc) : m1s(other.m1s, alloc) {}
// You may want to instead delegate to the true copy constructor. This means that
// the m1s array will be copied twice (unless the compiler is able to optimize
// away the first copy). So this would need to be performance tested.
M2(const M2 & other, const allocator_type & alloc) : M2(other)
{
MyArray realloc{other.m1s, alloc};
m1s.swap(realloc); // This is where we need swap support.
}
MyArray m1s;
};
class M3{ //e.g. analysed collision surface
public:
using allocator_type = std::scoped_allocator_adaptor<AllocatorOf<M2>>;
using MyArray = std::vector<M2, allocator_type>;
// Default construction still uses oneFrameAllocator, but this can be overridden.
explicit M3(const allocator_type & alloc = oneFrameAllocator) : m2s(alloc) {}
MyArray m2s;
};
Давайте посмотрим ... две строки, добавленные к Allocator
(можно сократить доодин), четыре раза до M2
, три до M3
, исключите шаблон MyArray
и добавьте шаблон AllocatorOf
.Это не огромная разница.Ну, немного больше, чем рассчитывать, если вы хотите использовать автоматически сгенерированный конструктор копирования для M2
(но с преимуществом полной поддержки замены векторов).В целом, изменения не столь существенны.
Вот как будет использоваться код:
int main()
{
M3 output_m3{heapAllocator};
for ( int timeStep = 0; timeStep < 100; timeStep++ ) {
//v start complex computation #2
M3 m3;
M2 m2;
M1 m1;
m2.m1s.push_back(m1); // <-- vector uses push_back() instead of add()
m3.m2s.push_back(m2); // <-- vector uses push_back() instead of add()
//^ end complex computation
output_m3 = m3; // change to heap allocation
//.... clean up oneFrameAllocator here ....
}
}
Присвоенное здесь присваивание сохраняет стратегию выделения output_m3
, поскольку AllocatorOf
нескажи делать иначе.Похоже, это должно быть желаемым поведением, а не старым способом копирования стратегии распределения.Обратите внимание, что если обе стороны назначения уже используют одну и ту же стратегию распределения, не имеет значения, будет ли стратегия сохранена или скопирована.Следовательно, существующее поведение должно быть сохранено без необходимости дальнейших изменений.
Помимо указания того, что одна переменная использует распределение кучи, использование классов не является более сложным, чем это было раньше.Поскольку предполагалось, что в какой-то момент потребуется указать распределение кучи, я не понимаю, почему это было бы нежелательно.Используйте стандартную библиотеку - она здесь, чтобы помочь.