Las plantillas de clase en el ::std
espacio 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::allocator
para mis propios tipos? Y si se me permite, ¿debo proporcionar todos los miembros de std::allocator
la 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 construct
que he proporcionado y si quito las construct
, destroy
o max_size
los 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::allocator
la 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_traits
mi asignador está vacío, pero sería parte de std::allocator
la interfaz de.
También me dejó fuera pointer
, const_pointer
, reference
, const_reference
, rebind
y address
, todos los cuales son proporcionados por std::allocator_traits
y en desuso en C ++ 17 para std::allocator
's interfaz.
Si cree que es necesario definir todo esto para que coincida con std::allocator
la interfaz, considérelos agregados al código.
Respuestas:
De acuerdo con 23.2.1 [container.requirements.general] / 3:
Además, de acuerdo con 17.6.4.2.1:
No creo que el estándar prohíba la especialización
std::allocator
, ya que leí todas las seccionesstd::allocator
y no mencionó nada. También miré cómo se ve que el estándar prohíbe la especialización y no encontré nada parecidostd::allocator
.Los requisitos para
Allocator
está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á adentronamespace std
;allocator
aquí está::std::allocator
):std::__uninitialized_default_n
llamadasstd::_Construct
que 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_n
llamadasque en realidad tiene una especialización si
__is_trivial(_ValueType) && __assignable
estrue
, que está aquí . Utilizastd::fill_n
(dondevalue
se construye trivialmente) en lugar de invocarstd::_Construct
cada elemento. ComoA
es trivial y se puede asignar copia, en realidad terminaría llamando a esta especialización. Por supuesto, esto tampoco sirvestd::allocator_traits<allocator_type>::construct
.fuente
std::_Construct
llamada por lo que puedo decir, porqueA
es trivialmente copiable y trivialmente copiable asignable, por lo tanto,__uninitialized_default_n_1
se elige la otra especialización de , que llama en sustd::fill_n
lugar, que luego se envía amemcpy
/memset
. Era consciente de esto como una optimización que hace libstdc ++, pero no me di cuenta de que lastd::allocator::construct
llamada también se omite para los tipos no triviales. Por lo tanto, puede ser un descuido en cómo libstdc ++ identifica questd::allocator
se utiliza la biblioteca suministrada .