rawメモリのビューとして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、そのベクトルを操作するためeraseinsert、、は必要ありません。

ライブラリからのデータに基づいてベクトルを構築し、そのベクトルを変更してデータをライブラリにコピーして戻すことができますが、データセットが非常に大きくなる可能性があるため、避けたい2つの完全なコピーになります。


16
あなたが探しているものは架空のstd::vector_viewものですよね?
眠りネロク

3
@眠りネロクはい、たぶん
Jabberwocky

5
そうではありませんstd::vector
Jesper Juhl、


34
標準アルゴリズムはイテレーターで機能し、ポインターイテレーターです。あなたがやることを妨げるものは何もありませんsort(arrayPointer, arrayPointer + elementCount);
cmaster-

回答:


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::span、C ++ 14以降を使用できます。


メソッドをnoexceptとしてマークしたのはなぜですか?例外がスローされないことを保証することはできませんか?
SonneXo


@moooeeeep単なるリンクよりも説明を残した方がいいです。これが頻繁に発生するのを見てきましたが、リンクは将来的に期限切れになる可能性があります。
Jason Liu

63

C ++ 20 std::span

C ++ 20を使用できる場合std::spanは、ポインタと長さのペアを使用できます。これにより、ユーザーは連続した要素のシーケンスを表示できます。それはある種のあるstd::string_view、との両方の間std::spanstd::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を使用できない場合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するのは、ポインターまで減衰しない場合に限られることに注意してください。長さ情報が失われるためです。


7
これが正解です。アルゴリズムは範囲に適用されます。コンテナ(例:std :: vector)は範囲を管理する1つの方法ですが、それだけが方法ではありません。
ピートベッカー

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の範囲などの一部のツールは、イテレータのペアだけでは機能しませんが、ベクトルなどのコンテナでは機能します。範囲として動作し、これらのツールで動作するイテレータ+サイズのラッパークラスを作成することができます。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集計初期化を使用しました。しかし、コンストラクタは問題ありません。
だるね

1
@eerorika私はあなたが正しいと思います、私は非constバージョンを削除しました
darune

1
これusing value_type = std::remove_cv_t<T>;は主に、テンプレートプログラミングで使用する場合に必要です(「範囲」のvalue_typeを取得するため)。イテレータを使いたいだけなら、スキップ/削除できます。
だるね

6

実際にstd::vector、カスタムアロケーター機能を悪用して、表示するメモリへのポインターを返すことで、これをほぼ使用できます。これは、標準では動作することが保証されていません(パディング、配置、戻り値の初期化。初期サイズを割り当てる際には骨の折れる作業が必要です。また、プリミティブ以外の場合は、コンストラクターをハックする必要があります。 )、しかし実際には十分な調整が施されていると思います。

決してそれをすることはありません。醜くて意外でハックで不必要です。標準ライブラリのアルゴリズムは、ベクターと同様に生の配列でも機能するように設計されてます。詳細については、他の回答を参照してください。


1
うーん、そうです。これは、カスタムAllocator参照をコンストラクタの引数(テンプレートパラメータだけでなく)として取るvectorコンストラクタで機能します。テンプレートパラメータとしてではなく、ランタイムポインタ値を含むアロケータオブジェクトが必要になると思います。それ以外の場合は、constexprアドレスに対してのみ機能します。vectorデフォルトのオブジェクトを作成し.resize()て既存のデータを上書きしないように注意する必要があります。あなたが.push_backなどを使用して起動する場合は、ベクター対非所有スパンなどの所有コンテナの間のミスマッチは巨大です
ピーター・コルド

1
@PeterCordesつまり、Ledを埋めないでください。あなたクレイジーである必要があります。私の考えでは、このアイデアの最も奇妙な点は、アロケータインターフェイスにconstruct必要なメソッドが含まれていることです...どのような非ハッキーなユースケースで、placement-newよりもそれが必要になるとは思えません。
Sneftel

1
明らかな使用例は、他の方法で書き込もうとしている要素の作成に時間を浪費しないようにすることです。たとえばresize()、純粋な出力として使用したいものへの参照を渡す前に(たとえば、読み取りシステムコール)。実際には、コンパイラーはそのmemsetなどを最適化しないことがよくあります。または、callocを使用して事前にゼロ化されたメモリを取得するアロケータがある場合std::vector<int>は、すべてゼロのビットパターンを持つオブジェクトをデフォルトで構築するときに、デフォルトでstupid が行う方法のダーティを回避することもできます。en.cppreference.com/w/cpp/container/vector/vectorの
Peter Cordes

4

他の人が指摘したように、std::vector(カスタムアロケータをいじるのを除いて)基礎となるメモリを所有する必要があるため、使用できません。

他の人もc ++ 20のスパンを推奨していますが、明らかにc ++ 20が必要です。

span-liteスパンをお勧めします。副題を引用するには:

span lite-単一ファイルのヘッダーのみのライブラリにあるC ++ 98、C ++ 11以降のC ++ 20に似たスパン

それは非所有で変更可能なビューを提供し(要素とその順序を変更することはできますが、挿入することはできません)、引用が示すように依存関係はなく、ほとんどのコンパイラで動作します。

あなたの例:

#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

1日あなたは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 は(データがにコピーされたIOW)から取得した値でstd::copy(std::begin(src_table), std::end(src_table), std::back_inserter(dest_vector));間違いなくを埋めるので、コメントは得られませんでした。説明してもらえますか?dest_vectorsrc_tabledest_vector
Jabberwocky

@Jabberwockyは値をコピーしません。参照ラッパーでベクターを埋めます。
eerorika

3
それはだ@Jabberwocky 以上の整数値の場合には非効率的。
eerorika
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.