Especialización std :: allocator proporcionada por el usuario

8

Las plantillas de clase en el ::stdespacio de nombres generalmente pueden estar especializadas por programas para tipos definidos por el usuario. No encontré ninguna excepción a esta regla para std::allocator.

Entonces, ¿tengo permitido especializarme std::allocatorpara mis propios tipos? Y si se me permite, ¿debo proporcionar todos los miembros de std::allocatorla plantilla principal, dado que muchos de ellos pueden ser proporcionados por std::allocator_traits(y en consecuencia están en desuso en C ++ 17)?

Considera este programa

#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();
}

La salida de este programa con libc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libc++) es:

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

y la salida con libstdc ++ (Clang with -std=c++17 -Wall -Wextra -pedantic-errors -O2 -stdlib=libstdc++) es:

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

Como se puede ver libstdc ++ no siempre honor a la sobrecarga de los constructque he proporcionado y si quito las construct, destroyo max_sizelos miembros, entonces el programa no compila incluso con libstdc ++ quejándose de estos miembros que faltan, a pesar de que son suministrados por std::allocator_traits.

¿El programa tiene un comportamiento indefinido y, por lo tanto, las bibliotecas estándar son correctas o el comportamiento del programa está bien definido y la biblioteca estándar es necesaria para usar mi especialización?


Tenga en cuenta que hay algunos miembros de std::allocatorla plantilla principal de los que aún no he incluido en mi especialización. ¿Necesito agregarlos también?

Para ser precisos, dejé afuera

using is_always_equal = std::true_type

que es proporcionado por std::allocator_traitsmi asignador está vacío, pero sería parte de std::allocatorla interfaz de.

También me dejó fuera pointer, const_pointer, reference, const_reference, rebindy address, todos los cuales son proporcionados por std::allocator_traitsy en desuso en C ++ 17 para std::allocator's interfaz.

Si cree que es necesario definir todo esto para que coincida con std::allocatorla interfaz, considérelos agregados al código.

nuez
fuente
Retiro todo esto. Lo probé en Visual Studio 2019 y tiene todas las llamadas de constructor (incluso las llamadas de constructor de copia). . Sin embargo, supongo que libstdc ++ lo implementa de una manera que el optimizador cree que puede eliminarlos.
Spencer

Respuestas:

3

De acuerdo con 23.2.1 [container.requirements.general] / 3:

Para los componentes afectados por esta subcláusula que declaran un allocator_type, los objetos almacenados en estos componentes se construirán utilizando la allocator_traits<allocator_type>::constructfunción

Además, de acuerdo con 17.6.4.2.1:

El programa puede agregar una especialización de plantilla para cualquier plantilla de biblioteca estándar al espacio de nombres stdsolo si la declaración depende de un tipo definido por el usuario y la especialización cumple con los requisitos de biblioteca estándar para la plantilla original y no está explícitamente prohibida.

No creo que el estándar prohíba la especialización std::allocator, ya que leí todas las secciones std::allocatory no mencionó nada. También miré cómo se ve que el estándar prohíbe la especialización y no encontré nada parecido std::allocator.

Los requisitos para Allocatorestán aquí , y su especialización los satisface.

Por lo tanto, solo puedo concluir que libstdc ++ realmente viola el estándar (tal vez cometí un error en alguna parte). Descubrí que si uno simplemente se especializa std::allocator, libstdc ++ responderá usando la colocación nueva para el constructor porque tienen una especialización de plantilla específicamente para este caso mientras usan el asignador especificado para otras operaciones; el código relevante está aquí (esto está adentro namespace std; allocatoraquí está ::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_nllamadas std::_Constructque utiliza colocación nueva. Esto explica por qué no ve "Construir uno" antes de "Asignar para 4" en su salida.

EDITAR: como OP señaló en un comentario, las std::__uninitialized_default_nllamadas

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

que en realidad tiene una especialización si __is_trivial(_ValueType) && __assignablees true, que está aquí . Utiliza std::fill_n(donde valuese construye trivialmente) en lugar de invocar std::_Constructcada elemento. Como Aes trivial y se puede asignar copia, en realidad terminaría llamando a esta especialización. Por supuesto, esto tampoco sirve std::allocator_traits<allocator_type>::construct.

Leonid
fuente
1
En mi caso específico, en realidad no está usando la std::_Constructllamada por lo que puedo decir, porque Aes trivialmente copiable y trivialmente copiable asignable, por lo tanto, __uninitialized_default_n_1se elige la otra especialización de , que llama en su std::fill_nlugar, que luego se envía a memcpy/ memset. Era consciente de esto como una optimización que hace libstdc ++, pero no me di cuenta de que la std::allocator::constructllamada también se omite para los tipos no triviales. Por lo tanto, puede ser un descuido en cómo libstdc ++ identifica que std::allocatorse utiliza la biblioteca suministrada .
nogal
Tienes razón. Editado
Leonid