Почему многие функции распределителя являются необязательными?

Я только что завершил личный проект по переопределению операторов new и delete и в процессе узнал о классах распределителей. Прочитав несколько онлайн-справочников, в том числе cppreference.com, я заметил, что многие функции описываются как необязательные.

Мой вопрос заключается в том, как получатель распределителя, например. std::set, работать, если его полученный аллокатор только опционально реализует функции и типы?

Я бы понял, если бы распределитель должен был быть производным от некоторого базового класса, имеющего реализации по умолчанию для всех функций, но, похоже, для распределителей не требуется наследования.

Или это рассуждение о том, что о необходимости реализации этих необязательных функций можно было бы узнать из-за ошибки компиляции?

В качестве справки, вот моя первая попытка распределителя, который я использовал в качестве третьего аргумента шаблона для std::set. Я работал на существующем примере, и поэтому я считаю, что многое из того, что я реализовал, может быть ненужным, но я пока не понимаю, как судить, будет ли необходимо, если я решу использовать распределитель с каким-либо другим контейнером STL в будущем. Если, опять же, ожидание не состоит в том, чтобы понять это на основе ошибок компиляции...?

template <typename T>
class Allocator // Custom allocator for use by AllocSet::mAllocatedPtrs to avoid infinite recursion
{               // (because inserting an element in std::set calls ::operator new, which is overridden to add to AllocSet::mAllocatedPtrs).
    public:
        typedef T value_type;
        typedef T* pointer;
        typedef T& reference;
        typedef const T* const_pointer;
        typedef const T& const_reference;
        typedef size_t size_type;
        typedef ptrdiff_t difference_type;

        template <typename U>
        struct rebind { typedef Allocator<U> other; };

        Allocator() { }
        template <typename U> Allocator(const Allocator<U>&) { }
        virtual ~Allocator() { }

        pointer allocate(size_t numT) { return (T*)(malloc(numT * sizeof(T))); }
        void deallocate(pointer p, size_type st) { free(p); }

        size_type max_size() { return size_type(-1); }

        reference operator=(const_reference) { return *this; }
        template <typename U> reference operator=(const Allocator<U>&) { return *this; }
        template <typename U> bool operator==(const Allocator<U>&) { return true; }
        template <typename U> bool operator!=(const Allocator<U>&) { return false; }

        pointer address(reference r) { return &r; }
        const_pointer address(const_reference r) { return &r; }
};

person StoneThrow    schedule 09.09.2016    source источник


Ответы (1)


Мой вопрос заключается в том, как получатель распределителя, например. std::set, работает, если его полученный аллокатор только необязательно реализует функции и типы?

Если вы посмотрите на концепцию распределителя:

Некоторые требования являются необязательными: шаблон std::allocator_traits предоставляет реализации по умолчанию для всех необязательных требований, а все контейнеры стандартных библиотек и другие классы, поддерживающие распределители, обращаются к распределителю через std::allocator_traits, а не напрямую.

Это также является причиной того, что так много вещей являются необязательными — большинство реализаций распределителя действительно не нуждаются в их изменении, так зачем беспокоиться? Скажем, у вас есть идея нового алгоритма реорганизации памяти, зачем вам определять pointer?


Или это рассуждение о том, что о необходимости реализации этих необязательных функций можно было бы узнать из-за ошибки компиляции?

Нет, концепция распределителя четко определена. В нем указано, что вы должны предоставить и что вы можете предоставить. Нет необходимости полагаться на ошибки компиляции.

Вы можете найти требования в стандарте, $17.6.3.5.

person Ami Tavory    schedule 09.09.2016
comment
Я думаю, что понимаю: поэтому пользователи распределителей будут использовать std::allocator_traits по умолчанию, если они не были реализованы в созданном пользователем распределителе - это правильно? Если да, то это похоже на полиморфизм. Знаете ли вы, почему эта концепция распределителя была реализована именно таким образом, а не интерфейсом/подходом к реализации? - person StoneThrow; 09.09.2016
comment
@StoneThrow Не совсем так: пользователи распределителей должны безоговорочно использовать std::allocator_traits. std::allocator_traits - это тот, кто выясняет, предоставляет ли распределитель что-то или нет. Например, см. здесь как проверить, существует ли max_size(). - person Ami Tavory; 09.09.2016
comment
Ясно спасибо. Опять же, есть идеи, почему этот подход был использован вместо полиморфизма на основе классов? Я только недавно занялся кодированием на основе шаблонов, поэтому мне любопытны плюсы и минусы, а также причины, когда его использовать, а когда нет. - person StoneThrow; 09.09.2016
comment
@StoneThrow Не уверен, что вы подразумеваете под полиморфизмом на основе классов. Если вы имеете в виду использование виртуальных функций, то они 1. несут накладные расходы во время выполнения, 2. не могут использоваться для типов. Трейты времени компиляции, однако, не имеют этих проблем (хотя они добавляют что-то ко времени сборки, но не более того). - person Ami Tavory; 09.09.2016
comment
Если вы имеете в виду полагаться на виртуальные функции — да, я это и имел в виду. Спасибо за ваши ответы. - person StoneThrow; 09.09.2016
comment
Я создал раздел allocator_traits стандарта и одобряю этот ответ. :-) - person Pablo Halpern; 03.09.2017
comment
Я все еще не понимаю дизайн STL API, почему бы не изобрести концепцию под названием AllocatorTrait и позволить мне передать любой тип, который ее удовлетворяет (std::allocator_traits считается достаточно хорошим примером, ленивые люди могут изобрести custom_allocator‹T›, тогда просто использовать std::allocator_traits‹custom_allocator‹T››)? Так же, как второй параметр шаблона std::basic_string , позволяя мне передать любой тип, удовлетворяющий концепции CharTraits. - person 炸鱼薯条德里克; 18.10.2017