使用 std::vector 作为原始内存的视图

Jab*_*cky 86 c++ vector stdvector c++11

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

现在我想使用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 
}
Run Code Online (Sandbox Code Playgroud)

预期输出:

1
2
3
4
5
Run Code Online (Sandbox Code Playgroud)

原因是我需要<algorithm>在该数据上应用算法(排序、交换元素等)。

在另一方面改变这种载体的大小将永远不会改变,因此push_backeraseinsert不要求工作在该载体。

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

Nut*_*ker 74

C++20 年代 std::span

如果您能够使用 C++20,您可以使用std::spanwhich 是一个指针 - 长度对,它为用户提供了一个连续元素序列的视图。它是某种 a std::string_view,虽然std::spanstd::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;
}
Run Code Online (Sandbox Code Playgroud)

现场查看

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

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

注意:并非所有编译器都支持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_;
    }
};
Run Code Online (Sandbox Code Playgroud)

实时查看 C++11 版本

  • 如果您的编译器未实现 `std::span`,您可以对 C++14 及更高版本使用 [`gsl::span`](https://github.com/microsoft/GSL) (4认同)

眠りネ*_*ネロク 67

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

为了避免这种情况,您可以对数组使用切片对象(即,类似于std::string_viewto 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_; }
};
Run Code Online (Sandbox Code Playgroud)

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';
}
Run Code Online (Sandbox Code Playgroud)

输出:

1 2 3 4 5
Run Code Online (Sandbox Code Playgroud)

使用std::span(或gsl::span)代替

上面的实现揭示了切片对象背后的概念。但是,从 C++20 开始,您可以直接使用std::span。在任何情况下,您都可以gsl::span从 C++14 开始使用。

  • @SonneXo https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Re-noexcept (9认同)
  • @moooeeeeep 最好留下一些解释,而不仅仅是一个链接。该链接将来可能会过期,而我已经看到这种情况发生了很多次。 (2认同)

chu*_*ill 30

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

对于指针和已知数组长度

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

#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";
    }
}
Run Code Online (Sandbox Code Playgroud)

data指向 dirst 数组成员,就像 返回的迭代器一样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 
    }
}
Run Code Online (Sandbox Code Playgroud)

但请记住,这仅适用于data不会衰减到指针的情况,因为长度信息会丢失。

  • 这是正确的答案。算法适用于**范围**。容器(例如,std::vector)是管理范围的一种方法,但它们不是唯一的方法。 (7认同)

Poo*_*oSH 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;
    }
Run Code Online (Sandbox Code Playgroud)

如果您使用原始指针(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;
    }
Run Code Online (Sandbox Code Playgroud)

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;
    }
Run Code Online (Sandbox Code Playgroud)

当使用 C++11 或更高版本时,我们甚至可以让 get_data_from_library() 返回一个向量。由于移动操作,不会有内存复制。

  • 然后你可以使用常规指针作为迭代器: `auto begin = data;` `auto end = data + size;` (2认同)

Nat*_*ica 9

如果std::vector不制作副本,您就无法做到这一点。 std::vector拥有它在引擎盖下的指针,并通过提供的分配器分配空间。

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

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

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


eer*_*ika 9

现在我想使用 std::vector 来访问和修改这些值

你不能。那不是std::vector为了什么。std::vector管理自己的缓冲区,该缓冲区始终从分配器获取。它永远不会获得另一个缓冲区的所有权(除了来自另一个相同类型的向量)。

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

原因是我需要在该数据上应用算法(排序、交换元素等)。

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

std::sort(data, data + size);
Run Code Online (Sandbox Code Playgroud)

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


dar*_*une 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");
Run Code Online (Sandbox Code Playgroud)

如果您对更通用的范围概念感兴趣,请特别注意提升范围库https : //www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html

范围概念也将在


Sne*_*tel 6

实际上,你可以几乎利用std::vector这一点,通过滥用自定义分配器功能返回一个指向您要查看的内存。标准并不能保证这会起作用(填充、对齐、返回值的初始化;在分配初始大小时您必须费心,对于非原始大小,您还需要修改构造函数),但在实践中,我希望它能够进行足够的调整。

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