使用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_backeraseinsert不要求工作在该载体。

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


16
您正在寻找的是假设的std::vector_view,不是吗?
眠りネロク

3
@眠りネロク是的,可能
Jabberwocky

5
那不是std::vector工作原理。
Jesper Juhl


34
标准算法在迭代器上工作,而指针迭代器。没有什么可以阻止您的工作sort(arrayPointer, arrayPointer + elementCount);
cmaster-恢复莫妮卡

Answers:


60

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

为了避免这种情况,你可以用一个切片对象的数组(即类似于std::string_viewstd::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::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。无论如何,您都可以使用gsl::spanC ++ 14以后的版本。


为什么将这些方法标记为noexcept?您完全不能保证不会引发任何异常,可以吗?
SonneXo


@moooeeeep最好留下一些解释,而不仅仅是一个链接。该链接将来可能会过期,尽管我已经看到很多情况。
Jason Liu

63

C ++ 20年代 std::span

如果能够使用C ++ 20,则可以使用“ std::span指针-长度对”,它使用户可以查看连续的元素序列。它是一种std::string_view,虽然std::span和和std::string_view均为非所有者视图,但std::string_view它是只读视图。

从文档:

类模板范围描述了一个对象,该对象可以引用对象的连续序列,序列的第一个元素位于位置0。跨度既可以具有静态范围,也可以具有动态范围,在这种情况下,序列中的元素数量是已知的并且已按类型进行编码。

因此,以下方法将起作用:

#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,则可以使用C ++ 20 gsl::span的基本版本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

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

对于指针和已知的数组长度

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

#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>,某些工具(例如range-for,std::begin/ std::end和C ++ 20范围)不能仅与一对迭代器一起使用,而可以与向量(vector)等容器一起使用。可以为迭代器+ size创建一个包装器类,该包装器的行为类似于范围,并可以与这些工具一起使用。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我猜你是对的,我删除了非常量版本
darune

1
using value_type = std::remove_cv_t<T>;主要需要如果与模板编程中使用的(用于得到一个“范围”的VALUE_TYPE)。如果只想使用迭代器,则可以跳过/删除它。
darune

6

实际上,通过滥用自定义分配器功能将指针返回到要查看的内存,您几乎可以使用std::vector它。标准无法保证这会正常工作(填充,对齐,返回值的初始化;在分配初始大小时,您将需要付出很多努力;对于非基本类型,您还需要修改构造函数),但实际上,我希望它能进行足够的调整。

永远不会那样做。这是丑陋的,令人惊讶的,hacky和不必要的。标准库的算法已设计为与原始数组以及矢量均能正常工作。有关详细信息,请参见其他答案。


1
嗯,是的,这可能与工作vector的构造是需要自定义分配器引用作为构造ARG(不只是一个模板PARAM)。我猜想您需要一个其中具有运行时指针值的分配器对象,而不是模板参数,否则它仅适用于constexpr地址。您必须注意不要让vector默认构造对象打开.resize()并覆盖现有数据。如果您开始使用.push_back等,那么像矢量这样的拥有容器与非拥有跨度之间的不匹配是巨大的
Peter Cordes

1
@PeterCordes我的意思是,我们不要埋葬书架-您必须疯了。在我看来,关于这个想法的最奇怪的事情是分配器接口包含了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。引用它的字幕:

span lite-单文件标头库中的C ++ 98,C ++ 11和更高版本的类似于C ++ 20的span

它提供了一个无所有权和可变的视图(因为您可以更改元素及其顺序,但不能插入它们),并且正如引号所说的那样,它没有依赖关系,并且可以在大多数编译器上使用。

你的例子:

#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::spanstd::span


3

您可以使用std::reference_wrapperC ++ 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_vectorsrc_table将数据复制到的IOW中)获取的值填充dest_vector,所以我没有收到您的评论。你能解释一下吗?
Jabberwocky

@Jabberwocky它不会复制值。它用参考包装器填充矢量。
eerorika

3
@Jabberwocky它在整数值的情况下,效率不高。
eerorika
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.