Ham belleğe görünüm olarak std :: vector kullanımı


71

Bir noktada bana tamsayılar dizisi ve bir boyut için ham bir işaretçi veren harici bir kütüphane kullanıyorum.

Şimdi std::vectorbu değerlere ham işaretçilerle erişmek yerine yerinde erişmek ve değiştirmek için kullanmak istiyorum .

İşte konuyu açıklayan yapay bir örnek:

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 
}

Beklenen çıktı:

1
2
3
4
5

Nedeni <algorithm>(sıralama, takas öğeleri vb.) Algoritmaları bu verilerde uygulamak gerekir.

Bu vektörün boyutunu değiştirmek Öte yandan değiştirilemez asla günü, böylece push_back, erase, inserto vektör üzerinde çalışmaya gerek yoktur.

Kütüphanedeki verilere dayalı bir vektör oluşturabilirim, bu vektörde değişiklik yapmayı ve verileri tekrar kütüphaneye kopyalamayı kullanabilirim, ancak veri seti gerçekten büyük olabileceğinden kaçınmak istediğim iki tam kopya olurdu.


16
Aradığınız şey varsayımsal std::vector_view, değil mi?
眠 り ネ ロ ク

3
@ 眠 り ネ ロ ク evet, muhtemelen
Jabberwocky

5
Bu şekilde çalışmaz std::vector.
Jesper Juhl


34
Standart algoritmalar iterators üzerinde çalışmak ve işaretçileri olan yineleyiciler. Yapmaný engelleyecek hiçbir ţey yok sort(arrayPointer, arrayPointer + elementCount);.
cmaster - reinstate monica

Yanıtlar:


60

Sorun, std::vectoröğelerini, içerdiği nesnelerin sahipliğine sahip olduğu için, başlattığınız diziden bir kopyasını oluşturması gerektiğidir.

Bunu önlemek için, bir kullanabilirsiniz dilim (ne benzer, yani bir diziye için nesne std::string_viewetmektir std::string). array_viewÖrnekleri bir dizinin ilk öğesine ve dizi uzunluğuna ham bir işaretçi alarak oluşturulan kendi sınıf şablonu uygulamanızı yazabilirsiniz :

#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_viewbir dizi saklamaz; sadece dizinin başına ve bu dizinin uzunluğuna bir işaretçi tutar. Bu nedenle, array_viewnesnelerin oluşturulması ve kopyalanması ucuzdur.

Yana array_viewsağlar begin()ve end()üye fonksiyonları, standart kütüphane algoritmaları (örneğin kullanabilirsiniz std::sort, std::find, std::lower_bound, vb) üzerinde:

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

Çıktı:

1 2 3 4 5

Kullanım std::span(veya gsl::span) yerine

Yukarıdaki uygulama, dilim nesnelerinin arkasındaki kavramı ortaya koymaktadır . Bununla birlikte, C ++ 20'den beri doğrudan kullanabilirsiniz std::span. Her durumda, gsl::spanC ++ 14'ten beri kullanabilirsiniz .


Yöntemleri neden istisna olarak işaretlediniz? Hiçbir istisna yapılmadığını garanti edemezsiniz, değil mi?
SonneXo


@moooeeeep Bir bağlantıdan biraz açıklama bırakmak daha iyidir. Bunun çok fazla olduğunu gördüğümde bağlantının süresi dolmuş olabilir.
Jason Liu

63

C ++ 20 en std::span

C ++ 20 kullanabiliyorsanız std::span, kullanıcıya bitişik bir dizi öğeye bir görünüm veren bir işaretçi uzunluğu çifti kullanabilirsiniz . Bu bir çeşit std::string_viewve her iki iken std::spanve std::string_viewsigara sahibi görünümlerdir, std::string_viewsalt okunur bir görünümüdür.

Dokümanlardan:

Sınıf şablonu yayılımı, sıranın birinci öğesi sıfır konumunda olan bitişik bir nesne dizisine başvurabilen bir nesneyi açıklar. Bir yayılma alanı ya statik bir boyuta sahip olabilir, bu durumda dizideki elemanların sayısı tipte bilinir ve kodlanır, ya da dinamik bir kapsamdadır.

Yani aşağıdakiler işe yarar:

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

Canlı olarak kontrol et

Yana std::spantemelde göstericidir - uzunluk çifti, siz de bir aşağıdaki şekilde kullanabilirsiniz:

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

Not: Tüm derleyiciler desteklenmez std::span. Derleyici desteğini buradan kontrol edin .

GÜNCELLEME

C ++ 20 kullanamıyorsanız gsl::span, temel olarak C ++ standartlarının temel sürümü olan kullanabilirsiniz std::span.

C ++ 11 çözümü

C ++ 11 standardıyla sınırlıysanız, kendi basit spansınıfınızı uygulamayı deneyebilirsiniz :

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

Canlı C ++ 11 sürümünü inceleyin


4
gsl::spanDerleyiciniz uygulanmazsa C ++ 14 ve üstü için kullanabilirsinizstd::span
Artyer

2
@Artyer Cevabımı bununla güncelleyeceğim. Thanks
NutCracker

29

Algoritma kütüphanesi yineleyicilerle çalıştığından diziyi koruyabilirsiniz.

İşaretçiler ve bilinen dizi uzunluğu için

Burada ham işaretçileri yineleyiciler olarak kullanabilirsiniz. Yineleyicinin desteklediği tüm işlemleri destekler (artış, eşitlik karşılaştırması, değeri vb.):

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

datatarafından döndürülen bir yineleyici gibi Dirst dizi elemanına noktaları begin()ve data + sizedönen bir yineleyici gibi dizinin son öğe sonra elemana noktaları end().

Diziler için

Burada std::begin()vestd::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 
    }
}

Ancak, bunun yalnızca databir işaretçiye bozulmazsa çalıştığını unutmayın , çünkü uzunluk bilgileri kaybolur.


7
Bu doğru cevap. Algoritmalar aralıklara uygulanır . Kapsayıcılar (örneğin, std :: vector) aralıkları yönetmenin bir yoludur, ancak bunlar tek yol değildir.
Pete Becker

13

Ham dizilerde yineleyiciler alabilir ve bunları algoritmalarda kullanabilirsiniz:

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

Ham işaretçilerle (ptr + boyut) çalışıyorsanız, aşağıdaki tekniği kullanabilirsiniz:

    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: Ancak, yukarıdaki örnek kötü tasarıma sahiptir. Kütüphane bize ham bir işaretçi döndürür ve temel tamponun nereye tahsis edildiğini ve kimin onu serbest bırakması gerektiğini bilmiyoruz.

Arayan genellikle işlevin verileri doldurması için arabelleğe alınır. Bu durumda, vektörü önceden konumlandırabilir ve alttaki tamponu kullanabiliriz:

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

C ++ 11 veya üstünü kullanırken, bir vektör döndürmek için get_data_from_library () bile yapabiliriz. Taşıma işlemleri sayesinde bellek kopyası olmayacaktır.


2
Sonra yineleyiciler olarak düzenli işaretçiler kullanabilirsiniz:auto begin = data; auto end = data + size;
PooSH

Ancak soru, döndürülen verilerin nereye get_data_from_library()tahsis edildiğidir ? Belki de hiç değiştirmemiz gerekmiyor. Kütüphaneye bir tampon geçirmemiz gerekiyorsa, vektör tahsis edebilir ve geçirebilirizv.data()
PooSH

1
@PooSH verileri kütüphaneye aittir, ancak herhangi bir kısıtlama olmaksızın değiştirilebilir (aslında tüm sorunun noktasıdır). Yalnızca verilerin boyutu değiştirilemez.
Jabberwocky

1
@Jabberwocky, verileri doldurmak için vektörün temel tamponunun nasıl kullanılacağına dair daha iyi bir örnek ekledi.
PooSH

9

Bunu std::vectorbir kopyasını yapmadan bir ile yapamazsınız . std::vectorkaputun altında olan işaretçiye sahiptir ve sağlanan ayırıcı üzerinden boşluk tahsis eder.

C ++ 20 desteği olan bir derleyiciye erişiminiz varsa , tam olarak bu amaçla yapılmış std :: span kullanabilirsiniz . Bir işaretçi ve boyutu C ++ kapsayıcı arabirimi olan bir "kapsayıcı" içine sarar.

Değilse , standart sürümün temelini oluşturan gsl :: span kullanabilirsiniz .

Başka bir kütüphaneyi içe aktarmak istemiyorsanız, sahip olmak istediğiniz tüm işlevselliğe bağlı olarak bunu kendiniz uygulayabilirsiniz.


9

Şimdi bu değerlere yerinde erişmek ve değiştirmek için std :: vector kullanmak istiyorum

Yapamazsın. Bunun için bu değil std::vector. std::vectorher zaman bir ayırıcıdan alınan kendi arabelleğini yönetir. Asla başka bir tamponun sahipliğini almaz (aynı tipte başka bir vektör hariç).

Öte yandan, buna da gerek yok çünkü ...

Nedeni (sıralama, takas öğeleri vb.) Algoritmaları bu verilerde uygulamak zorunda olmasıdır.

Bu algoritmalar yineleyiciler üzerinde çalışır. İşaretçi bir dizi için yineleyicidir. Bir vektöre ihtiyacınız yok:

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

İşlev şablonlarının aksine <algorithm>, range-for, std::begin/ std::endve C ++ 20 aralıkları gibi bazı araçlar yalnızca bir çift yineleyici ile çalışmazken, vektörler gibi kaplarla çalışır. Yineleyici + boyutu için aralık olarak davranan ve bu araçlarla çalışan bir sarıcı sınıf oluşturmak mümkündür. C ++ 20 standart kitaplığı bu tür sarıcı tanıtacak: std::span.


7

std::span gelme ve o zamana kadar kendi (hafif) sınıfınız dahil olmak üzere diğer iyi önerilerin yanı sıra yeterince kolay (kopyalamaktan çekinmeyin):gsl:spanspan

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

Özel Not artışı aralığı kütüphanesi de daha genel aralık konsepti ilgilenen varsa: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

Aralık kavramları da gelecek


1
Ne using value_type = std::remove_cv_t<T>;için?
Jabberwocky

1
... ve yapıcı unuttun: span(T* first_, size_t length) : first(first), length(length) {};. Cevabınızı düzenledim.
Jabberwocky

@ Jabberwocky Az önce toplam başlatma kullandım. Ancak yapıcı iyidir.
darune

1
@eerorika haklısın, const olmayan sürümleri kaldırdım
darune

1
using value_type = std::remove_cv_t<T>;(A 'aralık' içinde VALUE_TYPE almak için) şablonu programlama ile kullanıldığında esas ihtiyaç vardır. Sadece yineleyicileri kullanmak istiyorsanız bunu atlayabilir / kaldırabilirsiniz.
darune

6

Aslında olabilir neredeyse kullanmak std::vectorgörüntülemek istediğiniz belleğin bir gösterici döndürmek için özel ayırıcı özelliğe kötüye kullanarak, bunun için. Bu standart tarafından çalışmak için garanti edilmez (dolgu, hizalama, döndürülen değerlerin başlatılması; ilk boyutu atarken acı çekmeniz gerekir ve ilkel olmayanlar için de yapıcılarınızı hacklemeniz gerekir ), ancak pratikte yeterli ayarlamalar yapmasını beklerdim.

Bunu asla asla yapma. Çirkin, şaşırtıcı, hileli ve gereksiz. Standart kütüphanenin algoritmaları vardır zaten vektörler ile ham dizilerle de çalışmak üzere tasarlanmış. Bunun ayrıntıları için diğer cevaplara bakınız.


1
Hmm, evet, vectoryapıcı argümanı olarak özel bir Allocator referansı alan yapıcılar ile çalışabilir (sadece bir şablon parametresi değil). Sanırım bir şablon parametresi olarak değil, çalışma zamanı işaretçi değeri olan bir ayırıcı nesneye ihtiyacınız olurdu aksi takdirde sadece constexpr adresleri için çalışabilir. Var olan verilerin vectorüzerinde varsayılan yapı nesnelerinin oluşturulmasına .resize()ve üzerine yazılmasına izin vermemeye dikkat etmelisiniz ; .push_back vb. kullanmaya başlarsanız vektör gibi sahip olmayan bir kapsayıcıya sahip olan bir kapsayıcı arasındaki uyumsuzluk çok büyüktür
Peter Cordes

1
@PeterCordes Yani, lede gömmeyelim - ayrıca deli olmalısın . Fikrime göre, garip olan şey, allocator arabiriminin constructgerekli olan yöntemi içerdiğidir ... Ne kadar hacky olmayan kullanım durumlarının fazla yerleştirme gerektirdiğini düşünemiyorum.
Sneftel

1
Bariz kullanım örneği, başka bir yolla yazmak üzere olduğunuz öğeleri inşa etmek için zaman kaybetmekten kaçınmaktır, örneğin, resize()onu saf bir çıktı olarak kullanmak isteyen bir şeye (örneğin bir okuma sistemi çağrısı) geçmeden önce. Pratikte derleyiciler genellikle bu memset'i ya da her neyse onu optimize etmezler. Ya da önceden sıfırlanmış belleği elde etmek için calloc kullanan bir ayırıcıya sahipseniz, std::vector<int>sıfırdan farklı bir bit desenine sahip varsayılan oluşturma nesneleri varsayılan olarak aptalın yaptığı gibi kirlenmesini de önleyebilirsiniz . En.cppreference.com/w/cpp/container/vector/vector
Peter Cordes

4

Diğerlerinin de belirttiği gibi std::vector, temel hafızanın (özel bir ayırıcıyla uğraşmaktan yoksun) sahibi olması gerekir.

Diğerleri de c ++ 20'nin yayılımını önerdi, ancak bu açıkça c ++ 20 gerektirir.

Span-lite span tavsiye ederim . Altyazısını alıntılamak için:

span lite - C ++ 98, C ++ 11 ve sonraki sürümleri için yalnızca tek dosya içeren bir başlık kitaplığında C ++ 20 benzeri açıklık

Sahip olmayan ve değişebilir bir görünüm sağlar (olduğu gibi öğeleri ve bunların sırasını değiştirebilir, ancak ekleyemezsiniz) ve alıntıda belirtildiği gibi çoğu derleyicide bağımlılık yoktur ve çalışır.

Örneğiniz:

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

Baskılar

1
2
3
4
5

Bir gün c ++ 20 geçiş yapmak zaman da ters eklemiştir, sadece bu değiştirmek gerekir nonstd::spanile std::span.


3

Kullanılabilir bir std::reference_wrapperC ++ 11'den beri kullanabilirsiniz :

#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
Bu, verilerin bir kopyasını gerçekleştirir ve bundan kaçınmak istediğim şey tam olarak budur.
Jabberwocky

1
@Jabberwocky Bu, verileri kopyalamaz. Ama bu soruda sizin istediğiniz şey de değil.
eerorika

@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));kesinlikle doldururdest_vector alınan değerlerisrc_table (verilerin kopyalandığı IOW dest_vector), bu yüzden yorumunuzu almadım. Açıklayabilir misiniz?
Jabberwocky

@Jabberwocky değerleri kopyalamaz. Vektörü referans sargıları ile doldurur.
eerorika

3
@ErkekTerimleri tamsayı değerlerinde daha verimsizdir.
eerorika
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.