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

要約

これまで、コースのコレクションはどれも一つの型のデータだけを保持していた。成績、身長、表のマスである。ところが、問題に現れる実体は型の異なるいくつもの特徴をもつ。学生には名前と成績があり、本には書名と著者と出版年がある。このレッスンではレコードを紹介する。これは、そうした特徴を一つの名前のもとにまとめるために、プログラマ自身が struct で定義する複合型である。まずレコードの存在理由、すなわち並列配列のもろさから始め、続いて struct の定義、初期値をもつフィールド、波括弧と指示付き初期化子によるレコードの作成を扱う。次に関数の引数と結果としてのレコードを、さらにレコードのベクターを扱う。これは構造化束縛で走査し、一つのフィールドによってソートし探索するもので、安定なソートの問題もあわせて論じる。最後に、問題の領域を映す型で問題をモデル化する技術で締めくくる。

学習目標

  1. フィールドと初期値をもつ struct を定義し、波括弧による初期化で、指示付き初期化子も使ってレコードを作成する。
  2. 渡し方を選んで、レコードを受け取り返す関数を書く。
  3. 射影を伴う std::ranges::sort、std::ranges::stable_sort、std::ranges::find で、レコードのベクターを一つのフィールドによってソートし探索し、安定なソートとそうでないソートを区別する。
  4. 問題のデータをレコードでモデル化し、どのフィールドと型を選ぶかを正当化する。

データをまとめる

並列配列

あるプログラムが、グループの学生一人一人について、名前と成績と年齢を保持しなければならないとしよう。レッスン 表 までに学んだことで手早く書ける解決は、特徴ごとに一つずつ、計三つのベクターを使い、添字 ii が三つのベクターで同じ人を指すという取り決めを置くことである。names[i]、grades[i]、ages[i] がそれである。これを並列配列という。表は使えない。表のマスはすべて同じ型でなければならず、名前は数ではないからである。

C++ のプログラム
#include <algorithm>#include <cstddef>#include <print>#include <string>#include <vector>​int main(){    const std::vector<std::string> names{"アナ", "ルイス", "エバ"};    std::vector<int> grades{6, 4, 7};    const std::vector<int> ages{19, 22, 20};    std::ranges::sort(grades);    for (std::size_t i{0}; i < names.size(); ++i) {        std::println("{}:成績 {}、{} 歳", names[i], grades[i], ages[i]);    }}
出力
アナ:成績 4、19 歳ルイス:成績 6、22 歳エバ:成績 7、20 歳

このプログラムは学生を成績順に並べたかったのに、成績だけを並べてしまった。警告なしにコンパイルでき、メモリエラーのサニタイザ AddressSanitizer も未定義動作のサニタイザ UndefinedBehaviorSanitizer も報告することは何もない。それでも出力は誤りである。アナの成績は 6 だったのに、4 と表示されている。三つのベクターを結びつけていた取り決めは、コンパイラが読める場所のどこにも書かれていないので、それが破られるのを妨げるものは何もない。ここから並列配列のもろさが出てくる。要素を移動し、追加し、削除する操作はどれも、すべてのベクターで同じように同じ順序で繰り返さなければならない。そして一つ忘れるだけで、データは黙って壊れる。

例ずれていく行と丸ごと動くカード

三つの並列配列と、一人分の三つのデータをそれぞれ収めたカードのベクターとを比べる。

証明

  1. 列 0 は一人の人を表す。names[0]、grades[0]、ages[0] は、アナの名前、成績、年齢である。これらを結ぶのは添字だけである。

  2. std::ranges::sort(grades) は一つの行だけを並べ替える。ほかの二つの行はそれを知らない。列 0 はいまや、アナの名前、ルイスの成績、アナの年齢を組み合わせており、これは誰のものでもない。

  3. 代わりの方法は、要素がカードであるベクターを一つだけ使うことである。各カードは一人分の三つのデータを収めており、それらを結ぶのはもはや添字についての取り決めではなく、カードそのものである。

  4. 成績でソートすると、カードが丸ごと動く。ルイスのカードは名前と年齢を伴って先頭に移る。どのデータもほかから切り離されることはありえない。操作がそれらを別々に見ることがないからである。

そうであれば、問題が求めているのは、値がカードであるような型である。つまり、名前と成績と年齢を含み、全体としてコピーされ、関数に渡され、ベクターに保持され、ソートされる値である。C++ にはそれが既製品として用意されていない。問題ごとにカードが異なるからである。しかし、プログラマがそれを定義することは許されている。その型がレコードである。