Usando std :: vector como vista en memoria sin procesar

71

Estoy usando una biblioteca externa que en algún momento me da un puntero en bruto a una matriz de enteros y un tamaño.

Ahora me gustaría utilizar std::vectorpara acceder y modificar estos valores en su lugar, en lugar de acceder a ellos con punteros sin formato.

Aquí hay un ejemplo articular que explica el punto:

size_t size = 0;
int * data = get_data_from_library(size);   // raw data from library {5,3,2,1,4}, size gets filled in

std::vector<int> v = ????;                  // pseudo vector to be used to access the raw data

std::sort(v.begin(), v.end());              // sort raw data in place

for (int i = 0; i < 5; i++)
{
  std::cout << data[i] << "\n";             // display sorted raw data 
}

Rendimiento esperado:

1
2
3
4
5

La razón es que necesito aplicar algoritmos de <algorithm>(clasificación, intercambio de elementos, etc.) en esos datos.

Por otro lado cambiar el tamaño de ese vector no sería cambiado, por lo que push_back, erase, insertno están obligados a trabajar en ese vector.

Podría construir un vector basado en los datos de la biblioteca, usar modificar ese vector y copiar los datos de nuevo a la biblioteca, pero serían dos copias completas que me gustaría evitar ya que el conjunto de datos podría ser realmente grande.

Jabberwocky
fuente
16
Lo que estás buscando es un hipotético std::vector_view, ¿no?
眠 り ネ ロ ク
3
@ 眠 り ネ ロ ク sí, probablemente
Jabberwocky
55
Así no es como std::vectorfunciona.
Jesper Juhl
34
Los algoritmos estándar funcionan en iteradores, y los punteros son iteradores. No hay nada que te impida hacer sort(arrayPointer, arrayPointer + elementCount);.
cmaster - reinstalar a monica

Respuestas:

60

El problema es que std::vectortiene que hacer una copia de los elementos de la matriz con la que lo inicializa, ya que tiene la propiedad de los objetos que contiene.

Para evitar esto, puede usar un objeto de división para una matriz (es decir, similar a lo que std::string_viewes std::string). Puede escribir su propia array_viewimplementación de plantilla de clase cuyas instancias se construyen tomando un puntero sin procesar al primer elemento de una matriz y la longitud de la matriz:

#include <cstdint>

template<typename T>
class array_view {
   T* ptr_;
   std::size_t len_;
public:
   array_view(T* ptr, std::size_t len) noexcept: ptr_{ptr}, len_{len} {}

   T& operator[](int i) noexcept { return ptr_[i]; }
   T const& operator[](int i) const noexcept { return ptr_[i]; }
   auto size() const noexcept { return len_; }

   auto begin() noexcept { return ptr_; }
   auto end() noexcept { return ptr_ + len_; }
};

array_viewno almacena una matriz; solo tiene un puntero al comienzo de la matriz y la longitud de esa matriz. Por lo tanto, los array_viewobjetos son baratos de construir y copiar.

Dado que array_viewproporciona las begin()y end()miembros de funciones, se pueden utilizar los algoritmos de la biblioteca estándar (por ejemplo, std::sort, std::find, std::lower_bound, etc.) sobre el mismo:

#define LEN 5

auto main() -> int {
   int arr[LEN] = {4, 5, 1, 2, 3};

   array_view<int> av(arr, LEN);

   std::sort(av.begin(), av.end());

   for (auto const& val: av)
      std::cout << val << ' ';
   std::cout << '\n';
}

Salida:

1 2 3 4 5

Use std::span(o gsl::span) en su lugar

La implementación anterior expone el concepto detrás de los objetos de corte . Sin embargo, desde C ++ 20 puede usar directamente en su std::spanlugar. En cualquier caso, puedes usarlo gsl::spandesde C ++ 14.

眠 り ネ ロ ク
fuente
¿Por qué marcaste los métodos como noexcept? No puede garantizar que no se produzca ninguna excepción, ¿verdad?
SonneXo
@moooeeeep Es mejor dejar alguna explicación que solo un enlace. El enlace podría expirar en el futuro mientras he visto que esto sucedió mucho.
Jason Liu
63

C ++ 20's std::span

Si puede usar C ++ 20, puede usar std::spanun par de longitud de puntero que le da al usuario una vista de una secuencia contigua de elementos. Es una especie de std::string_view, y mientras tanto std::spany std::string_viewson vistas no propietario, std::string_viewes una vista de sólo lectura.

De los documentos:

El intervalo de plantilla de clase describe un objeto que puede referirse a una secuencia contigua de objetos con el primer elemento de la secuencia en la posición cero. Un intervalo puede tener una extensión estática, en cuyo caso el número de elementos en la secuencia es conocido y codificado en el tipo, o una extensión dinámica.

Entonces lo siguiente funcionaría:

#include <span>
#include <iostream>
#include <algorithm>

int main() {
    int data[] = { 5, 3, 2, 1, 4 };
    std::span<int> s{data, 5};

    std::sort(s.begin(), s.end());

    for (auto const i : s) {
        std::cout << i << "\n";
    }

    return 0;
}

Compruébalo en vivo

Dado que std::spanes básicamente un par de puntero - longitud, también puede usarlo de la siguiente manera:

size_t size = 0;
int *data = get_data_from_library(size);
std::span<int> s{data, size};

Nota: No todos los compiladores son compatibles std::span. Verifique el soporte del compilador aquí .

ACTUALIZAR

Si no puede usar C ++ 20, podría usar gsl::spanbásicamente la versión base de los estándares de C ++ std::span.

Solución C ++ 11

Si está limitado al estándar C ++ 11, puede intentar implementar su propia spanclase simple :

template<typename T>
class span {
   T* ptr_;
   std::size_t len_;

public:
    span(T* ptr, std::size_t len) noexcept
        : ptr_{ptr}, len_{len}
    {}

    T& operator[](int i) noexcept {
        return *ptr_[i];
    }

    T const& operator[](int i) const noexcept {
        return *ptr_[i];
    }

    std::size_t size() const noexcept {
        return len_;
    }

    T* begin() noexcept {
        return ptr_;
    }

    T* end() noexcept {
        return ptr_ + len_;
    }
};

Echa un vistazo a la versión C ++ 11 en vivo

Cascanueces
fuente
44
Podría usarlo gsl::spanpara C ++ 14 y superior si su compilador no se implementastd::span
Artyer
2
@Artyer Actualizaré mi respuesta con esto. Gracias
NutCracker
29

Dado que la biblioteca de algoritmos funciona con iteradores, puede mantener la matriz.

Para punteros y longitud de matriz conocida

Aquí puede usar punteros sin formato como iteradores. Admiten todas las operaciones que admite un iterador (incremento, comparación para igualdad, valor de, etc.):

#include <iostream>
#include <algorithm>

int *get_data_from_library(int &size) {
    static int data[] = {5,3,2,1,4}; 

    size = 5;

    return data;
}


int main()
{
    int size;
    int *data = get_data_from_library(size);

    std::sort(data, data + size);

    for (int i = 0; i < size; i++)
    {
        std::cout << data[i] << "\n";
    }
}

dataseñala al miembro de la matriz dirst como un iterador devuelto por begin()y data + sizeapunta al elemento después del último elemento de la matriz como un iterador devuelto por end().

Para matrices

Aquí puedes usar std::begin()ystd::end()

#include <iostream>
#include <algorithm>

int main()
{
    int data[] = {5,3,2,1,4};         // raw data from library

    std::sort(std::begin(data), std::end(data));    // sort raw data in place

    for (int i = 0; i < 5; i++)
    {
        std::cout << data[i] << "\n";   // display sorted raw data 
    }
}

Pero tenga en cuenta que esto solo funciona, si datano se descompone en un puntero, porque la información de longitud se pierde.

churill
fuente
77
Esta es la respuesta correcta. Los algoritmos se aplican a los rangos . Los contenedores (p. Ej., Std :: vector) son una forma de administrar rangos, pero no son la única.
Pete Becker
13

Puede obtener iteradores en matrices sin procesar y usarlos en algoritmos:

    int data[] = {5,3,2,1,4};
    std::sort(std::begin(data), std::end(data));
    for (auto i : data) {
        std::cout << i << std::endl;
    }

Si está trabajando con punteros sin formato (ptr + tamaño), puede usar la siguiente técnica:

    size_t size = 0;
    int * data = get_data_from_library(size);
    auto b = data;
    auto e = b + size;
    std::sort(b, e);
    for (auto it = b; it != e; ++it) {
        cout << *it << endl;
    }

UPD: Sin embargo, el ejemplo anterior es de mal diseño. La biblioteca nos devuelve un puntero sin procesar y no sabemos dónde está asignado el búfer subyacente y quién se supone que lo liberará.

Por lo general, la persona que llama proporciona un búfer para que la función complete los datos. En ese caso, podemos preasignar el vector y usar su búfer subyacente:

    std::vector<int> v;
    v.resize(256); // allocate a buffer for 256 integers
    size_t size = get_data_from_library(v.data(), v.size());
    // shrink down to actual data. Note that no memory realocations or copy is done here.
    v.resize(size);
    std::sort(v.begin(), v.end());
    for (auto i : v) {
        cout << i << endl;
    }

Al usar C ++ 11 o superior, incluso podemos hacer que get_data_from_library () devuelva un vector. Gracias a las operaciones de movimiento, no habrá copia de memoria.

PooSH
fuente
2
Luego puede usar punteros regulares como iteradores:auto begin = data; auto end = data + size;
PooSH
Sin embargo, la pregunta es ¿dónde get_data_from_library()se asignan los datos devueltos por ? Tal vez no debemos cambiarlo en absoluto. Si necesitamos pasar un búfer a la biblioteca, entonces podemos asignar el vector y pasarv.data()
PooSH
1
@PooSH los datos son propiedad de la biblioteca, pero se pueden cambiar sin restricciones (ese es realmente el punto de toda la pregunta). Solo el tamaño de los datos no se puede cambiar.
Jabberwocky
1
@Jabberwocky agregó un mejor ejemplo de cómo usar el búfer subyacente del vector para completar los datos.
PooSH
9

No puede hacer esto con un std::vectorsin hacer una copia. std::vectorposee el puntero que tiene debajo del capó y asigna espacio a través del asignador que se proporciona.

Si tiene acceso a un compilador que tiene soporte para C ++ 20, puede usar std :: span que fue construido exactamente para este propósito. Envuelve un puntero y un tamaño en un "contenedor" que tiene la interfaz de contenedor C ++.

Si no, puede usar gsl :: span, que es en lo que se basó la versión estándar.

Si no desea importar otra biblioteca, puede implementarla trivialmente usted mismo dependiendo de qué funcionalidad quiera tener.

NathanOliver
fuente
9

Ahora me gustaría usar std :: vector para acceder y modificar estos valores en su lugar

No se puede. Eso no std::vectores para lo que sirve. std::vectorgestiona su propio búfer, que siempre se adquiere de un asignador. Nunca toma posesión de otro búfer (excepto de otro vector del mismo tipo).

Por otro lado, tampoco es necesario porque ...

La razón es que necesito aplicar algoritmos de (clasificación, intercambio de elementos, etc.) en esos datos.

Esos algoritmos funcionan en iteradores. Un puntero es un iterador de una matriz. No necesitas un vector:

std::sort(data, data + size);

A diferencia de las plantillas de función en <algorithm>, algunas herramientas como el rango para std::begin/ / std::endy los rangos C ++ 20 no funcionan con solo un par de iteradores, aunque sí funcionan con contenedores como los vectores. Es posible crear una clase de contenedor para iterador + tamaño que se comporte como un rango y funcione con estas herramientas. C ++ 20 introducirá tales envoltura en la biblioteca estándar: std::span.

eerorika
fuente
7

Además de la otra buena sugerencia sobre std::spanvenir en e gsl:spanincluir su propia spanclase (ligera) hasta entonces ya es bastante fácil (siéntase libre de copiar):

template<class T>
struct span {
    T* first;
    size_t length;
    span(T* first_, size_t length_) : first(first_), length(length_) {};
    using value_type = std::remove_cv_t<T>;//primarily needed if used with templates
    bool empty() const { return length == 0; }
    auto begin() const { return first; }
    auto end() const { return first + length; }
};

static_assert(_MSVC_LANG <= 201703L, "remember to switch to std::span");

De especial interés es también la biblioteca boost range si está interesado en el concepto de rango más genérico: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

Los conceptos de rango también llegarán en

Darune
fuente
1
¿Qué es la using value_type = std::remove_cv_t<T>;de?
Jabberwocky
1
... y se le olvidó el constructor: span(T* first_, size_t length) : first(first), length(length) {};. Edité tu respuesta.
Jabberwocky
@Jabberwocky Acabo de usar la inicialización agregada. Pero el constructor está bien.
Darune
1
@eerorika supongo que tienes razón,
eliminé las
1
Se using value_type = std::remove_cv_t<T>;necesita principalmente si se usa con la programación de plantillas (para obtener el value_type de un 'rango'). Si solo desea utilizar los iteradores, puede omitir / eliminar eso.
Darune
6

En realidad casi podrías usarstd::vector esto, abusando de la funcionalidad del asignador personalizado para devolver un puntero a la memoria que desea ver. Eso no estaría garantizado por el estándar para trabajar (relleno, alineación, inicialización de los valores devueltos; tendrías que esforzarte al asignar el tamaño inicial, y para los no primitivos también necesitarías hackear tus constructores ), pero en la práctica esperaría que le dieran suficientes ajustes.

Nunca jamás hagas eso. Es feo, sorprendente, hacky e innecesario. Los algoritmos de la biblioteca estándar ya están diseñados para funcionar tan bien con matrices sin procesar como con vectores. Vea las otras respuestas para detalles de eso.

Sneftel
fuente
1
Hmm, sí, eso podría funcionar con los vectorconstructores que toman una referencia de asignación personalizada como un constructor arg (no solo un parámetro de plantilla). Supongo que necesitaría un objeto asignador que tuviera el valor del puntero en tiempo de ejecución, no como un parámetro de plantilla; de lo contrario, solo podría funcionar para direcciones constexpr. Tendría que tener cuidado de no permitir que vectorlos objetos de construcción predeterminados .resize()ni sobrescriban los datos existentes; el desajuste entre la posesión de un recipiente como vector frente a no poseer palmo es enorme si se inicia mediante .push_back etc
Peter Cordes
1
@PeterCordes quiero decir, no vamos a enterrar a los Lede - que le también tiene que estar loco. En mi opinión, lo más extraño de la idea es que la interfaz del asignador incluye el constructmétodo que se requeriría ... No puedo pensar qué casos de uso no hacky requerirían que la colocación fuera nueva.
Sneftel
1
El caso de uso obvio es evitar perder el tiempo construyendo elementos que está a punto de escribir de otra manera, por ejemplo, resize()antes de pasar una referencia a algo que quiere usarlo como una salida pura (por ejemplo, una llamada al sistema de lectura). En la práctica, los compiladores a menudo no optimizan ese memset o lo que sea. O si tuviera un asignador que usa calloc para obtener memoria pre-puesta a cero, también podría evitar ensuciarlo de la manera estúpida std::vector<int>por defecto cuando construye objetos por defecto que tienen un patrón de bits cero. Vea las notas en es.cppreference.com/w/cpp/container/vector/vector
Peter Cordes
4

Como otros han señalado, std::vector debe ser dueño de la memoria subyacente (menos que jugar con un asignador personalizado) para que no se pueda usar.

Otros también han recomendado la duración de c ++ 20, sin embargo, obviamente eso requiere c ++ 20.

Recomendaría el span-lite span. Para citar es subtítulo:

span lite: un intervalo similar a C ++ 20 para C ++ 98, C ++ 11 y posterior en una biblioteca de solo encabezado de archivo único

Proporciona una vista no propietaria y mutable (ya que puede mutar elementos y su orden pero no insertarlos) y, como dice la cita, no tiene dependencias y funciona en la mayoría de los compiladores.

Su ejemplo:

#include <algorithm>
#include <cstddef>
#include <iostream>

#include <nonstd/span.hpp>

static int data[] = {5, 1, 2, 4, 3};

// For example
int* get_data_from_library()
{
  return data;
}

int main ()
{
  const std::size_t size = 5;

  nonstd::span<int> v{get_data_from_library(), size};

  std::sort(v.begin(), v.end());

  for (auto i = 0UL; i < v.size(); ++i)
  {
    std::cout << v[i] << "\n";
  }
}

Huellas dactilares

1
2
3
4
5

Esto también tiene la ventaja adicional si un día cambias a c ++ 20, solo deberías poder reemplazarlo nonstd::spanpor std::span.

Arghnews
fuente
3

Puede usar un std::reference_wrapperdisponible desde C ++ 11:

#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>

int main()
{
    int src_table[] = {5, 4, 3, 2, 1, 0};

    std::vector< std::reference_wrapper< int > > dest_vector;

    std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));
    // if you don't have the array defined just a pointer and size then:
    // std::copy(src_table_ptr, src_table_ptr + size, std::back_inserter(dest_vector));

    std::sort(std::begin(dest_vector), std::end(dest_vector));

    std::for_each(std::begin(src_table), std::end(src_table), [](int x) { std::cout << x << '\n'; });
    std::for_each(std::begin(dest_vector), std::end(dest_vector), [](int x) { std::cout << x << '\n'; });
}
Robert Andrzejuk
fuente
2
Esto realiza una copia de los datos, y eso es precisamente lo que quiero evitar.
Jabberwocky
1
@Jabberwocky Esto no copia los datos. Pero tampoco es lo que pediste en la pregunta.
Eerorika
@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));definitivamente llena los dest_vectorvalores tomados de src_table(IOW se copian los datos dest_vector), por lo que no recibí tu comentario. ¿Podrías explicar?
Jabberwocky
@Jabberwocky no copia valores. Llena el vector con envoltorios de referencia.
Eerorika
3
@Jabberwocky es más ineficiente en caso de valores enteros.
Eerorika