Предоставляемая пользователем специализация std :: allocator - PullRequest
8 голосов
/ 11 апреля 2020

Шаблоны классов в пространстве имен ::std обычно могут специализироваться программами для пользовательских типов. Я не нашел никаких исключений из этого правила для std::allocator.

Итак, мне разрешено специализировать std::allocator для моих собственных типов? И если мне разрешено, нужно ли предоставлять все члены первичного шаблона std::allocator, учитывая, что многие из них могут быть предоставлены std::allocator_traits (и, следовательно, не рекомендуется в C ++ 17)?

Рассмотрим эту программу

#include<vector>
#include<utility>
#include<type_traits>
#include<iostream>
#include<limits>
#include<stdexcept>

struct A { };

namespace std {
    template<>
    struct allocator<A> {
        using value_type = A;
        using size_type = std::size_t;
        using difference_type = std::ptrdiff_t;
        using propagate_on_container_move_assignment = std::true_type;

        allocator() = default;

        template<class U>
        allocator(const allocator<U>&) noexcept {}

        value_type* allocate(std::size_t n) {
            if(std::numeric_limits<std::size_t>::max()/sizeof(value_type) < n)
                throw std::bad_array_new_length{};
            std::cout << "Allocating for " << n << "\n";
            return static_cast<value_type*>(::operator new(n*sizeof(value_type)));
        }

        void deallocate(value_type* p, std::size_t) {
            ::operator delete(p);
        }

        template<class U, class... Args>
        void construct(U* p, Args&&... args) {
            std::cout << "Constructing one\n";
            ::new((void *)p) U(std::forward<Args>(args)...);
        };

        template<class U>
        void destroy( U* p ) {
            p->~U();
        }

        size_type max_size() const noexcept {
            return std::numeric_limits<size_type>::max()/sizeof(value_type);
        }
    };
}

int main() {
    std::vector<A> v(2);
    for(int i=0; i<6; i++) {
        v.emplace_back();
    }
    std::cout << v.size();
}

Вывод этой программы с помощью libc ++ (Clang с -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++):

Allocating for 2
Constructing one
Constructing one
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

и вывод с libstdc ++ (Clang с -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++):

Allocating for 2
Allocating for 4
Constructing one
Constructing one
Allocating for 8
Constructing one
Constructing one
Constructing one
Constructing one
8

Как вы можете видеть, libstdc ++ не всегда учитывает предоставленную мною перегрузку construct, и если я удаляю construct, destroy или max_size членов, тогда программа даже не компилируется с libstdc ++, жалуясь на эти отсутствующие члены, хотя они предоставляются std::allocator_traits.

Имеет ли программа неопределенное поведение и поэтому обе стандартные библиотеки корректны или поведение программы четко определено и стандартная библиотека необходима для моей специализации?


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

Если быть точным, я пропустил

using is_always_equal = std::true_type

, который предоставляется std::allocator_traits, так как мой распределитель пуст, но будет частью * Интерфейс 1040 *.

Я также пропустил pointer, const_pointer, reference, const_reference, rebind и address, все из которых предоставлены std::allocator_traits и устарел в C ++ 17 для интерфейса std::allocator.

Если вы считаете, что необходимо определить все это для соответствия интерфейсу std::allocator, то, пожалуйста, рассмотрите их как добавленные в код.

1 Ответ

3 голосов
/ 16 апреля 2020

Согласно 23.2.1 [container.requirements.general] / 3:

Для компонентов, затронутых в этом подпункте, которые объявляют allocator_type, объекты, хранящиеся в этих компонентах, должны создаваться с использованием allocator_traits<allocator_type>::construct функция

Также, согласно 17.6.4.2.1:

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

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

Требования для Allocator: здесь , и Ваша специализация их удовлетворяет.

Поэтому я могу только заключить, что libstdc ++ фактически нарушает стандарт (возможно, я где-то допустил ошибку). Я обнаружил, что если кто-то просто специализирует std::allocator, libstdc ++ ответит, используя размещение new для конструктора, потому что у них есть специализация шаблона специально для этого случая при использовании указанного распределителя для других операций; соответствующий код здесь (это namespace std; allocator здесь ::std::allocator):

  // __uninitialized_default_n_a
  // Fills [first, first + n) with n default constructed value_types(s),
  // constructed with the allocator alloc.
  template<typename _ForwardIterator, typename _Size, typename _Allocator>
    _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                _Allocator& __alloc)
    {
      _ForwardIterator __cur = __first;
      __try
    {
      typedef __gnu_cxx::__alloc_traits<_Allocator> __traits;
      for (; __n > 0; --__n, (void) ++__cur)
        __traits::construct(__alloc, std::__addressof(*__cur));
      return __cur;
    }
      __catch(...)
    {
      std::_Destroy(__first, __cur, __alloc);
      __throw_exception_again;
    }
    }

  template<typename _ForwardIterator, typename _Size, typename _Tp>
    inline _ForwardIterator
    __uninitialized_default_n_a(_ForwardIterator __first, _Size __n, 
                allocator<_Tp>&)
    { return std::__uninitialized_default_n(__first, __n); }

std::__uninitialized_default_n вызывает std::_Construct, в котором используется новое размещение. Это объясняет, почему вы не видите «Построение одного» перед «Выделением для 4» в своих выходных данных.

РЕДАКТИРОВАТЬ: Как указано в комментарии OP, std::__uninitialized_default_n звонит

__uninitialized_default_n_1<__is_trivial(_ValueType)
                             && __assignable>::
__uninit_default_n(__first, __n)

, который на самом деле имеет специализацию, если __is_trivial(_ValueType) && __assignable равно true, что здесь . Он использует std::fill_n (где value создается тривиально) вместо вызова std::_Construct для каждого элемента. Так как A является тривиальным и может быть назначен для копирования, он фактически вызовет эту специализацию. Конечно, это также не использует std::allocator_traits<allocator_type>::construct.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...