使用 std::vector 查看原始内存

Using std::vector as view on to raw memory

我正在使用一个外部库,它有时会给我一个指向整数数组和大小的原始指针。

现在我想使用 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_backeraseinsert 来处理该向量。

我可以根据库中的数据构建一个向量,使用修改该向量并将数据复制回库,但我想避免这将是两个完整的副本,因为数据集可能真的很大

由于算法库使用迭代器,您可以保留数组。

对于指针和已知数组长度

在这里你可以使用原始指针作为迭代器。它们支持迭代器支持的所有操作(递增、相等性比较、值等...):

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

databegin() 返回的迭代器一样指向第一个数组成员,而 data + size 像 [= 返回的迭代器一样指向数组最后一个元素之后的元素15=].

对于数组

这里可以使用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 不衰减为指针时才有效,因为那样长度信息就会丢失。

您可以在原始数组上获取迭代器并在算法中使用它们:

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

更新: 但是,上面的例子设计得很糟糕。库 return 为我们提供了一个原始指针,我们不知道底层缓冲区分配在哪里以及应该由谁释放它。

通常,调用者会为函数提供一个缓冲来填充数据。在那种情况下,我们可以预分配向量并使用其底层缓冲区:

    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() 到 return 成为向量。由于移动操作,不会有内存复制。

如果不制作副本,则无法使用 std::vector 执行此操作。 std::vector 拥有其内部的指针,并通过提供的分配器分配 space。

如果您可以使用支持 C++20 的编译器,您可以使用专为此目的而构建的 std::span。它将指针和大小包装到具有 C++ 容器接口的 "container" 中。

如果没有,您可以使用 gsl::span,这是标准版本的基础。

如果您不想导入另一个库,您可以根据自己想要的所有功能自行实现。

你实际上几乎可以为此使用std::vector,通过滥用自定义分配器功能return指向你想要查看的内存的指针。标准无法保证这能正常工作(returned 值的填充、对齐、初始化;分配初始大小时你必须努力,对于非基元,你还需要破解你的构造函数),但实际上我希望它能进行足够的调整。

永远不要那样做。这是丑陋的、令人惊讶的、骇人听闻的和不必要的。标准库的算法 已经 设计用于处理原始数组和向量。有关详细信息,请参阅其他答案。

C++20 的 std::span

如果您能够使用 C++20,则可以使用 std::span,这是一个指针 - 长度对,让用户可以查看连续的元素序列。它是某种 std::string_view,虽然 std::spanstd::string_view 都是非拥有视图,但 std::string_view 是只读视图。

来自文档:

The class template span describes an object that can refer to a contiguous sequence of objects with the first element of the sequence at position zero. A span can either have a static extent, in which case the number of elements in the sequence is known and encoded in the type, or a dynamic extent.

所以下面的方法可行:

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

看看live

由于std::span基本上是指针-长度对,您也可以按以下方式使用:

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

注:并非所有编译器都支持std::span。检查编译器支持 here.

更新

如果您不能使用 C++20,您可以使用 gsl::span,它基本上是 C++ 标准的 std::span.

的基础版本

C++11 解决方案

如果你受限于 C++11 标准,你可以尝试实现你自己的简单 span class:

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 版本 live

问题是 std::vector 必须从您初始化它的数组中复制元素,因为它拥有它所包含的对象的所有权。

为避免这种情况,您可以对数组使用 slice 对象(即类似于 std::string_viewstd::string 的作用)。您可以编写自己的 array_view class 模板实现,其实例是通过获取指向数组第一个元素的原始指针和数组长度来构造的:

#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::sortstd::findstd::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 instead. In any case, you can use gsl::span 自 C++14 起。

您可以使用自 C++11 起可用的 std::reference_wrapper

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

除了关于 std::span 加入 gsl:span 的其他好建议,包括你自己的(轻量级)span class 到那时很容易已经足够了(随意复制):

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

需要特别注意的还有提升范围库 if you are interested in the more generic range concept: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference/utilities/iterator_range.html

范围概念也将到达

Now I'd like to use std::vector to access and modify these values in place

你不能。这不是 std::vector 的目的。 std::vector 管理它自己的缓冲区,它总是从分配器中获取。它从不取得另一个缓冲区的所有权(除了来自另一个相同类型的向量)。

另一方面,您也不需要,因为...

The reason is that I need to apply algorithms from (sorting, swaping elements etc.) on that data.

这些算法适用于迭代器。指针是指向数组的迭代器。您不需要矢量:

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

<algorithm> 中的函数模板不同,range-for、std::begin/std::end 和 C++20 ranges 等一些工具不能只使用一对迭代器,而它们确实使用容器(例如向量)。可以为迭代器 + 大小创建一个包装器 class,其行为类似于一个范围,并与这些工具一起使用。 C++20 将在标准库中引入这样的包装器:std::span.

正如其他人指出的那样,std::vector 必须拥有底层内存(除了与自定义分配器混淆)所以不能使用。

其他人也推荐了 c++20 的跨度,但显然这需要 c++20。

我会推荐 span-lite 跨度。引用它的副标题:

span lite - A C++20-like span for C++98, C++11 and later in a single-file header-only library

它提供了一个 non-owning 和可变视图(因为你可以改变元素和它们的顺序但不能插入它们)并且正如引用所说没有依赖性并且适用于大多数编译器。

你的例子:

#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,这也有额外的好处,你应该能够用 std::span 替换这个 nonstd::span

用 C++ 编写的 ArrayView 的完整实现:​​

template<typename T>
class ArrayView {
public:
    using value_type = T;
    using const_iterator = const T*;

    ArrayView(T* ptr, size_t size) noexcept : ptr_(ptr), size_(size) {}
  
    template <typename U, size_t N>
    ArrayView(U (&buffer)[N]) noexcept : ArrayView(buffer, N) {}

    // ArrayView<T> to ArraryView<const T>
    // std::vector<T> to ArraryView<const T> or ArraryView<T>
    template <
        typename U,
        // Container has data and size
        typename std::enable_if<
            std::is_convertible<decltype(std::declval<U>().data()), T*>::value &&
            std::is_convertible<decltype(std::declval<U>().size()), std::size_t>::value
        >::type* = nullptr
    >
    ArrayView(const U& u) noexcept : ArrayView(u.data(), u.size()) {}

    T& operator[](int i) noexcept { return ptr_[i]; }
    T const& operator[](int i) const noexcept { return  ptr_[i]; }
    T* data() const noexcept { return ptr_; }
    size_t size() const noexcept { return size_; };

    T* begin() const noexcept { return this->data(); }
    T* end() const noexcept { return this->data() + this->size(); }
    const T* cbegin() const { return this->data(); }
    const T* cend() const { return this->data() + this->size(); }

    std::reverse_iterator<T*> rbegin() const {
        return std::make_reverse_iterator(end());
    }
    std::reverse_iterator<T*> rend() const {
        return std::make_reverse_iterator(begin());
    }
    std::reverse_iterator<const T*> crbegin() const {
        return std::make_reverse_iterator(cend());
    }
    std::reverse_iterator<const T*> crend() const {
        return std::make_reverse_iterator(cbegin());
    }

    ArrayView<T> subview(size_t offset, size_t size) const noexcept { 
        return offset < this->size() ? ArrayView<T>(this->data() + offset, std::min(size, this->size() - offset))
                                     : ArrayView<T>(nullptr, 0);
    }

    ArrayView<T> subview(size_t offset) const noexcept { 
        return subview(offset, this->size());
    }

private:
    T* ptr_;
    size_t size_;
};