Usando std :: vector come vista sulla memoria grezza


71

Sto usando una libreria esterna che ad un certo punto mi dà un puntatore non elaborato a una matrice di numeri interi e dimensioni.

Ora vorrei utilizzare std::vectorper accedere e modificare questi valori sul posto, anziché accedervi con puntatori non elaborati.

Ecco un esempio articolare che spiega il 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 
}

Uscita prevista:

1
2
3
4
5

Il motivo è che devo applicare algoritmi da <algorithm>(ordinamento, scambio di elementi, ecc.) Su quei dati.

D'altra parte la modifica della dimensione di quel vettore non sarebbe mai essere cambiato, così push_back, erase, insertnon sono tenuti a lavorare su quel vettore.

Potrei costruire un vettore basato sui dati della libreria, usare modificare quel vettore e copiare i dati nella libreria, ma sarebbero due copie complete che vorrei evitare poiché il set di dati potrebbe essere davvero grande.


16
Quello che stai cercando è un ipotetico std::vector_view, vero?
眠 り ネ ロ ク

3
@ 眠 り ネ ロ ク sì, probabilmente
Jabberwocky,

5
Non std::vectorfunziona così .
Jesper Juhl,


34
Gli algoritmi standard funzionano sugli iteratori e i puntatori sono iteratori. Non c'è niente che ti impedisce di fare sort(arrayPointer, arrayPointer + elementCount);.
cmaster - ripristina monica il

Risposte:


60

Il problema è che std::vectordeve creare una copia degli elementi dall'array con cui lo inizializzi in quanto ha la proprietà degli oggetti che contiene.

Per evitare ciò, è possibile utilizzare un oggetto slice per un array (cioè simile a ciò che std::string_viewè std::string). È possibile scrivere la propria array_viewimplementazione del modello di classe le cui istanze sono costruite prendendo un puntatore non elaborato sul primo elemento di una matrice e sulla lunghezza della matrice:

#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_viewnon memorizza un array; contiene solo un puntatore all'inizio della matrice e la lunghezza della matrice. Pertanto, gli array_viewoggetti sono economici da costruire e da copiare.

Dal momento che array_viewfornisce le begin()e end()membri funzioni, è possibile utilizzare gli algoritmi della libreria standard (ad esempio, std::sort, std::find, std::lower_bound, ecc) su di esso:

#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';
}

Produzione:

1 2 3 4 5

Utilizzare std::span(o gsl::span) invece

L'implementazione sopra esposta espone il concetto alla base degli oggetti slice . Tuttavia, dal C ++ 20 è possibile utilizzare direttamente std::spaninvece. In ogni caso, puoi usarlo gsl::spandal C ++ 14.


Perché hai contrassegnato i metodi come noexcept? Non puoi assolutamente garantire che non venga generata alcuna eccezione, vero?
SonneXo


@moooeeeep È meglio lasciare qualche spiegazione piuttosto che un semplice link. Il link potrebbe essere scaduto in futuro mentre ho visto che è successo molto.
Jason Liu il

63

C ++ '20 std::span

Se si è in grado di utilizzare C ++ 20, è possibile utilizzare std::spanuna coppia lunghezza puntatore che offre all'utente una vista in una sequenza contigua di elementi. È una sorta di a std::string_view, e sebbene entrambe std::spane std::string_viewsiano viste non proprietarie, std::string_viewè una vista di sola lettura.

Dai documenti:

L'intervallo del modello di classe descrive un oggetto che può fare riferimento a una sequenza contigua di oggetti con il primo elemento della sequenza nella posizione zero. Un intervallo può avere un'estensione statica, nel qual caso il numero di elementi nella sequenza è noto e codificato nel tipo o un'estensione dinamica.

Quindi il seguente funzionerebbe:

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

Dai un'occhiata dal vivo

Poiché std::spanè sostanzialmente una coppia lunghezza puntatore, puoi usare anche in questo modo:

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

Nota: non tutti i compilatori supportano std::span. Controlla qui il supporto del compilatore .

AGGIORNARE

Se non si è in grado di utilizzare C ++ 20, è possibile utilizzare gsl::spanla versione di base dello standard C ++ std::span.

Soluzione C ++ 11

Se sei limitato allo standard C ++ 11, puoi provare a implementare la tua spanclasse semplice :

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_;
    }
};

Guarda la versione C ++ 11 dal vivo


4
È possibile utilizzare gsl::spanper C ++ 14 e versioni successive se il compilatore non implementastd::span
Artyer

2
@Artyer Aggiornerò la mia risposta con questo. Grazie
Schiaccianoci

29

Poiché la libreria algoritmo funziona con iteratori, è possibile mantenere l'array.

Per puntatori e lunghezza dell'array nota

Qui puoi usare i puntatori non elaborati come iteratori. Supportano tutte le operazioni supportate da un iteratore (incremento, confronto per uguaglianza, valore di, ecc ...):

#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";
    }
}

datapunta al membro dell'array dirst come un iteratore restituito da begin()e data + sizepunta all'elemento dopo l'ultimo elemento dell'array come un iteratore restituito da end().

Per array

Qui puoi usare std::begin()estd::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 
    }
}

Ma tieni presente che questo funziona solo, se datanon decade in un puntatore, perché in questo caso le informazioni sulla lunghezza scompaiono.


7
Questa è la risposta esatta. Gli algoritmi si applicano agli intervalli . I contenitori (ad es. Std :: vector) sono un modo per gestire gli intervalli, ma non sono l'unico modo.
Pete Becker,

13

È possibile ottenere iteratori su array non elaborati e utilizzarli negli algoritmi:

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

Se stai lavorando con puntatori non elaborati (ptr + dimensione), puoi utilizzare la seguente tecnica:

    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: Tuttavia, l'esempio sopra è di cattiva progettazione. La libreria ci restituisce un puntatore non elaborato e non sappiamo dove sia allocato il buffer sottostante e chi dovrebbe liberarlo.

Di solito, il chiamante fornisce un buffer per la funzione per riempire i dati. In tal caso, possiamo preallocare il vettore e utilizzare il suo buffer sottostante:

    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;
    }

Quando si utilizza C ++ 11 o versioni successive, è anche possibile creare get_data_from_library () per restituire un vettore. Grazie alle operazioni di spostamento, non ci sarà alcuna copia di memoria.


2
Quindi è possibile utilizzare puntatori regolari come iteratori:auto begin = data; auto end = data + size;
PooSH

Tuttavia, la domanda è: dove vengono get_data_from_library()allocati i dati restituiti da ? Forse non dovremmo cambiarlo affatto. Se abbiamo bisogno di passare un buffer alla libreria, allora possiamo allocare vector e passv.data()
PooSH

1
@PooSH i dati sono di proprietà della libreria, ma possono essere modificati senza restrizioni (questo è in realtà il punto dell'intera domanda). Non è possibile modificare solo la dimensione dei dati.
Jabberwocky,

1
@Jabberwocky ha aggiunto un esempio migliore di come utilizzare il buffer sottostante del vettore per compilare i dati.
PooSH

9

Non puoi farlo con a std::vectorsenza fare una copia. std::vectorpossiede il puntatore che ha sotto il cofano e alloca lo spazio attraverso l'allocatore che viene fornito.

Se si ha accesso a un compilatore che supporta C ++ 20, è possibile utilizzare std :: span che è stato creato proprio per questo scopo. Avvolge un puntatore e le dimensioni in un "contenitore" che ha l'interfaccia del contenitore C ++.

Altrimenti, puoi usare gsl :: span che è alla base della versione standard.

Se non si desidera importare un'altra libreria, è possibile implementarlo banalmente da soli a seconda di tutte le funzionalità che si desidera avere.


9

Ora vorrei usare std :: vector per accedere e modificare questi valori sul posto

Non puoi. Non è quello che std::vectorserve. std::vectorgestisce il proprio buffer, che viene sempre acquisito da un allocatore. Non prende mai la proprietà di un altro buffer (tranne che per un altro vettore dello stesso tipo).

D'altra parte, non è nemmeno necessario perché ...

Il motivo è che devo applicare algoritmi da (ordinamento, scambio di elementi, ecc.) Su quei dati.

Tali algoritmi funzionano su iteratori. Un puntatore è un iteratore di un array. Non hai bisogno di un vettore:

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

A differenza dei modelli di funzione in <algorithm>, alcuni strumenti come range-for, std::begin/ std::ende C ++ 20, tuttavia, non funzionano solo con una coppia di iteratori, mentre funzionano con contenitori come i vettori. È possibile creare una classe wrapper per iteratore + dimensione che si comporta come un intervallo e funziona con questi strumenti. C ++ 20 introdurrà tale involucro nella libreria standard: std::span.


7

Oltre all'altro buon suggerimento su come std::spanentrare in e gsl:span, inclusa la tua spanclasse (leggera) fino ad allora, è già abbastanza facile (sentiti libero di copiare):

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");

Di particolare nota è anche la libreria se sei interessato al concetto di range più generico: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

I concetti di portata arriveranno anche in


1
A cosa serve using value_type = std::remove_cv_t<T>;?
Jabberwocky,

1
... e ti sei dimenticato il costruttore: span(T* first_, size_t length) : first(first), length(length) {};. Ho modificato la tua risposta.
Jabberwocky,

@Jabberwocky Ho appena usato l'inizializzazione aggregata. Ma il costruttore va bene.
martedì

1
@eerorika credo che tu abbia ragione, ho rimosso le versioni non const
martedì

1
È using value_type = std::remove_cv_t<T>;necessario principalmente se utilizzato con la programmazione del modello (per ottenere il valore_tipo di un 'intervallo'). Se vuoi solo usare gli iteratori puoi saltarlo / rimuoverlo.
martedì

6

In realtà potrebbe quasi usarlo std::vector, abusando della funzionalità di allocazione personalizzata per restituire un puntatore alla memoria che desideri visualizzare. Ciò non sarebbe garantito dallo standard per funzionare (imbottitura, allineamento, inizializzazione dei valori restituiti; dovresti fare attenzione quando assegni le dimensioni iniziali e per i non primitivi dovresti anche hackerare i tuoi costruttori ), ma in pratica mi aspetterei che le modifiche fossero sufficienti.

Mai e poi mai. È brutto, sorprendente, confuso e non necessario. Gli algoritmi della libreria standard sono già progettati per funzionare sia con array grezzi che con vettori. Vedi le altre risposte per i dettagli.


1
Hmm, sì, potrebbe funzionare con i vectorcostruttori che accettano un riferimento Allocator personalizzato come arg costruttore (non solo un parametro template). Immagino che avresti bisogno di un oggetto allocatore che contenesse il valore del puntatore di runtime, non come parametro del modello, altrimenti potrebbe funzionare solo per indirizzi constexpr. Dovresti stare attento a non lasciare vectoroggetti predefiniti .resize()e sovrascrivere i dati esistenti; la discrepanza tra un contenitore proprietario come vettore o intervallo non proprietario è enorme se si inizia a utilizzare .push_back ecc.
Peter Cordes,

1
@PeterCordes Voglio dire, non seppelliamo il lede - dovresti anche essere pazzo. A mio avviso, la cosa più strana dell'idea è che l'interfaccia dell'allocatore includa il constructmetodo che sarebbe richiesto ... Non riesco a pensare quali casi d'uso non confusi richiederebbero un posizionamento superiore.
Sneftel,

1
Il caso d'uso ovvio è quello di evitare di perdere tempo a costruire elementi che stai per scrivere in un altro modo, ad esempio resize()prima di passare un riferimento a qualcosa che vuole usarlo come output puro (ad esempio una chiamata di sistema di lettura). In pratica i compilatori spesso non ottimizzano quel memset o altro. Oppure, se disponevi di un allocatore che utilizza calloc per ottenere memoria pre-azzerata, puoi anche evitare di sporcarlo come std::vector<int>fa di default lo stupido quando costruisci oggetti di default con un modello di bit tutto zero. Vedi le note in en.cppreference.com/w/cpp/container/vector/vector
Peter Cordes

4

Come altri hanno sottolineato, std::vector deve possedere la memoria sottostante (a corto di pasticciare con un allocatore personalizzato), quindi non può essere utilizzata.

Altri hanno anche raccomandato l'intervallo di c ++ 20, tuttavia ovviamente ciò richiede c ++ 20.

Consiglierei l' intervallo di span-lite . Per citare i sottotitoli:

span lite - Un intervallo simile a C ++ 20 per C ++ 98, C ++ 11 e versioni successive in una libreria di sola intestazione di file singolo

Fornisce una vista non proprietaria e mutevole (come in te puoi mutare elementi e il loro ordine ma non inserirli) e come dice la citazione non ha dipendenze e funziona sulla maggior parte dei compilatori.

Il tuo esempio:

#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";
  }
}

stampe

1
2
3
4
5

Questo ha anche il vantaggio aggiunto se un giorno passerai a c ++ 20, dovresti essere in grado di sostituirlo nonstd::spancon std::span.


3

È possibile utilizzare un std::reference_wrapperdisponibile da 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'; });
}

2
Questo esegue una copia dei dati, ed è esattamente ciò che voglio evitare.
Jabberwocky,

1
@Jabberwocky Questo non copia i dati. Ma non è nemmeno quello che hai chiesto nella domanda.
Eerorika,

@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));riempie definitivamente dest_vectori valori presi da src_table(IOW i dati vengono copiati dest_vector), quindi non ho ricevuto il tuo commento. Potresti spiegare?
Jabberwocky,

@Jabberwocky non copia i valori. Riempie il vettore con wrapper di riferimento.
eerorika,

3
@Jabberwocky è più inefficiente in caso di valori interi.
eerorika,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.