Как я могу написать stateful allocator в С++ 11, учитывая требования к построению копии?

Насколько я могу судить, требования к распределителю, который будет использоваться с STL контейнеры размещены в таблице 28 раздела 17.6.3.5 стандарта С++ 11.

Я немного смущен в отношении взаимодействия между некоторыми из этих требований. Для типа X, который является распределителем для типа T, тип Y, который является "соответствующий класс распределителя" для типа U, экземпляры a, a1 и a2 of X и экземпляр b Y, таблица говорит:

  • Выражение a1 == a2 оценивается как true, только если выделено хранилище из a1 может быть освобождено a2 и наоборот.

  • Выражение X a1(a); хорошо сформировано, не выходит через исключение, и после этого a1 == a истинно.

  • Выражение X a(b) хорошо сформировано, не выходит через исключение и после a == b.

Я прочитал это, сказав, что все распределители должны быть конструктивно-копируемыми в таких что копии являются взаимозаменяемыми с оригиналами. Хуже того, то же самое true через границы типов. Это кажется довольно обременительным требованием; в виде насколько я могу судить, это делает невозможным большое количество типов распределителей.

Например, скажем, у меня был класс freelist, который я хотел использовать в своем распределителе, чтобы кэшировать освобожденные объекты. Если я что-то не упустил, я не мог включить экземпляр этого класса в распределитель, поскольку размеры или выравнивания T и U могут различаться, и, следовательно, списки фриланда несовместимо.

Мои вопросы:

  • Правильны ли мои интерпретации?

  • Я читал в нескольких местах, что С++ 11 улучшил поддержку для "stateful распределители ". Как это обстоятельство, учитывая эти ограничения?

  • Есть ли у вас какие-либо предложения о том, как делать то, что я пытаюсь делать? То есть, как я могу включить состояние выделенного типа в свой распределитель?

  • В общем, язык вокруг распределителей кажется неаккуратным. (Например, пролог к ​​таблице 28 говорит, что a имеет тип X&, но некоторые из выражения redefine a.) Кроме того, по крайней мере поддержка GCC является несоответствующей. Что объясняет эту странность вокруг распределителей? Это просто нечасто используемая функция?

Ответ 1

1) Правильны ли мои интерпретации?

Вы правы, что ваш бесплатный список не подходит для распределителей, он должен иметь возможность обрабатывать несколько размеров (и выравниваний) для соответствия. Это проблема для свободного списка.

2) Я прочитал в нескольких местах, что С++ 11 улучшил поддержку "генераторов с сохранением состояния". Как это обстоятельство, учитывая эти ограничения?

Это не так улучшилось, как родилось. В С++ 03 любой экземпляр распределителя должен был быть эквивалентен другому экземпляру того же типа, что фактически делало невозможным использование генераторов с сохранением состояния.

3) Есть ли у вас какие-либо предложения о том, как делать то, что я пытаюсь сделать? То есть, как я могу включить состояние выделенного типа в свой распределитель?

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

Да, это дорогостоящее требование.

4) В общем, язык вокруг распределителей кажется неаккуратным. (Например, пролог к ​​таблице 28 говорит, что предполагается, что a имеет тип X &, но некоторые из выражений переопределяют a.) Кроме того, по крайней мере поддержка GCC является несоответствующей. Что объясняет эту странность вокруг распределителей? Это нечасто используемая функция?

Стандарт вообще не так просто читать, а не только распределители. Вы должны быть осторожны.

Чтобы быть педантом, gcc не поддерживает распределители (это компилятор). Я предполагаю, что вы говорите о libstdС++ (реализация стандартной библиотеки поставляется с gcc). libstdС++ является старым, и поэтому он был скроен С++ 03. Он был адаптирован к С++ 11, но пока не полностью соответствует (по-прежнему использует Copy-On-Write для строк). Причина в том, что libstdС++ имеет большой упор на двоичную совместимость, и ряд изменений, требуемых С++ 11, нарушит эту совместимость; поэтому их следует тщательно вводить.

Ответ 2

Равенство распределителей не означает, что они должны иметь точно такое же внутреннее состояние, только то, что они должны оба освобождать память, которая была распределена с помощью распределителя. Радиальное распределение распределителей a == b для распределителя a типа X и распределителя b типа Y определено в таблице 28 как "такое же, как a == Y::template rebind<T>::other(b)". Другими словами, a == b, если память, выделенная с помощью a, может быть освобождена распределителем, созданным путем восстановления b до a value_type.

Ваши сторонние распределители не должны освобождать узлы произвольного типа, вам нужно только освободить память, выделенную FreelistAllocator<T>, с помощью FreelistAllocator<U>::template rebind<T>::other. Учитывая, что FreelistAllocator<U>::template rebind<T>::other является тем же самым типом, что и FreelistAllocator<T> в большинстве нормальных реализаций, этого довольно легко достичь.

Простой пример (Живая демонстрация в Coliru):

template <typename T>
class FreelistAllocator {
    union node {
        node* next;
        typename std::aligned_storage<sizeof(T), alignof(T)>::type storage;
    };

    node* list = nullptr;

    void clear() noexcept {
        auto p = list;
        while (p) {
            auto tmp = p;
            p = p->next;
            delete tmp;
        }
        list = nullptr;
    }

public:
    using value_type = T;
    using size_type = std::size_t;
    using propagate_on_container_move_assignment = std::true_type;

    FreelistAllocator() noexcept = default;
    FreelistAllocator(const FreelistAllocator&) noexcept {}
    template <typename U>
    FreelistAllocator(const FreelistAllocator<U>&) noexcept {}
    FreelistAllocator(FreelistAllocator&& other) noexcept :  list(other.list) {
        other.list = nullptr;
    }

    FreelistAllocator& operator = (const FreelistAllocator&) noexcept {
        // noop
        return *this;
    }

    FreelistAllocator& operator = (FreelistAllocator&& other) noexcept {
        clear();
        list = other.list;
        other.list = nullptr;
        return *this;
    }

    ~FreelistAllocator() noexcept { clear(); }

    T* allocate(size_type n) {
        std::cout << "Allocate(" << n << ") from ";
        if (n == 1) {
            auto ptr = list;
            if (ptr) {
                std::cout << "freelist\n";
                list = list->next;
            } else {
                std::cout << "new node\n";
                ptr = new node;
            }
            return reinterpret_cast<T*>(ptr);
        }

        std::cout << "::operator new\n";
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

    void deallocate(T* ptr, size_type n) noexcept {
        std::cout << "Deallocate(" << static_cast<void*>(ptr) << ", " << n << ") to ";
        if (n == 1) {
            std::cout << "freelist\n";
            auto node_ptr = reinterpret_cast<node*>(ptr);
            node_ptr->next = list;
            list = node_ptr;
        } else {
            std::cout << "::operator delete\n";
            ::operator delete(ptr);
        }
    }
};

template <typename T, typename U>
inline bool operator == (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return true;
}

template <typename T, typename U>
inline bool operator != (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return false;
}

Ответ 3

Я прочитал это, сказав, что все распределители должны быть построены с возможностью копирования таким образом, чтобы копии были взаимозаменяемы с оригиналами. Хуже того, то же самое верно для границ типов. Это кажется довольно обременительным требованием; насколько я могу судить, это делает невозможным большое количество типов распределителей.

Тривиально удовлетворять требованиям, если распределители являются легким дескриптором на некотором ресурсе памяти. Просто не пытайтесь встроить ресурс в отдельные объекты распределителя.

Например, скажем, у меня был класс freelist, который я хотел использовать в моем распределителе, чтобы кэшировать освобожденные объекты. Если я чего-то не упускаю, я не мог бы включить экземпляр этого класса в распределитель, потому что размеры или выравнивания T и U могут отличаться, и поэтому списки фрилансистов несовместимы.

[allocator.requirements] пункт 9:

Распределитель может ограничивать типы, на которых он может быть создан, и аргументы, для которых может быть вызван его член construct. Если тип не может использоваться с определенным распределителем, класс allocator или вызов construct могут не создаваться.

ОК, чтобы ваш распределитель отказался выделить память для чего-либо, кроме T. Это предотвратит его использование в контейнерах на основе node, таких как std::list, которым необходимо выделить собственные внутренние типы node (а не только контейнер value_type), но он отлично работает для std::vector

  • Правильны ли мои интерпретации?

Не совсем.

  1. В нескольких местах я читал, что С++ 11 улучшает поддержку "распределенных состояний". Как это обстоятельство, учитывая эти ограничения?

Ограничения до С++ 11 были еще хуже!

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

  1. Есть ли у вас предложения по тому, как делать то, что я пытаюсь сделать? То есть, как я могу включить состояние выделенного типа в свой распределитель?

Не вставляйте его непосредственно в распределитель, храните его отдельно и назначайте его распределителем указателем (возможно, умным указателем, в зависимости от того, как вы планируете управление ресурсом ресурса).

Не пытайтесь поддерживать распределение для всех типов, если вам нужно только его поддерживать.

  1. В общем, язык вокруг распределителей кажется неаккуратным. (Например, пролог к ​​таблице 28 говорит, что предполагается, что a имеет тип X &, но некоторые из выражений переопределяют a.)

Да, как вы сообщили в https://github.com/cplusplus/draft/pull/334 (спасибо).

Кроме того, по крайней мере поддержка GCC является несоответствующей.

Это не 100%, но будет в следующей версии.

Что объясняет эту странность вокруг распределителей? Это нечасто используемая функция?

Да. И там много исторического багажа, и его трудно назвать широко полезным. Моя презентация ACCU 2012 содержит некоторые детали, я буду очень удивлена, если после прочтения вы подумаете, что можете сделать это проще; -)