Menggunakan std :: vector sebagai view on ke memori mentah

71

Saya menggunakan perpustakaan eksternal yang pada beberapa titik memberi saya pointer mentah ke array bilangan bulat dan ukuran.

Sekarang saya ingin menggunakan std::vectoruntuk mengakses dan memodifikasi nilai-nilai ini di tempat, daripada mengaksesnya dengan pointer mentah.

Berikut adalah contoh artikifial yang menjelaskan intinya:

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 
}

Output yang diharapkan:

1
2
3
4
5

Alasannya adalah bahwa saya perlu menerapkan algoritma dari <algorithm>(menyortir, menukar elemen dll) pada data itu.

Di sisi lain mengubah ukuran vektor yang tidak akan pernah berubah, jadi push_back, erase, inserttidak diharuskan untuk bekerja pada vektor itu.

Saya bisa membuat vektor berdasarkan data dari perpustakaan, gunakan memodifikasi vektor itu dan menyalin data kembali ke perpustakaan, tapi itu akan menjadi dua salinan lengkap yang ingin saya hindari karena kumpulan data bisa sangat besar.

Jabberwocky
sumber
16
Apa yang Anda cari adalah hipotesis std::vector_view, bukan?
眠 り ネ ロ ク
3
@ 眠 り ネ ロ ク ya, mungkin
Jabberwocky
5
Itu bukan cara std::vectorkerjanya.
Jesper Juhl
34
Algoritma standar bekerja pada iterator, dan pointer adalah iterator. Tidak ada yang menghentikan Anda dari melakukan sort(arrayPointer, arrayPointer + elementCount);.
cmaster - mengembalikan monica

Jawaban:

60

Masalahnya adalah bahwa std::vectorharus membuat salinan elemen dari array yang Anda inisialisasi karena memiliki kepemilikan objek yang dikandungnya.

Untuk menghindari ini, Anda bisa menggunakan objek slice untuk array (yaitu, mirip dengan apa std::string_viewyang harus std::string). Anda bisa menulis array_viewimplementasi templat kelas Anda sendiri yang instansinya dibangun dengan mengambil pointer mentah ke elemen pertama array dan panjang array:

#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_viewtidak menyimpan array; itu hanya memegang pointer ke awal array dan panjang array itu. Oleh karena itu, array_viewobjek murah untuk dikonstruksi dan disalin.

Karena array_viewmenyediakan begin()dan end()anggota fungsi, Anda dapat menggunakan algoritma standar perpustakaan (misalnya, std::sort, std::find, std::lower_bound, dll) di atasnya:

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

Keluaran:

1 2 3 4 5

Gunakan std::span(atau gsl::span) sebagai gantinya

Implementasi di atas memperlihatkan konsep di balik objek slice . Namun, karena C ++ 20 Anda dapat langsung menggunakannya std::span. Bagaimanapun, Anda dapat menggunakan gsl::spansejak C ++ 14.

眠 り ネ ロ ク
sumber
Mengapa Anda menandai metode ini sebagai pengecualian? Anda tidak dapat menjamin sama sekali bahwa tidak ada pengecualian yang dilemparkan, bukan?
SonneXo
@ mooooeeeep Lebih baik meninggalkan beberapa penjelasan daripada sekadar tautan. Tautan dapat kedaluwarsa di masa mendatang sementara saya telah melihat ini banyak terjadi.
Jason Liu
63

C ++ 20-an std::span

Jika Anda bisa menggunakan C ++ 20, Anda bisa menggunakan std::spanpasangan panjang-penunjuk yang memberi pengguna tampilan ke urutan elemen yang berdekatan. Ini adalah semacam std::string_view, dan meskipun keduanya std::spandan std::string_viewtidak memiliki tampilan, std::string_viewadalah tampilan hanya baca.

Dari dokumen:

Rentang template kelas menjelaskan objek yang dapat merujuk ke urutan objek yang berdekatan dengan elemen pertama dari urutan di posisi nol. Suatu rentang dapat memiliki tingkat statis, dalam hal ini jumlah elemen dalam urutan diketahui dan dikodekan dalam jenisnya, atau tingkat dinamis.

Jadi yang berikut ini akan berhasil:

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

Lihat langsung

Karena std::spanpada dasarnya adalah pasangan pointer-panjang, Anda dapat menggunakan dengan cara berikut juga:

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

catatan: Tidak semua dukungan kompiler std::span. Periksa dukungan kompiler di sini .

MEMPERBARUI

Jika Anda tidak dapat menggunakan C ++ 20, Anda bisa menggunakan gsl::span yang pada dasarnya adalah versi dasar dari standar C ++ std::span.

Solusi C ++ 11

Jika Anda terbatas pada standar C ++ 11, Anda dapat mencoba menerapkan spankelas sederhana Anda sendiri :

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

Lihat versi C ++ 11 langsung

Alat pemecah buah keras
sumber
4
Anda dapat menggunakan gsl::spanuntuk C ++ 14 dan di atasnya jika kompiler Anda tidak mengimplementasikanstd::span
Artyer
2
@Artyer Saya akan memperbarui jawaban saya dengan ini. Terima kasih
NutCracker
29

Karena pustaka algoritma bekerja dengan iterator, Anda dapat menyimpan array.

Untuk pointer dan panjang array yang diketahui

Di sini Anda dapat menggunakan pointer mentah sebagai iterator. Mereka mendukung semua operasi yang didukung oleh iterator (kenaikan, perbandingan untuk kesetaraan, nilai, dll ...):

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

datamenunjuk ke anggota array dirst seperti iterator yang dikembalikan oleh begin()dan data + sizemenunjuk ke elemen setelah elemen terakhir array seperti iterator dikembalikan olehend() .

Untuk array

Di sini Anda dapat menggunakan std::begin()danstd::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 
    }
}

Tetapi perlu diingat bahwa ini hanya berfungsi, jika datatidak membusuk ke sebuah pointer, karena informasi panjang itu hilang.

Churill
sumber
7
Ini adalah jawaban yang benar. Algoritma berlaku untuk rentang . Kontainer (misalnya, std :: vector) adalah salah satu cara mengelola rentang, tetapi bukan satu-satunya cara.
Pete Becker
13

Anda bisa mendapatkan iterator pada array mentah dan menggunakannya dalam algoritma:

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

Jika Anda bekerja dengan pointer mentah (ptr + size), maka Anda dapat menggunakan teknik berikut:

    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: Namun, contoh di atas adalah desain yang buruk. Perpustakaan mengembalikan kita pointer mentah dan kita tidak tahu di mana buffer yang mendasarinya dialokasikan dan siapa yang seharusnya membebaskannya.

Biasanya, penelepon menyediakan buffered untuk fungsi untuk mengisi data. Dalam hal ini, kita dapat melakukan prealokasi vektor dan menggunakan buffer yang mendasarinya:

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

Saat menggunakan C ++ 11 atau lebih tinggi kita bahkan dapat membuat get_data_from_library () untuk mengembalikan vektor. Berkat memindahkan operasi, tidak akan ada salinan memori.

PooSH
sumber
2
Maka Anda dapat menggunakan pointer reguler sebagai iterator:auto begin = data; auto end = data + size;
PooSH
Namun, pertanyaannya adalah di mana data yang dikembalikan get_data_from_library()dialokasikan? Mungkin kita tidak seharusnya mengubahnya sama sekali. Jika kita perlu meneruskan buffer ke perpustakaan, maka kita dapat mengalokasikan vektor dan lulusv.data()
PooSH
1
@PooSH data dimiliki oleh perpustakaan, tetapi dapat diubah tanpa batasan (itu sebenarnya inti dari seluruh pertanyaan). Hanya ukuran data yang tidak dapat diubah.
Jabberwocky
1
@Jabberwocky menambahkan contoh yang lebih baik tentang cara menggunakan buffer vektor yang mendasari untuk mengisi data.
PooSH
9

Anda tidak dapat melakukan ini dengan std::vector tanpa membuat salinan. std::vectormemiliki pointer yang dimilikinya di bawah kap dan mengalokasikan ruang melalui pengalokasi yang disediakan.

Jika Anda memiliki akses ke kompiler yang memiliki dukungan untuk C ++ 20 yang dapat Anda gunakan std :: span yang dibangun untuk tujuan ini. Ini membungkus pointer dan ukuran menjadi "wadah" yang memiliki antarmuka wadah C ++.

Jika tidak, Anda bisa menggunakannya gsl :: span yang merupakan dasar dari versi standar.

Jika Anda tidak ingin mengimpor perpustakaan lain, Anda sendiri dapat menerapkannya sendiri tergantung pada semua fungsi yang ingin Anda miliki.

NathanOliver
sumber
9

Sekarang saya ingin menggunakan std :: vector untuk mengakses dan memodifikasi nilai-nilai ini di tempat

Kamu tidak bisa. Bukan itu untuk apa std::vector.std::vectormengelola buffer sendiri, yang selalu diperoleh dari pengalokasi. Tidak pernah mengambil kepemilikan buffer lain (kecuali dari vektor lain dengan jenis yang sama).

Di sisi lain, Anda juga tidak perlu karena ...

Alasannya adalah bahwa saya perlu menerapkan algoritma dari (menyortir, menukar elemen dll) pada data itu.

Algoritma tersebut bekerja pada iterator. Pointer adalah iterator ke array. Anda tidak perlu vektor:

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

Tidak seperti templat fungsi <algorithm>, beberapa alat seperti rentang-untuk, std::begin/ std::enddan rentang C ++ 20 tidak berfungsi hanya dengan sepasang iterator, sementara mereka bekerja dengan wadah seperti vektor. Dimungkinkan untuk membuat kelas pembungkus untuk ukuran iterator + yang berperilaku sebagai rentang, dan bekerja dengan alat ini. C ++ 20 akan memperkenalkan wrapper tersebut ke perpustakaan standar: std::span.

eerorika
sumber
7

Selain saran bagus lainnya tentang std::spandatang di dan gsl:span, termasuk kelas Anda sendiri (ringan) spansampai saat itu sudah cukup mudah (jangan ragu untuk menyalin):

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

Catatan khusus juga merupakan peningkatan jangkauan perpustakaan jika Anda tertarik pada konsep rentang yang lebih umum: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

Konsep rentang juga akan tiba di

sayang
sumber
1
Untuk apa using value_type = std::remove_cv_t<T>;?
Jabberwocky
1
... dan Anda lupa konstruktor: span(T* first_, size_t length) : first(first), length(length) {};. Saya mengedit jawaban Anda.
Jabberwocky
@ Jabberwocky Saya baru saja menggunakan inisialisasi agregat. Tapi konstruktor baik-baik saja.
darune
1
@eerorika saya kira Anda benar, saya menghapus versi non-const
darune
1
The using value_type = std::remove_cv_t<T>;terutama diperlukan jika digunakan dengan pemrograman template (untuk mendapatkan value_type dari 'range'). Jika Anda hanya ingin menggunakan iterator, Anda dapat melewati / menghapusnya.
darune
6

Anda sebenarnya hampir dapat menggunakannya std::vectoruntuk ini, dengan menyalahgunakan fungsi pengalokasi khusus untuk mengembalikan pointer ke memori yang ingin Anda lihat. Itu tidak akan dijamin oleh standar untuk bekerja (padding, alignment, inisialisasi nilai yang dikembalikan; Anda harus bersusah payah ketika menetapkan ukuran awal, dan untuk non-primitif Anda juga perlu meretas konstruktor Anda ), tetapi dalam prakteknya saya berharap untuk memberikan tweak yang cukup.

Tidak pernah melakukannya. Itu jelek, mengejutkan, peretasan, dan tidak perlu. Algoritme pustaka standar sudah dirancang untuk bekerja juga dengan array mentah seperti halnya dengan vektor. Lihat jawaban lain untuk detailnya.

Sneftel
sumber
1
Hmm, ya itu bisa bekerja dengan vectorkonstruktor yang mengambil referensi Allocator kustom sebagai konstruktor arg (bukan hanya param template). Saya kira Anda akan memerlukan objek pengalokasi yang memiliki nilai pointer runtime di dalamnya, bukan sebagai parameter template kalau tidak itu hanya bisa bekerja untuk alamat constexpr. Anda harus berhati-hati untuk tidak membiarkan objek vector-objek bawaan dibangun .resize()dan menimpa data yang ada; ketidakcocokan antara wadah yang memiliki seperti vektor vs rentang yang tidak memiliki sangat besar jika Anda mulai menggunakan .push_back dll
Peter Cordes
1
@PeterCordes Maksudku, jangan mengubur lede - Anda juga harus menjadi gila. Menurut saya, hal yang paling aneh tentang ide tersebut adalah bahwa antarmuka pengalokasi mencakup constructmetode yang akan diperlukan ... Saya tidak dapat berpikir kasus penggunaan apa yang memerlukan non-hacky yang memerlukan penempatan lebih baru.
Sneftel
1
Kasus penggunaan yang jelas adalah untuk menghindari membuang-buang waktu membangun elemen-elemen yang akan Anda tulis dengan cara lain, misalnya resize()sebelum Anda meneruskan referensi ke sesuatu yang ingin menggunakannya sebagai output murni (misalnya panggilan sistem baca). Dalam praktiknya kompiler sering tidak mengoptimalkan memset itu atau apa pun. Atau jika Anda memiliki pengalokasi yang menggunakan calloc untuk mendapatkan memori pra-zeroed, Anda juga bisa menghindari mengotori seperti yang dilakukan secara bodoh std::vector<int>ketika default-membangun objek yang memiliki pola bit semua-nol. Lihat Catatan di en.cppreference.com/w/cpp/container/vector/vector
Peter Cordes
4

Seperti yang telah ditunjukkan orang lain, std::vectorharus memiliki memori yang mendasarinya (tidak dapat digunakan untuk pengalokasi khusus) sehingga tidak dapat digunakan.

Yang lain juga merekomendasikan rentang c ++ 20, namun jelas yang membutuhkan c ++ 20.

Saya akan merekomendasikan rentang span-lite . Mengutip subtitle:

span lite - A C ++ 20-like span untuk C ++ 98, C ++ 11 dan yang lebih baru di pustaka header file tunggal

Ini memberikan tampilan yang tidak memiliki dan dapat diubah (seperti pada Anda dapat mengubah elemen dan urutannya tetapi tidak memasukkannya) dan seperti kata kutipan tidak memiliki dependensi dan berfungsi pada sebagian besar kompiler.

Contoh Anda:

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

Cetakan

1
2
3
4
5

Ini juga memiliki sisi positif yang ditambahkan jika suatu hari Anda beralih ke c ++ 20, Anda seharusnya hanya dapat menggantinya nonstd::spandengan std::span.

Arghnews
sumber
3

Anda dapat menggunakan yang std::reference_wrappertersedia sejak 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 Andrzantai
sumber
2
Ini melakukan salinan data, dan itulah tepatnya yang ingin saya hindari.
Jabberwocky
1
@ Jabberwocky Ini tidak menyalin data. Tetapi bukan itu yang Anda minta dalam pertanyaan itu juga.
eerorika
@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));jelas mengisi dest_vectordengan nilai yang diambil dari src_table(TKI datanya disalin dest_vector), jadi saya tidak mendapatkan komentar Anda. Bisakah Anda jelaskan?
Jabberwocky
@ Jabberwocky tidak menyalin nilai. Itu mengisi vektor dengan pembungkus referensi.
eerorika
3
@ Jabberwocky lebih tidak efisien dalam hal nilai integer.
eerorika