Використання std :: vector для перегляду на необроблену пам'ять


71

Я використовую зовнішню бібліотеку, яка в якийсь момент дає мені необроблений покажчик на масив цілих чисел та розмірів.

Тепер я хотів би використати std::vectorдля доступу та зміни цих значень на місці, а не для доступу до них із необробленими покажчиками.

Ось мистецький приклад, який пояснює суть:

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 
}

Очікуваний вихід:

1
2
3
4
5

Причина полягає в тому, що мені потрібно застосувати алгоритми <algorithm>(сортування, підміна елементів тощо) до цих даних.

З іншого боку зміни розміру цього вектора рука ніколи не буде змінена, тому push_back, erase, insertне зобов'язані працювати на цьому векторі.

Я міг би побудувати вектор на основі даних з бібліотеки, використовувати модифікацію цього вектора та копіювання даних назад у бібліотеку, але це були б дві повні копії, яких я б хотів уникнути, оскільки набір даних може бути справді великим.


16
Те, що ви шукаєте, є гіпотетичним std::vector_view, чи не так?
眠 り ネ ロ ク

3
@ 眠 り ネ ロ ク так, ймовірно
Jabberwocky

5
Це не так, як std::vectorпрацює.
Jesper Juhl


34
Стандартні алгоритми працюють над ітераторами, а покажчики - ітераторами. Ніщо не заважає тобі робити sort(arrayPointer, arrayPointer + elementCount);.
cmaster - відновити моніку

Відповіді:


60

Проблема полягає в тому, що std::vectorпотрібно зробити копію елементів з масиву, з якого ви ініціалізуєте його, оскільки він має право власності на об'єкти, які він містить.

Щоб уникнути цього, ви можете використовувати об’єкт фрагмента для масиву (тобто, аналогічно тому, що std::string_viewмає бути std::string). Ви можете написати власну array_viewреалізацію шаблону класу, екземпляри якої побудовані, взявши необроблений покажчик на перший елемент масиву та довжину масиву:

#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_viewне зберігає масив; він просто містить покажчик на початок масиву та довжину цього масиву. Тому array_viewоб'єкти дешево будувати та копіювати.

Оскільки array_viewзабезпечує begin()і end()члени функції, ви можете використовувати стандартні алгоритми бібліотеки (наприклад, std::sort, std::find, std::lower_boundі т.д.) на ньому:

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

Вихід:

1 2 3 4 5

Використовуйте std::span(або gsl::span) натомість

Виконання вище розкриває концепцію за об'єктами фрагмента . Однак, оскільки C ++ 20 ви можете безпосередньо використовувати std::spanзамість цього. У будь-якому випадку, ви можете використовувати gsl::spanз C ++ 14.


Чому ви позначили методи як неісключені? Ви взагалі не можете гарантувати, що вас не кидають винятки, чи не так?
SonneXo


@moooeeeep Краще залишити якесь пояснення, ніж просто посилання. Термін дії посилання може бути закінчений у майбутньому, хоча я бачив, що це траплялося багато.
Джейсон Лю

63

C ++ 20-х std::span

Якщо ви можете використовувати C ++ 20, ви можете скористатись std::spanпарою довжини вказівника, яка надає користувачеві уявлення про суміжну послідовність елементів. Це певний вид std::string_view, і хоча обидва std::spanта std::string_viewне є власними поглядами, std::string_viewце перегляд лише для читання.

З документів:

Проміжок шаблону класу описує об'єкт, який може посилатися на суміжну послідовність об'єктів з першим елементом послідовності в нульовому положенні. Проміжок може мати або статичну величину, і в цьому випадку кількість елементів у послідовності відома і закодована за типом, або динамічна ступінь.

Отже, працює наступне:

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

Перевір наживо

Оскільки std::spanце в основному пара довжини вказівника, ви можете використовувати і наступним чином:

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

Примітка: Не всі компілятори підтримують std::span. Перевірте підтримку компілятора тут .

ОНОВЛЕННЯ

Якщо ви не можете використовувати C ++ 20, ви можете використовувати gsl::spanбазову версію стандарту C ++ std::span.

C ++ 11 розчин

Якщо ви обмежені стандартом C ++ 11, ви можете спробувати впровадити власний простий spanклас:

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

Ознайомтесь наживо на версії C ++ 11


4
Ви можете використовувати gsl::spanдля C ++ 14 і вище, якщо ваш компілятор не реалізуєstd::span
Artyer

2
@Artyer Я оновлю свою відповідь на це. Спасибі
NutCracker

29

Оскільки бібліотека алгоритмів працює з ітераторами, ви можете зберегти масив.

Для покажчиків та відомої довжини масиву

Тут ви можете використовувати необроблені покажчики як ітератори. Вони підтримують усі операції, які підтримує ітератор (приріст, порівняння для рівності, значення тощо):

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

dataвказує на елемент масиву засухи, як ітератор, що повертається, begin()і data + sizeвказує на елемент після останнього елемента масиву, як ітератор, повернутий end().

Для масивів

Тут ви можете використовувати std::begin()іstd::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 
    }
}

Але майте на увазі, що це працює лише в тому випадку, якщо dataне похитнеться до покажчика, тому що тоді інформація про довжину пропадає.


7
Це правильна відповідь. Алгоритми застосовуються до діапазонів . Контейнери (наприклад, std :: vector) - це один із способів управління діапазонами, але це не єдиний спосіб.
Піт Бекер

13

Ви можете отримати ітератори в необроблених масивах і використовувати їх в алгоритмах:

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

Якщо ви працюєте з необробленими покажчиками (ptr + розмір), то ви можете використовувати таку техніку:

    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: Однак, наведений вище приклад - поганий дизайн. Бібліотека повертає нам необроблений покажчик, і ми не знаємо, де виділяється основний буфер і хто повинен його звільнити.

Зазвичай абонент надає буферизовану функцію заповнення даних. У цьому випадку ми можемо попередньо виділити вектор і використовувати його базовий буфер:

    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 або вище, ми навіть можемо зробити get_data_from_library () для повернення вектора. Завдяки операціям переміщення копії пам'яті не буде.


2
Тоді ви можете використовувати звичайні покажчики як ітератори:auto begin = data; auto end = data + size;
PooSH

Однак питання полягає в тому, куди get_data_from_library()розподіляються дані, повернуті ? Можливо, ми взагалі не повинні це змінювати. Якщо нам потрібно передати буфер до бібліотеки, тоді ми можемо виділити вектор і передатиv.data()
PooSH

1
@PooSH дані належать бібліотеці, але їх можна змінювати без обмежень (це фактично суть у всьому питанні). Змінити лише розмір даних не можна.
Jabberwocky

1
@Jabberwocky додав кращий приклад того, як використовувати базовий буфер вектора для заповнення даних.
PooSH

9

Ви не можете зробити це за std::vectorдопомогою копіювання. std::vectorналежить покажчику, який він має під кришкою, і виділяє простір через виділений розподільник.

Якщо у вас є доступ до компілятора, який підтримує C ++ 20, ви можете використовувати std :: span, який був побудований саме для цієї мети. Він загортає вказівник та розмір у "контейнер", який має контейнер C ++.

Якщо ні, ви можете використовувати gsl :: span , на чому базувалася стандартна версія.

Якщо ви не хочете імпортувати іншу бібліотеку, ви можете банально реалізувати це самостійно залежно від того, яка функціональність ви хочете мати.


9

Тепер я хотів би використовувати std :: vector для доступу та зміни цих значень на місці

Ви не можете. Це не теstd::vector . std::vectorуправляє власним буфером, який завжди отримується у алокатора. Він ніколи не приймає право власності на інший буфер (крім іншого вектору того ж типу).

З іншого боку, вам також не потрібно, тому що ...

Причина полягає в тому, що мені потрібно застосувати алгоритми (сортування, підміна елементів тощо) до цих даних.

Ці алгоритми працюють на ітераторах. Вказівник - це ітератор масиву. Вам не потрібен вектор:

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

На відміну від шаблонів функцій у <algorithm>деяких інструментах, таких як діапазони діапазону, std::begin/ std::endі C ++ 20, не працює лише пара ітераторів, хоча вони працюють з контейнерами, такими як вектори. Можна створити клас обгортки для ітератора + розміру, який поводиться як діапазон, і працює з цими інструментами. C ++ 20 буде ввести таку обгортку в стандартну бібліотеку: std::span.


7

Окрім інших хороших пропозицій щодо std::spanприходу в та gsl:span, включаючи власний (легкий) spanклас, до цього часу вже досить просто (сміливо копіюйте):

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

Особливу увагу заслуговує також розширювальний діапазон якщо вас цікавить більш загальна концепція діапазону: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

Концепції діапазону також надходять у


1
Для чого using value_type = std::remove_cv_t<T>;?
Jabberwocky

1
... і ви забули конструктор: span(T* first_, size_t length) : first(first), length(length) {};. Я відредагував вашу відповідь.
Jabberwocky

@Jabberwocky Я просто використав сукупну ініціалізацію. Але конструктор чудово.
darune

1
@eerorika Думаю, ти маєш рацію, я видалив версії non-const
darune

1
В using value_type = std::remove_cv_t<T>;основному це потрібно, якщо використовується з програмуванням шаблонів (для отримання значення_типу 'діапазону'). Якщо ви просто хочете використовувати ітератори, ви можете їх пропустити / видалити.
darune

6

Ви насправді могли майже використати std::vectorдля цього, зловживаючи функцією спеціального розподільника, щоб повернути вказівник на пам'ять, яку ви хочете переглянути. Це не гарантується стандартом для роботи (прокладка, вирівнювання, ініціалізація повернених значень; вам потрібно буде взяти болі при призначенні початкового розміру, а для непомітивів вам також потрібно буде зламати ваші конструктори ), але на практиці я б очікував, що це дасть достатньо настроїв.

Ніколи ніколи цього не роби. Це некрасиво, дивно, хакі і непотрібно. Алгоритми стандартної бібліотеки вже розроблені для роботи так само, як із сирими масивами, так і з векторами. Детальні відомості див. В інших відповідях.


1
Гм, так, це могло б працювати з vectorконструкторами, які використовують аргументацію Allocator як аргумент конструктора (а не лише параметр шаблону). Я думаю, вам знадобиться алокаторний об’єкт, який має в ньому значення вказівника часу виконання, а не як параметр шаблону, інакше він може працювати лише для адрес constexpr. Вам доведеться бути обережними, щоб не дозволяти vectorоб'єктам конструювати за замовчуванням .resize()і перезаписувати наявні дані; невідповідність між контейнером, що володіє, як векторний та не володіючим прольотом, величезна, якщо ви почнете використовувати .push_back тощо
Пітер Кордес,

1
@PeterCordes Я маю в виду, давайте не будемо ховати Lede - ви також повинні бути божевільним. На мій погляд, найдивніше в ідеї полягає в тому, що інтерфейс алокатора включає constructметод, який був би необхідний ... Я не можу подумати, які випадки використання, які не мають хакі, потребують цього над розміщенням-новим.
Sneftel

1
Очевидним випадком використання є те, щоб не витрачати час на конструювання елементів, які ви збираєтеся написати іншим способом, наприклад, resize()перед тим, як передати посилання на щось, що хоче використовувати його як чистий вихід (наприклад, системний виклик читання). На практиці компілятори часто не оптимізують цей мемсет чи що завгодно. Або якщо у вас був розподільник, який використовує calloc для отримання попередньо нульової пам’яті, ви також можете уникнути забруднення його так, як std::vector<int>це робиться дурним за замовчуванням, коли об’єкти, що створюють за замовчуванням, мають бітовий малюнок за всіма нулями. Дивіться примітки в en.cppreference.com/w/cpp/container/vector/vector
Пітер Кордес

4

Як зазначали інші, std::vectorнеобхідно мати власну базову пам'ять (окрім того, що не возитися зі спеціальним розподільником), тому її не можна використовувати.

Інші також рекомендували інтервал c ++ 20, однак очевидно, що для цього потрібен c ++ 20.

Я б порекомендував " span-lite" . Щоб процитувати його підзаголовки:

span lite - C ++ 20-подібний проміжок для C ++ 98, C ++ 11 та пізніших версій у однофайловій бібліотеці, призначеній лише для заголовка

Він надає невласницький і змінний вигляд (як ви можете мутувати елементи та їх порядок, але не вставляти їх), і як каже цитата, не має залежностей і працює на більшості компіляторів.

Ваш приклад:

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

Друкує

1
2
3
4
5

Це також має додаткове вгору , якщо один день ви робите перехід на C ++ 20, ви просто повинні бути в змозі замінити це nonstd::spanз std::span.


3

Ви можете використовувати std::reference_wrapperдоступне з 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
Це виконує копію даних, і саме цього я хочу уникати.
Jabberwocky

1
@Jabberwocky Це не копіює дані. Але це не те, про що ви запитували.
eerorika

@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));напевно заповнює dest_vectorзначення, взяті з src_table(IOW, дані копіюються dest_vector), тому я не отримав ваш коментар. Чи можете ви пояснити?
Jabberwocky

@Jabberwocky він не копіює значення. Він заповнює його вектор за допомогою опорних обгортків.
eerorika

3
@Jabberwocky це більш неефективно у випадку цілих значень.
eerorika
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.