¿Por qué no puedo envolver una T * en un std :: vector ?

Tengo una T* trata un búfer con elementos len de tipo T Necesito estos datos en forma de std::vector , por ciertas razones. Por lo que puedo decir, no puedo construir un vector que use mi búfer como su almacenamiento interno. ¿Porqué es eso?

Notas:

  • Por favor, no sugiera que use iteradores. Sé que esa es la forma de solucionar estos problemas.
  • No me importa que el vector tenga que copiar datos si se redimensiona más tarde.
  • Esta pregunta me desconcierta especialmente ahora que C ++ tiene semántica de movimiento. Si podemos sacar el almacenamiento de un objeto por debajo de sus pies, ¿por qué no poder empujar el nuestro por nuestra cuenta?

Usted puede.

Escribe sobre std::vector , pero std::vector toma dos argumentos de plantilla, no solo uno. El segundo argumento de la plantilla especifica el tipo de asignador a usar, y los constructores de vector tienen sobrecargas que permiten pasar una instancia personalizada de ese tipo de asignador.

Así que todo lo que necesita hacer es escribir un asignador que use su propio búfer interno cuando sea posible, y volver a preguntar al asignador predeterminado cuando su propio búfer interno esté lleno.

El asignador predeterminado no puede aspirar a manejarlo, ya que no tendría idea de qué bits de memoria se pueden liberar y cuáles no.


Un asignador con estado de muestra con un búfer interno que contiene elementos ya construidos que el vector no debe sobrescribir, incluida una demostración de un gran Gotcha:

 struct my_allocator_state { void *buf; std::size_t len; bool bufused; const std::type_info *type; }; template  struct my_allocator { typedef T value_type; my_allocator(T *buf, std::size_t len) : def(), state(std::make_shared({ buf, len, false, &typeid(T) })) { } template  my_allocator(T(&buf)[N]) : def(), state(std::make_shared({ buf, N, false, &typeid(T) })) { } template  friend struct my_allocator; template  my_allocator(my_allocator other) : def(), state(other.state) { } T *allocate(std::size_t n) { if (!state->bufused && n == state->len && typeid(T) == *state->type) { state->bufused = true; return static_cast(state->buf); } else return def.allocate(n); } void deallocate(T *p, std::size_t n) { if (p == state->buf) state->bufused = false; else def.deallocate(p, n); } template  void construct(T *c, Args... args) { if (!in_buffer(c)) def.construct(c, std::forward(args)...); } void destroy(T *c) { if (!in_buffer(c)) def.destroy(c); } friend bool operator==(const my_allocator &a, const my_allocator &b) { return a.state == b.state; } friend bool operator!=(const my_allocator &a, const my_allocator &b) { return a.state != b.state; } private: std::allocator def; std::shared_ptr state; bool in_buffer(T *p) { return *state->type == typeid(T) && points_into_buffer(p, static_cast(state->buf), state->len); } }; int main() { int buf [] = { 1, 2, 3, 4 }; std::vector> v(sizeof buf / sizeof *buf, {}, buf); v.resize(3); v.push_back(5); v.push_back(6); for (auto &i : v) std::cout << i << std::endl; } 

Salida:

 1
 2
 3
 4
 6

El push_back de 5 encaja en el búfer anterior, por lo que se omite la construcción. Cuando se agrega 6 , se asigna una nueva memoria y todo comienza a funcionar normalmente. Podría evitar ese problema agregando un método a su asignador para indicar que a partir de ese momento en adelante, la construcción ya no debe ser anulada.

points_into_buffer resultó ser la parte más difícil de escribir, y omití eso en mi respuesta. La semántica intencionada debería ser obvia de cómo lo estoy usando. Por favor vea mi pregunta aquí para una implementación portátil en mi respuesta, o si su implementación lo permite, use una de las versiones más simples en esa otra pregunta.

Por cierto, no estoy realmente contento con el modo en que algunas implementaciones utilizan la rebind de tal manera que no se evite almacenar información de tipo de tiempo de ejecución junto con el estado, pero si su implementación no lo necesita, podría hacerlo una un poco más simple al hacer que el estado sea una clase de plantilla (o una clase anidada) también.

La respuesta corta es que un vector no puede usar su búfer porque no fue diseñado de esa manera.

Tiene sentido, también. Si un vector no asigna su propia memoria, ¿cómo cambia el tamaño del búfer cuando se agregan más elementos? Asigna un nuevo búfer, pero ¿qué hace con el anterior? Lo mismo se aplica al movimiento: si el vector no controla su propio búfer, ¿cómo puede dar el control de este búfer a otra instancia?

En estos días, ya no es necesario, puede envolver el T* con un std::span (en C ++ 20; antes de eso, use gsl::span ). Un tramo le ofrece toda la comodidad de un contenedor de biblioteca estándar, de hecho, básicamente todas las características relevantes de std::vector excluyendo el cambio de tamaño, con una clase de envoltura muy delgada. Eso es lo que quieres usar, de verdad.

Para más información sobre los intervalos , lea: ¿Qué es un “intervalo” y cuándo debo usar uno?