c++ - Preguntas sobre el repartidor de pila de Hinnant
c++11 memory-alignment (1)
He estado utilizando el asignador de pila de Howard Hinnant y funciona a la perfección, pero algunos detalles de la implementación me resultan poco claros.
Me alegra que haya estado trabajando para ti.
1. ¿Por qué los operadores globales son
new
ydelete
? Las funciones miembro deallocate()
ydeallocate()
usan::operator new
y::operator delete
respectivamente. De manera similar, la función miembroconstruct()
usa la ubicación global new. ¿Por qué no permitir sobrecargas globales definidas por el usuario o específicas de la clase?
No hay ninguna razón particular. Siéntase libre de modificar este código de la manera que mejor le convenga. Esto pretendía ser más un ejemplo, y de ninguna manera es perfecto. Los únicos requisitos son que el asignador y el desasignador suministran memoria correctamente alineada, y que el miembro de la construcción construye un argumento.
En C ++ 11, los miembros de construcción (y destrucción) son opcionales. Le recomendaría que los elimine del asignador si está operando en un entorno que proporciona allocator_traits
. Para averiguarlo, simplemente elimínelos y vea si las cosas aún se compilan.
2. ¿Por qué se establece la alineación en 16 bytes codificados en vez de
std::alignment_of<T>
?
std::alignment_of<T>
probablemente funcionaría bien. Probablemente estaba siendo paranoico ese día.
3. ¿Por qué los constructores y
max_size
tienen una especificación de excepciónthrow()
? ¿No se desanima esto (ver, por ejemplo, el artículo 14 de C ++ más efectivo)? ¿Es realmente necesario terminar y abortar cuando ocurre una excepción en el asignador? ¿Esto cambia con la nueva palabra clavenoexcept
C ++ 11?
Estos miembros simplemente nunca tirarán. Para C ++ 11 debería actualizarlos a noexcept
. En C ++ 11 se vuelve más importante decorar las cosas con noexcept
, especialmente los miembros especiales. En C ++ 11, se puede detectar si una expresión no se muestra o no. El código puede ramificarse dependiendo de esa respuesta. Es más probable que el código que se sabe que no se ve, haga que el código genérico se ramifique a una ruta más eficiente. std::move_if_noexcept
es el ejemplo canónico en C ++ 11.
No uses throw(type1, type2)
nunca. Ha sido desaprobado en C ++ 11.
Use throw()
cuando realmente quiera decir: Esto nunca se lanzará, y si me equivoco, finalice el programa para poder depurarlo. throw()
también está en desuso en C ++ 11, pero tiene un reemplazo noexcept
: noexcept
.
4. La función miembro
construct()
sería un candidato ideal para un reenvío perfecto (al constructor que se está llamando). ¿Es esta la manera de escribir asignadores conformes de C ++ 11?
Sí. Sin embargo allocator_traits
lo hará por ti. Dejarlo. El std :: lib ya ha depurado ese código para usted. Los contenedores C ++ 11 llamarán allocator_traits<YourAllocator>::construct(your_allocator, pointer, args...)
. Si su asignador implementa estas funciones, allocator_traits llamará a su implementación, o llamará a una implementación depurada, eficiente y por defecto.
5. ¿Qué otros cambios son necesarios para que el código actual C ++ 11 sea compatible?
A decir verdad, este asignador no es realmente conforme a C ++ 03 o C ++ 11. Cuando copia un asignador, se supone que el original y la copia son iguales entre sí. En este diseño, eso nunca es cierto. Sin embargo, esta cosa todavía funciona en muchos contextos.
Si desea hacerlo estrictamente conforme, necesita otro nivel de direccionamiento indirecto tal que las copias apunten al mismo búfer.
Aparte de eso, los asignadores de C ++ 11 son mucho más fáciles de construir que los asignadores de C ++ 98/03. Aquí está lo mínimo que debes hacer:
template <class T>
class MyAllocator
{
public:
typedef T value_type;
MyAllocator() noexcept; // only required if used
MyAllocator(const MyAllocator&) noexcept; // copies must be equal
MyAllocator(MyAllocator&&) noexcept; // not needed if copy ctor is good enough
template <class U>
MyAllocator(const MyAllocator<U>& u) noexcept; // requires: *this == MyAllocator(u)
value_type* allocate(std::size_t);
void deallocate(value_type*, std::size_t) noexcept;
};
template <class T, class U>
bool operator==(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;
template <class T, class U>
bool operator!=(const MyAllocator<T>&, const MyAllocator<U>&) noexcept;
Opcionalmente, podría considerar la posibilidad de MyAllocator
y colocar el siguiente tipo anidado en el asignador:
typedef std::true_type propagate_on_container_swap;
Hay algunas otras perillas como las que puedes modificar en los asignadores de C ++ 11. Pero todos los mandos tienen valores predeterminados razonables.
Actualizar
Anteriormente, observo que mi asignador de pila no se ajusta debido al hecho de que las copias no son iguales. Decidí actualizar este asignador a un asignador de C ++ 11 conforme. El nuevo asignador se llama short_allocator y se documenta here .
El short_allocator difiere del asignador de pila en que el búfer "interno" ya no es interno al asignador, sino que ahora es un objeto "arena" separado que se puede ubicar en la pila local, o una duración de almacenamiento estática o de subproceso dada. La arena
no es segura, así que ten cuidado. Podría hacerlo seguro si quisiera, pero eso tiene rendimientos decrecientes (eventualmente reinventará malloc).
Esto es conforme porque las copias de los asignadores apuntan al mismo arena
externo. Tenga en cuenta que la unidad de N
ahora es bytes, no número de T
Uno podría convertir este asignador de C ++ 11 en un asignador de C ++ 98/03 agregando la placa de caldera C ++ 98/03 (los typedefs, el miembro de construcción, el miembro de destrucción, etc.). Una tarea tediosa, pero directa.
Las respuestas a esta pregunta para el nuevo short_allocator permanecen sin cambios.
He estado utilizando el asignador de pila de Howard Hinnant y funciona a la perfección, pero algunos detalles de la implementación me resultan poco claros.
- ¿Por qué los operadores globales son
new
ydelete
? Las funciones miembro deallocate()
ydeallocate()
usan::operator new
y::operator delete
respectivamente. De manera similar, la función miembroconstruct()
usa la ubicación global new. ¿Por qué no permitir sobrecargas globales definidas por el usuario o específicas de la clase? - ¿Por qué la alineación se establece en 16 bytes codificados en duro en lugar de
std::alignment_of<T>
? - ¿Por qué los constructores y
max_size
tienen una especificación de excepciónthrow()
? ¿No se desanima esto (ver, por ejemplo, el artículo 14 de C ++ más efectivo)? ¿Es realmente necesario terminar y abortar cuando ocurre una excepción en el asignador? ¿Esto cambia con la nueva palabra clavenoexcept
C ++ 11? - La función miembro
construct()
sería un candidato ideal para un reenvío perfecto (al constructor que se está llamando). ¿Es esta la manera de escribir asignadores conformes de C ++ 11? - ¿Qué otros cambios son necesarios para que el código actual C ++ 11 sea compatible?