Использование 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работает.
Джеспер Джуль
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не распадается на указатель, потому что тогда информация о длине пропадает.

churill
источник
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), вы можете использовать следующую технику:

    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 (), чтобы вернуть вектор. Благодаря операциям перемещения, не будет копии памяти.

PooSH
источник
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, на котором была основана стандартная версия.

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

NathanOliver
источник
9

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

Тебе нельзя. Это не то std::vector, для чего. std::vectorуправляет своим собственным буфером, который всегда получается из распределителя. Он никогда не становится владельцем другого буфера (за исключением другого вектора того же типа).

С другой стороны, вам также не нужно, потому что ...

Причина в том, что мне нужно применить алгоритмы (сортировка, замена элементов и т. Д.) К этим данным.

Эти алгоритмы работают на итераторах. Указатель является итератором массива. Вам не нужен вектор:

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

В отличие от шаблонов функций <algorithm>, некоторые инструменты, такие как range-for и std::begin/ std::endи диапазоны C ++ 20, не работают только с парой итераторов, хотя работают с контейнерами, такими как векторы. Можно создать класс-оболочку для iterator + size, который ведет себя как диапазон и работает с этими инструментами. C ++ 20 будет ввести такую обертку в стандартную библиотеку: std::span.

eerorika
источник
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 .

Концепции диапазона также появятся в

darune
источник
1
Для чего using value_type = std::remove_cv_t<T>;?
Джаббервоки
1
... и вы забыли конструктор: span(T* first_, size_t length) : first(first), length(length) {};. Я отредактировал твой ответ.
Jabberwocky
@Jabberwocky Я только что использовал агрегатную инициализацию. Но конструктор в порядке.
Даруна
1
@eerorika Полагаю, вы правы, я удалил неконстантные версии
Дарью
1
Это using value_type = std::remove_cv_t<T>;главным образом необходимо, если используется с шаблонным программированием (для получения value_type для 'range'). Если вы просто хотите использовать итераторы, вы можете пропустить / удалить это.
Даруна
6

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

Никогда и никогда не делай этого. Это уродливо, удивительно, нахально и ненужно. Алгоритмы стандартной библиотеки уже разработаны для работы как с необработанными массивами, так и с векторами. Смотрите другие ответы для деталей этого.

Sneftel
источник
1
Хм, да, это может работать с vectorконструкторами, которые принимают пользовательскую ссылку на Allocator в качестве аргумента конструктора (а не просто параметр шаблона). Я думаю, вам понадобится объект распределителя, в котором есть значение указателя времени выполнения, а не как параметр шаблона, иначе он может работать только для адресов constexpr. Вы должны быть осторожны, чтобы не vectorсоздавать объекты по умолчанию .resize()и не перезаписывать существующие данные; несоответствие между владеющим контейнером , как вектор против не владеющей пяди огромно , если вы начнете использовать .push_back и т.д.
Питер Кордес
1
@PeterCordes Я имею в виду, давайте не будем хоронить Леду - вы также должны быть сумасшедшими. На мой взгляд, самое странное в этой идее заключается в том, что интерфейс распределителя включает в себя constructметод, который был бы необходим ... Я не могу представить, какие нехакерные варианты использования потребовали бы этого вместо размещения нового.
Sneftel
1
Очевидный вариант использования состоит в том, чтобы не тратить время на создание элементов, которые вы собираетесь написать другим способом, например, resize()перед тем, как передать ссылку на что-то, что хочет использовать ее в качестве чистого вывода (например, системный вызов read). На практике компиляторы часто не оптимизируют этот memset или что-то еще. Или, если бы у вас был распределитель, который использует calloc для получения предварительно обнуленной памяти, вы также можете избежать его загрязнения, как это std::vector<int>делает глупость по умолчанию при создании объектов по умолчанию, которые имеют шаблон с нулевым битом. См. Примечания в en.cppreference.com/w/cpp/container/vector/vector
Питер Кордес
4

Как уже отмечали другие, они std::vectorдолжны владеть основной памятью (если не возиться с пользовательским распределителем), поэтому ее нельзя использовать.

Другие также рекомендовали диапазон c ++ 20, однако очевидно, что для этого требуется c ++ 20.

Я бы порекомендовал span-lite span. Процитирую это субтитры:

span lite - C ++ 20-подобный span для 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.

Arghnews
источник
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