std::vector
は連続している必要があると読みました。私の理解では、その要素はメモリ全体に分散するのではなく、一緒に格納する必要があります。たとえば、data()
メソッドを使用して基礎となる連続したメモリを取得するときに、事実を受け入れ、この知識を使用しました。
しかし、ベクターのメモリが奇妙な方法で動作する状況に遭遇しました。
std::vector<int> numbers;
std::vector<int*> ptr_numbers;
for (int i = 0; i < 8; i++) {
numbers.Push_back(i);
ptr_numbers.Push_back(&numbers.back());
}
これにより、いくつかの数値のベクトルと、これらの数値へのポインターのベクトルが得られると期待していました。ただし、ptr_numbers
ポインターの内容を一覧表示するとき、メモリの間違った部分にアクセスしているように、異なる一見ランダムな数字があります。
私はすべてのステップで内容を確認しようとしました:
for (int i = 0; i < 8; i++) {
numbers.Push_back(i);
ptr_numbers.Push_back(&numbers.back());
for (auto ptr_number : ptr_numbers)
std::cout << *ptr_number << std::endl;
std::cout << std::endl;
}
結果はおおよそ次のようになります。
1
some random number
2
some random number
some random number
3
したがって、Push_back()
をnumbers
ベクトルに変更すると、古い要素の位置が変わります。
std::vector
は連続したコンテナであり、その要素が移動するのはなぜですか?おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?
編集:std::vector
はC++ 17以降のみ連続していますか? (私の以前の主張に関するコメントを将来の読者に関連するようにするために。)
おおよそ次のようになります(私のMS Paintの傑作を言い訳)。
スタックにあるstd::vector
インスタンスは、ヒープに割り当てられたバッファへのポインタと、ベクトルのサイズと容量を追跡するためのいくつかの追加変数を含む小さなオブジェクトです。
だから、
Push_back()
をnumbers
ベクトルにすると、古い要素が場所を変更するようです。
ヒープに割り当てられたバッファの容量は固定されています。バッファの最後に到達すると、新しいバッファがヒープ上のどこかに割り当てられ、以前のすべての要素が新しい要素に移動されます。したがって、アドレスは変更されます。
おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?
大体、はい。要素のイテレータとアドレスの安定性は、std::vector
で保証されますifif再割り当ては行われません。
std::vector
はC++ 17以降のみ連続したコンテナであることを認識しています
std::vector
のメモリレイアウトは、標準に初めて登場してから変更されていません。 ContiguousContainer
は、コンパイル時に隣接するコンテナを他のコンテナと区別するために追加された単なる「概念」です。
これは、単一の連続したストレージ(1次元配列)です。容量がなくなるたびに再割り当てされ、保存されたオブジェクトは新しい大きな場所に移動されます。これが、保存されたオブジェクトのアドレスが変化するのを観察する理由です。
C++17
以降ではなく、常にこのようになっています。
ストレージは、償却済みO(1)
Push_back()
の要件を確保するために、幾何学的に成長しています。成長因子は2(キャップn + 1=キャップn+キャップn)C++標準ライブラリのほとんどの実装( GCC 、 Clang 、 STLPort )および1.5(キャップn + 1=キャップn+キャップn/ 2) MSVC バリアント内。
vector::reserve(N)
と十分な大きさのN
で事前に割り当てた場合、新しいオブジェクトを追加しても、保存されているオブジェクトのアドレスは変更されません。
ほとんどの実際のアプリケーションでは、通常、少なくとも32個の要素に事前に割り当てて、すぐ後に続く最初のいくつかの再割り当て(0→1→2→4→8→16)をスキップする価値があります。
速度を落とし、算術成長ポリシーに切り替えることも時々実用的です(キャップn + 1=キャップn+定数)、またはアプリケーションがメモリを浪費したりメモリを使い果たしたりしないように、ある程度大きなサイズの後に完全に停止します。
最後に、列ベースのオブジェクトストレージのようないくつかの実用的なアプリケーションでは、セグメント化されたストレージを完全に支持して連続ストレージのアイデアを放棄する価値があるかもしれません(std::deque
と同じですが、より大きなチャンクがあります)。この方法では、列ごとと行ごとの両方のクエリに対してローカライズされたデータが適切にローカライズされます(ただし、メモリアロケーターの助けが必要になる場合もあります)。
std::vector
が連続したコンテナであるということは、まさにあなたがそれが意味すると思うことを意味します。
ただし、ベクターに対する多くの操作では、メモリ全体を再配置できます。
一般的なケースの1つは、要素を追加すると、ベクトルが大きくなり、すべての要素を別の連続したメモリに再割り当てしてコピーできることです。
それでは、std :: vectorは連続したコンテナであり、その要素はなぜ移動するのでしょうか?おそらく一緒に保存しますが、より多くのスペースが必要な場合は、それらをすべて一緒に移動しますか?
それがまさにその仕組みであり、要素の追加が実際にすべてのイテレータと再配置が行われたときにメモリの場所を無効にする理由です¹。これはC++ 17以降で有効であるだけでなく、それ以降も同様です。
このアプローチにはいくつかの利点があります。
data()
メソッドを使用して、基礎となる生メモリを生ポインタで動作するAPIに渡すことができます。Push_back
、reserve
、またはresize
に新しいメモリを割り当てるコストは、幾何学的な成長が時間とともに償却されるため、一定の時間になります(Push_back
は容量と呼ばれるたびにlibc ++およびlibstdc ++では2倍になり、MSVCでは約1.5倍に増加します)。これらの影響は、このようなメモリレイアウトの欠点と考えることができます。
Push_front
(std::list
またはstd::deque
が提供)などの操作は提供されません(insert(vec.begin(), element)
は機能しますが、高価になる可能性があります¹)、および複数のベクトルの効率的なマージ/スプライシングインスタンス。¹それを指摘してくれた@FrançoisAndrieuxに感謝します。
実際の構造に関しては、std::vector
はメモリ内で次のようになります。
struct vector { // Simple C struct as example (T is the type supplied by the template)
T *begin; // vector::begin() probably returns this value
T *end; // vector::end() probably returns this value
T *end_capacity; // First non-valid address
// Allocator state might be stored here (most allocators are stateless)
};
std::vector
の未加工メモリの内容の印刷:
(何をしているのかわからない場合は、これをしないでください!)
#include <iostream>
#include <vector>
struct vector {
int *begin;
int *end;
int *end_capacity;
};
int main() {
union vecunion {
std::vector<int> stdvec;
vector myvec;
~vecunion() { /* do nothing */ }
} vec = { std::vector<int>() };
union veciterator {
std::vector<int>::iterator stditer;
int *myiter;
~veciterator() { /* do nothing */ }
};
vec.stdvec.Push_back(1); // Add something so we don't have an empty vector
std::cout
<< "vec.begin = " << vec.myvec.begin << "\n"
<< "vec.end = " << vec.myvec.end << "\n"
<< "vec.end_capacity = " << vec.myvec.end_capacity << "\n"
<< "vec's size = " << vec.myvec.end - vec.myvec.begin << "\n"
<< "vec's capacity = " << vec.myvec.end_capacity - vec.myvec.begin << "\n"
<< "vector::begin() = " << (veciterator { vec.stdvec.begin() }).myiter << "\n"
<< "vector::end() = " << (veciterator { vec.stdvec.end() }).myiter << "\n"
<< "vector::size() = " << vec.stdvec.size() << "\n"
<< "vector::capacity() = " << vec.stdvec.capacity() << "\n"
;
}