Sử dụng std :: vector làm chế độ xem trên bộ nhớ thô


71

Tôi đang sử dụng một thư viện bên ngoài, tại một số điểm, cho tôi một con trỏ thô tới một mảng các số nguyên và kích thước.

Bây giờ tôi muốn sử dụng std::vectorđể truy cập và sửa đổi các giá trị này, thay vì truy cập chúng bằng các con trỏ thô.

Dưới đây là một ví dụ rõ ràng giải thích điểm:

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 
}

Sản lượng dự kiến:

1
2
3
4
5

Lý do là tôi cần áp dụng các thuật toán từ <algorithm>(sắp xếp, thay đổi các yếu tố, v.v.) trên dữ liệu đó.

Mặt khác thay đổi kích thước của vector rằng sẽ không bao giờ được thay đổi, vì vậy push_back, erase, insertkhông cần phải làm việc trên vectơ.

Tôi có thể xây dựng một vectơ dựa trên dữ liệu từ thư viện, sử dụng sửa đổi vectơ đó và sao chép dữ liệu trở lại thư viện, nhưng đó sẽ là hai bản sao hoàn chỉnh mà tôi muốn tránh vì tập dữ liệu có thể rất lớn.


16
Những gì bạn đang tìm kiếm là một giả thuyết std::vector_view, phải không?
眠 り ネ

3
@ 眠 り ネ ロ có, có lẽ
Jabberwocky

5
Đó không phải là cách làm std::vectorviệc.
Jesper Juhl


34
Các thuật toán tiêu chuẩn hoạt động trên các trình vòng lặp và các con trỏ là các trình vòng lặp. Không có gì ngăn cản bạn làm sort(arrayPointer, arrayPointer + elementCount);.
cmaster - phục hồi monica

Câu trả lời:


60

Vấn đề là std::vectorphải tạo một bản sao của các phần tử từ mảng mà bạn khởi tạo nó vì nó có quyền sở hữu các đối tượng mà nó chứa.

Để tránh điều này, bạn có thể sử dụng một lát đối tượng cho một mảng (tức là, tương tự như những gì std::string_viewđang đến std::string). Bạn có thể viết array_viewtriển khai mẫu lớp của riêng bạn có các thể hiện được xây dựng bằng cách lấy một con trỏ thô đến phần tử đầu tiên của mảng và độ dài của mảng:

#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_viewkhông lưu trữ một mảng; nó chỉ giữ một con trỏ đến đầu mảng và độ dài của mảng đó. Do đó, array_viewcác đối tượng là giá rẻ để xây dựng và sao chép.

Kể từ khi array_viewcung cấp begin()end()thành viên các chức năng, bạn có thể sử dụng các thuật toán tiêu chuẩn thư viện (ví dụ std::sort, std::find, std::lower_bound, vv) trên đó:

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

Đầu ra:

1 2 3 4 5

Sử dụng std::span(hoặc gsl::span) thay thế

Việc thực hiện ở trên cho thấy khái niệm đằng sau các đối tượng lát . Tuy nhiên, vì C ++ 20 bạn có thể trực tiếp sử dụng std::spanthay thế. Trong mọi trường hợp, bạn có thể sử dụng gsl::spankể từ C ++ 14.


Tại sao bạn đánh dấu các phương thức là không có ngoại lệ? Bạn không thể đảm bảo rằng không có ngoại lệ nào bị ném, phải không?
SonneXo


@moooeeeep Tốt hơn là để lại một số lời giải thích hơn là chỉ một liên kết. Liên kết có thể hết hạn trong tương lai trong khi tôi đã thấy điều này xảy ra rất nhiều.
Jason Liu

63

C ++ 20 std::span

Nếu bạn có thể sử dụng C ++ 20, bạn có thể sử dụng std::spancặp độ dài con trỏ cung cấp cho người dùng chế độ xem thành một chuỗi các phần tử liền kề. Đó là một số loại std::string_view, và trong khi cả hai std::spanstd::string_viewlà các chế độ xem không sở hữu, std::string_viewlà một chế độ chỉ đọc.

Từ các tài liệu:

Khoảng mẫu của lớp mô tả một đối tượng có thể tham chiếu đến một chuỗi các đối tượng liền kề với phần tử đầu tiên của chuỗi ở vị trí 0. Một nhịp có thể có một phạm vi tĩnh, trong trường hợp đó, số lượng phần tử trong chuỗi được biết và được mã hóa theo loại hoặc phạm vi động.

Vì vậy, sau đây sẽ làm việc:

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

Kiểm tra trực tiếp

std::spanvề cơ bản là cặp chiều dài con trỏ, bạn cũng có thể sử dụng theo cách sau:

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

Lưu ý: Không phải tất cả các trình biên dịch hỗ trợ std::span. Kiểm tra hỗ trợ trình biên dịch ở đây .

CẬP NHẬT

Nếu bạn không thể sử dụng C ++ 20, gsl::spanvề cơ bản , bạn có thể sử dụng phiên bản cơ bản của tiêu chuẩn C ++ std::span.

Giải pháp C ++ 11

Nếu bạn bị giới hạn ở tiêu chuẩn C ++ 11, bạn có thể thử triển khai spanlớp đơn giản của riêng mình :

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

Kiểm tra phiên bản C ++ 11 trực tiếp


4
Bạn có thể sử dụng gsl::spancho C ++ 14 trở lên nếu trình biên dịch của bạn không triển khaistd::span
Artyer

2
@Artyer Tôi sẽ cập nhật câu trả lời của tôi với điều này. Cảm ơn
NutCracker

29

Vì thư viện thuật toán làm việc với các trình vòng lặp, bạn có thể giữ mảng.

Đối với con trỏ và chiều dài mảng đã biết

Ở đây bạn có thể sử dụng con trỏ thô như các vòng lặp. Chúng hỗ trợ tất cả các quan điểm mà một trình vòng lặp hỗ trợ (tăng, so sánh cho đẳng thức, giá trị, v.v ...):

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

datatrỏ đến thành viên mảng dirst như một iterator được trả về bởi begin()data + sizetrỏ đến phần tử sau phần tử cuối cùng của mảng giống như một iterator được trả về bởi end().

Đối với mảng

Ở đây bạn có thể sử dụng 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 
    }
}

Nhưng hãy nhớ rằng điều này chỉ hoạt động, nếu datakhông phân rã thành một con trỏ, vì sau đó thông tin độ dài bị mất.


7
Đây là câu trả lời đúng. Các thuật toán áp dụng cho phạm vi . Các thùng chứa (ví dụ: std :: vector) là một cách để quản lý phạm vi, nhưng chúng không phải là cách duy nhất.
Pete Becker

13

Bạn có thể nhận được các trình vòng lặp trên các mảng thô và sử dụng chúng trong các thuật toán:

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

Nếu bạn đang làm việc với các con trỏ thô (ptr + size), thì bạn có thể sử dụng kỹ thuật sau:

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

CẬP NHẬT: Tuy nhiên, ví dụ trên là thiết kế xấu. Thư viện trả về cho chúng ta một con trỏ thô và chúng ta không biết bộ đệm cơ bản được phân bổ ở đâu và ai có nghĩa vụ giải phóng nó.

Thông thường, người gọi cung cấp một bộ đệm cho chức năng để điền dữ liệu. Trong trường hợp đó, chúng ta có thể phân bổ vectơ và sử dụng bộ đệm bên dưới của nó:

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

Khi sử dụng C ++ 11 trở lên, chúng ta thậm chí có thể tạo get_data_from_l Library () để trả về một vectơ. Nhờ các hoạt động di chuyển, sẽ không có bản sao bộ nhớ.


2
Sau đó, bạn có thể sử dụng các con trỏ thông thường như các trình vòng lặp:auto begin = data; auto end = data + size;
PooSH

Tuy nhiên, câu hỏi là dữ liệu được trả về get_data_from_library()được phân bổ ở đâu? Có lẽ chúng ta không nên thay đổi nó. Nếu chúng ta cần truyền một bộ đệm vào thư viện, thì chúng ta có thể phân bổ vectơ và vượt quav.data()
PooSH

1
@PooSH dữ liệu được sở hữu bởi thư viện, nhưng nó có thể được thay đổi mà không bị hạn chế (đó thực sự là điểm của toàn bộ câu hỏi). Chỉ có thể thay đổi kích thước của dữ liệu.
Jabberwocky

1
@Jabberwocky đã thêm một ví dụ tốt hơn về cách sử dụng bộ đệm cơ bản của vectơ để điền dữ liệu vào.
PooSH

9

Bạn không thể làm điều này với một std::vectormà không tạo một bản sao. std::vectorsở hữu con trỏ mà nó có dưới mui xe và phân bổ không gian thông qua bộ cấp phát được cung cấp.

Nếu bạn có một trình biên dịch hỗ trợ cho C ++ 20, bạn có thể sử dụng std :: span được xây dựng cho chính xác mục đích này. Nó bao bọc một con trỏ và kích thước vào một "thùng chứa" có giao diện chứa C ++.

Nếu không, bạn có thể sử dụng gsl :: span , đó là những gì phiên bản tiêu chuẩn được dựa trên.

Nếu bạn không muốn nhập một thư viện khác, bạn có thể tự thực hiện việc này tùy thuộc vào tất cả các chức năng bạn muốn có.


9

Bây giờ tôi muốn sử dụng std :: vector để truy cập và sửa đổi các giá trị này tại chỗ

Bạn không thể. Đó không phải std::vectorlà những gì dành cho. std::vectorquản lý bộ đệm riêng của nó, cái mà luôn có được từ một bộ cấp phát. Nó không bao giờ có quyền sở hữu bộ đệm khác (ngoại trừ từ một vectơ khác cùng loại).

Mặt khác, bạn cũng không cần phải vì ...

Lý do là tôi cần áp dụng các thuật toán từ (sắp xếp, thay đổi các yếu tố, v.v.) trên dữ liệu đó.

Những thuật toán làm việc trên các vòng lặp. Một con trỏ là một iterator đến một mảng. Bạn không cần một vectơ:

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

Chức năng không giống như các mẫu trong <algorithm>, một số công cụ như phạm vi-cho, std::begin/ std::endvà C ++ 20 dãy không làm việc chỉ với một cặp lặp Mặc dù vậy, trong khi họ làm việc với các container như vectơ. Có thể tạo một lớp bao bọc cho kích thước iterator + hoạt động như một phạm vi và hoạt động với các công cụ này. C ++ 20 sẽ giới thiệu trình bao bọc như vậy vào thư viện chuẩn : std::span.


7

Bên cạnh những gợi ý hay khác về việc std::spanđến trong gsl:span, bao gồm cả spanlớp (nhẹ) của riêng bạn cho đến lúc đó là đủ dễ dàng (hãy sao chép):

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

Đặc biệt lưu ý cũng là phạm vi tăng cường thư viện phạm vi nếu bạn quan tâm đến khái niệm phạm vi chung hơn: https://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/reference /utilities/iterator_range.html .

Các khái niệm phạm vi cũng sẽ đến trong


1
Để làm gì using value_type = std::remove_cv_t<T>;?
Jabberwocky

1
... và bạn đã quên nhà xây dựng : span(T* first_, size_t length) : first(first), length(length) {};. Tôi chỉnh sửa câu trả lời của bạn.
Jabberwocky

@Jabberwocky Tôi chỉ sử dụng khởi tạo tổng hợp. Nhưng nhà xây dựng là tốt.
darune

1
@eerorika tôi đoán bạn đúng, tôi đã xóa các phiên bản không phải là const
darune

1
Điều using value_type = std::remove_cv_t<T>;này chủ yếu là cần thiết nếu được sử dụng với lập trình mẫu (để lấy value_type của 'phạm vi'). Nếu bạn chỉ muốn sử dụng các trình vòng lặp, bạn có thể bỏ qua / loại bỏ nó.
darune

6

Bạn thực sự có thể sử dụng std::vectorcho việc này, bằng cách lạm dụng chức năng cấp phát tùy chỉnh để trả về một con trỏ tới bộ nhớ bạn muốn xem. Điều đó sẽ không được đảm bảo bởi tiêu chuẩn để hoạt động (đệm, căn chỉnh, khởi tạo các giá trị được trả về; bạn sẽ phải chịu khó khi gán kích thước ban đầu và đối với những người không phải là người nguyên thủy, bạn cũng cần phải hack các nhà xây dựng của mình ), nhưng trong thực tế, tôi hy vọng nó sẽ cung cấp đủ các điều chỉnh.

Không bao giờ bao giờ làm điều đó. Thật xấu xí, đáng ngạc nhiên, hacky, và không cần thiết. Các thuật toán của thư viện chuẩn đã được thiết kế để hoạt động tốt với các mảng thô như với các vectơ. Xem các câu trả lời khác để biết chi tiết về điều đó.


1
Hmm, vâng, có thể làm việc với các hàm vectortạo lấy tham chiếu Allocator tùy chỉnh làm hàm tạo arg (không chỉ là tham số mẫu). Tôi đoán bạn cần một đối tượng cấp phát có giá trị con trỏ thời gian chạy trong đó, không phải là tham số mẫu nếu không nó chỉ có thể hoạt động cho các địa chỉ constexpr. Bạn phải cẩn thận không để vectorcác đối tượng xây dựng mặc định bật .resize()và ghi đè lên dữ liệu hiện có; sự không phù hợp giữa một thùng chứa sở hữu như vectơ so với nhịp không sở hữu là rất lớn nếu bạn bắt đầu sử dụng .push_back, v.v.
Peter Cordes

1
@PeterCordes Ý tôi là, đừng chôn cái đèn led - bạn cũng phải phát điên. Theo tôi, điều kỳ lạ nhất về ý tưởng là giao diện cấp phát bao gồm constructphương thức sẽ được yêu cầu ... Tôi không thể nghĩ những trường hợp sử dụng không hack nào sẽ yêu cầu điều đó mới hơn vị trí.
Sneftel

1
Trường hợp sử dụng rõ ràng là để tránh lãng phí thời gian xây dựng các yếu tố bạn sắp viết theo một cách khác, ví dụ như resize()trước khi bạn chuyển một tham chiếu đến thứ gì đó muốn sử dụng nó làm đầu ra thuần túy (ví dụ như một cuộc gọi hệ thống đọc). Trong các trình biên dịch thực hành thường không tối ưu hóa bộ nhớ đó hoặc bất cứ điều gì. Hoặc nếu bạn có một bộ cấp phát sử dụng calloc để lấy bộ nhớ trước, bạn cũng có thể tránh làm bẩn nó theo cách ngu ngốc std::vector<int>theo mặc định khi các đối tượng xây dựng mặc định có mẫu bit hoàn toàn bằng không. Xem Ghi chú trong en.cppreference.com/w/cpp/container/vector/vector
Peter Cordes

4

Như những người khác đã chỉ ra, std::vectorphải sở hữu bộ nhớ cơ bản (thiếu thông tin với bộ cấp phát tùy chỉnh) để không thể sử dụng.

Những người khác cũng đã khuyến nghị khoảng thời gian của c ++ 20, tuy nhiên rõ ràng điều đó đòi hỏi c ++ 20.

Tôi muốn giới thiệu nhịp span-lite . Để trích dẫn phụ đề của nó:

span lite - Một nhịp giống như C ++ 20 cho C ++ 98, C ++ 11 trở lên trong thư viện chỉ có một tiêu đề tệp

Nó cung cấp một khung nhìn không sở hữu và có thể thay đổi (như trong bạn có thể thay đổi các phần tử và thứ tự của chúng nhưng không chèn chúng) và như trích dẫn nói không có phụ thuộc và hoạt động trên hầu hết các trình biên dịch.

Ví dụ của bạn:

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

Bản in

1
2
3
4
5

Điều này cũng có thêm ưu điểm nếu một ngày nào đó bạn chuyển sang c ++ 20, bạn sẽ có thể thay thế điều này nonstd::spanbằng std::span.


3

Bạn có thể sử dụng std::reference_wrappercó sẵn kể từ 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
Điều này thực hiện một bản sao của dữ liệu và đó chính xác là những gì tôi muốn tránh.
Jabberwocky

1
@Jabberwocky Điều này không sao chép dữ liệu. Nhưng đó không phải là những gì bạn yêu cầu trong câu hỏi.
eerorika

@eerorika std::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));chắc chắn điền vào dest_vectorcác giá trị được lấy từ src_table(IOW dữ liệu được sao chép vào dest_vector), vì vậy tôi không nhận được bình luận của bạn. Bạn có thể giải thích?
Jabberwocky

@Jabberwocky nó không sao chép giá trị. Nó lấp đầy vector ithe với các hàm bao tham chiếu.
eerorika

3
@Jabberwocky nó kém hiệu quả hơn trong trường hợp giá trị nguyên.
eerorika
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.