コンテンツへスキップ
Topos Uranos

要約

std::array は、要素がいくつになるかをコンパイル時に知っている必要がある。ところが、たいていの問題は、データが届くまでそれを教えてくれない。このレッスンでは、データに応じて伸びる列である std::vector を紹介する。その作り方、push_back による要素の追加、そしてそれを使って長さのわからない系列を読む方法である。続いて、コレクションを扱うあらゆるプログラミングに現れる三つの定番の走査(集約・抽出・変換)を、ループとライブラリのアルゴリズムの両方で書く。さらに、ベクターを関数に渡す方法と、新しいベクターを返すのが安くつく理由を述べる。最後にベクターの内部を扱う。サイズと容量、いっぱいになると二倍の大きさの別のメモリブロックに置き換えられるブロック、そしてそのために解放済みのメモリを指したまま残りうる要素への参照である。

学習目標

  1. std::vector を作り、push_back で要素を追加して走査し、それを使って長さのわからないデータの系列を読み取ることができる。
  2. ベクターを読むためには const 参照で、変更するためには参照で受け取り、結果が列であるときには新しいベクターを返す関数を書くことができる。
  3. 集約・抽出・変換の走査を実装し、問題の中にそれに対応するパターンを見つけ、ライブラリが許すときには std::ranges::count、std::ranges::max、std::ranges::fold_left で短く書くことができる。
  4. ベクターのサイズと容量の違いと、別の要素を追加すると要素への参照が無効になりうる理由を、例を挙げて説明できる。

伸びる列

ベクターを作って満たす

レッスン 配列 では、添字付きの列を紹介した。同じ型の要素が一つずつ並び、零から番号が振られたものである。その固定サイズの現代的な形 std::array<int, 7> は型の中にサイズをもち、コンパイラはそれを知っていなければならない。ところが、クラスの成績の平均を出すプログラムを書く人は、学生が何人になるかを知らない。ここから、サイズがもう一つのデータであり、実行中に変わる列が必要になる。それがベクター std::vector<T> であり、ヘッダ <vector> にある。ここで T は要素の型である。

C++ のプログラム
#include <print>#include <vector>​int main(){    std::vector<int> squares{};    std::println("開始時:サイズ {}、空:{}", squares.size(), squares.empty());    for (int k{1}; k <= 5; ++k) {        squares.push_back(k * k);    }    std::println("{}", squares);    std::println("サイズ {}、先頭 {}、末尾 {}", squares.size(), squares[0], squares.back());    squares[1] = 0;    squares.pop_back();    std::println("{}", squares);}
出力
開始時:サイズ 0、空:true[1, 4, 9, 16, 25]サイズ 5、先頭 1、末尾 25[1, 0, 9, 16]

6 行目は整数の空のベクターを作る。配列と違って、要素がいくつになるかを前もって告げる必要はない。size() は要素がいくつあるかを返し、empty() は一つもないかどうかを告げる。8 行目から 10 行目のループは push_back で五つの平方数を追加する。この関数は引数を末尾に置き、サイズを一つ増やす。11 行目は、前回のレッスンで配列に使った範囲の書式化でベクター全体を書き出す。添字によるアクセスはいつもどおりであり、squares.back() は squares[squares.size() - 1] の略記である。13 行目は既存の要素に代入し、14 行目は pop_back で末尾の要素を取り除く。このレッスンの操作をまとめると、次のとおりである。

  • v.push_back(x) は x を末尾に追加する。v.pop_back() は末尾の要素を取り除く(ベクターが空なら未定義動作である)。
  • v.size() はサイズであり、その型はレッスン 配列 で紹介した、サイズを表す符号なし整数 std::size_t である。v.empty() は v.size() == 0 と同値である。
  • v[i] と v.at(i) は添字 i の要素を与える。添字は i < v.size() を満たさなければならない。v.front() と v.back() は先頭と末尾の要素を与え、ベクターが空でないことを要求する。
  • v.resize(n) はベクターの要素を n 個にする。末尾の余分な要素を取り除くか、空の波かっこで初期化したのと同じように初期化された要素を末尾に追加する(数なら零)。

配列の場合と同じく、v[i] はどんな検査も保証しないが、v.at(i) は保証する。添字が 7 で要素が五つなら、添字とサイズを示すメッセージ vector::_M_range_check: __n (which is 7) >= this->size() (which is 5) を出してプログラムを止める。この停止を引き起こす仕組みである例外は、C++ プログラミング II で学ぶ。

波かっこと丸かっこ

このコースではすべてを波かっこで初期化するが、ベクターは、波かっこを丸かっこに替えると結果が変わる、コースで最初の型である。波かっこでは、書いた値が要素になる。丸かっこでは、最初の数が要素の個数であり、二つ目の数があれば、それが各要素の値になる。

C++ のプログラム
#include <print>#include <vector>​int main(){    const std::vector<int> a{5};    const std::vector<int> b(5);    const std::vector<int> c{3, 7};    const std::vector<int> d(3, 7);    std::println("{} {} {} {}", a, b, c, d);}
出力
[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]

a は要素を一つ、すなわち 5 をもつ。b は零を五つ、c は 3 と 7、d は 7 を三つもつ。したがって、波かっこの中のリストは「これが要素である」と言い、丸かっこは、零で始まるカウンタのベクターのように、同じ値の n 個の複製を求めるときのためにとっておかれる。

空のベクターの末尾

空のベクターは正当な場合であり(データのない入力、だれも通らない抽出)、サイズを含むどの式もこの場合について確かめなければならない。最も油断ならないのは v.size() - 1 である。v.size() は符号なし整数なので、それが 0 のときに 1 を引くと −1-1 にはならず、レッスン 整数とその表現 で見たように一周して、std::size_t の最大値になる。

C++ のプログラム
#include <cstddef>#include <print>#include <vector>​int main(){    const std::vector<int> values{};    std::println("values.size() - 1 = {}", values.size() - 1);    // 誤った上限:ベクターが空のとき、条件は真になる。    for (std::size_t i{0}; i <= values.size() - 1; ++i) {        std::println("{}", values[i]);    }}
出力
values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.

システムがシグナル 6 でプログラムを停止しました。

出力の最初の行は 18 446 744 073 709 551 61518\,446\,744\,073\,709\,551\,615、すなわち 264−12^{64} - 1 であり、これはこのプラットフォームでの std::size_t の最大値である。ここから、ループの条件 0 <= 18446744073709551615 は真であり、最初の周は要素 0 をもたないベクターの values[0] を読むことになる。未定義動作である。何が観察されるかは、配列の場合と同じくオプションによって変わる。最適化なしでコンパイルすると、GCC 15 のライブラリは operator[] の添字を検査し、__n < this->size() と告げるアサーションでプログラムを止める。最適化するとこの検査は消え、ループは間に合うように止まらず、要素がまったくない場所を読もうとする(Linux で -O2 を付けて確認した。空のベクターはブロックをもたないので、プログラムはセグメンテーション違反で終わり、原因を説明するメッセージは出ない)。直すには、上限を引き算なしで i < values.size() と書けばよい。これは空のベクターでは最初から偽である。引き算が必要に見える場所では、先に values.empty() を問うことである。