Article

C++標準コンテナの選び方:見えないメモリ確保と参照の寿命

C++標準コンテナは、明示的にnewを書かなくても内部でメモリを確保します。vectorの再確保、dequeやlistの参照保証、spanの所有権を整理し、配置と変更操作からコンテナを選ぶ考え方を解説します。

Share

こはるの読みどころ

残したいのは要素への参照なのか、それとも走査中のイテレータなのか。ここを分けると、コンテナの選び方がぐっと具体的になるよ。

こはるの読みどころ

C++でメモリ確保を調べるとき、newを検索するだけでは見つからない処理があります。ローカル変数として作ったコンテナでも、要素を追加する裏側で別の領域を確保するからです。

2026年10月17日にISO C++ Blogが紹介した「A quick C++ standard container overview」は、この見えにくい確保を入口に標準コンテナを整理しています。気になるのは確保の回数だけではありません。要素が移動したら、それまで持っていた参照はどうなるのでしょうか。

配置、容量、参照の寿命をつなげて考えると、コンテナを選ぶ理由が見えてきます。まずは、コンテナ自身と中身の置き場所を分けるところから始めましょう。

ローカルなvectorでも要素の領域は別に確保される

関数内に宣言したstd::vector<int>では、vectorオブジェクト自身と、要素を格納する領域を分けて考えます。vectorはアロケータを使うコンテナで、標準のstd::allocatorは領域を得るために::operator newを使います。呼び出す側にnewが見えなくても、動的確保は起こるわけですね。アロケータ対応コンテナの要件と標準アロケータの仕様が、この関係を定めています。

一方、std::array<int, 3>は固定個数の要素を自身に持ちます。ただし「arrayは必ずスタック上」と覚えるのは不正確です。arrayをメンバに持つオブジェクトを動的に作れば、その要素もそのオブジェクトの一部になります。arrayの仕様と部分オブジェクトの記憶域期間を合わせると、型の名前だけでは置き場所を決められないと分かります。

動的に増えるコンテナでは、領域のまとめ方も異なります。紹介先のJens Wellerによる解説は、連続領域のvector、ブロック単位のdeque、ノードをつなぐlistなどの典型的な配置を示しています。この配置は仕組みを理解する手掛かりですが、コードが依存する有効性の条件は、各操作の仕様で確かめる必要があります。

vectorのreserveは容量を用意し、要素数は増やさない

std::vector<T>は、Tがbool以外なら要素が連続します。末尾追加はならし定数時間ですが、途中への挿入や削除は線形時間です。この性質から、順番に蓄積して走査する用途では、まずvectorを検討しやすくなります。vectorの仕様

ただし、連続した領域に収まらなくなると再確保が必要です。そこで使うreserve(n)は、少なくともn要素分の容量を用意する操作で、size()は変えません。空のvectorにreserve(3)しても、まだ要素は0個です。容量とreserveの仕様

この違いは、次の小さな操作列で整理できます。各操作が成功することを前提にします。

操作 要素数 容量について分かること
空のvectorにreserve(3) 0 3以上
push_back(10) 1 追加による再確保は不要
続けて20と30をpush_back 3 引き続き再確保は不要
さらに40をpush_back 4 直前の容量が3なら再確保

reserve(3)が容量をちょうど3にする保証はありません。そのため「4個目は必ず再確保」とは言えません。

再確保が起きると、既存要素へのポインタ、参照、イテレータはすべて無効になります。また、再確保しなくても途中に挿入すれば挿入位置以降は無効になります。容量を確保しただけで、あらゆる変更に対して参照が安定するわけではありません。挿入と無効化の規則

dequeとlistは、残したい参照と変更操作で選ぶ

要素への参照を保持しながら追加したい場合は、vector以外の保証を比べる意味があります。ここで分けたいのが、要素そのものを参照することと、イテレータで走査を続けることです。

dequeの先頭・末尾への挿入は、既存要素への参照を無効にしません。一方、イテレータは無効になります。中央への挿入では参照もイテレータも無効になるため、「dequeなら要素を追加しても大丈夫」とひとまとめにはできません。dequeの挿入規則

listでは、挿入によって既存要素への参照やイテレータは無効にならず、削除で無効になるのは削除した要素のものだけです。ただし、1要素の挿入が定数時間という保証は、挿入位置が既に分かっている操作についての話です。位置を探す処理まで省けるわけではありません。listの変更操作

同じ区別はunordered_mapにも必要です。要素をバケットに振り分け直すrehashではイテレータが無効になりますが、要素へのポインタと参照は無効になりません。非順序連想コンテナの要件

参照の保証を満たした候補の中で、実際の処理時間やメモリ使用量を比べるのが自然な順序です。配置だけを見て「ノード型は常に遅い」と決めず、要素数と追加・削除・走査の割合をそろえて測ると、必要な保証に対するコストを判断しやすくなります。

spanで借りても、元のvectorの再確保は防げない

最後に、データを使う側が所有する必要はあるでしょうか。std::span<T>は、別のオブジェクトが所有する連続した要素列を見るための型です。渡すためだけに要素を別コンテナへコピーする必要をなくせますが、元の領域を生かし続ける機能はありません。spanの仕様

vectorからspanを作った後、そのvectorが再確保すれば、spanからの要素アクセスも有効ではなくなります。所有者の破棄も同様です。ビューを作る処理と使い終える処理の間に、所有者のどの操作が入るかを追う必要があります。

例えば、データを集め終えてからspanを渡し、読み取り処理が終わるまでvectorを変更しない流れなら、寿命の条件を整理しやすくなります。構築中のvectorからビューを配り続ける設計とは、確認すべき範囲が変わりますね。

見えないメモリ確保を理解する目的は、newを一律に排除することではありません。要素を誰が所有し、どの操作で領域や参照が変わるかを説明できるようにすることです。その条件が決まれば、vectorの容量調整で足りるのか、別の参照保証が必要なのか、spanで借りるだけでよいのかを判断できます。

出典

Share

Related Articles

カテゴリやタグが近い記事を続けて読めるように並べています。