std::sort で並べるクラス基本を一通り学んだ直後の 実戦編。std::sort は第 6 回で見たとおり「STL の看板商品」ですが、そのままでは自作クラスを並べられません。Student や Point のような自分で定義した型を sort するには、「どちらが前か」を決める情報が 1 行必要です。operator< を 1 つ書けば動き出す ― この体験が、クラスと STL が接続する最初の瞬間です。
std::sort で並ばないoperator< を 1 つ定義すれば並ぶようになるstd::greater<T> で降順const T& 2 引数・bool 返す)Student を点数順に並べるStudent(氏名と点数)を何人か作って、std::sort で点数の低い順に並べてみましょう。並べ方を選ぶと、実行されるコードと結果が切り替わります。
上のデモで選んだ並べ方を実現するコードは、おおむね次のような形になります。細部はこの先のセクションで順に説明していきます。
bool operator<(const Student&) const を書いておけば、std::sort はそれを使って並びを決めます。「自分のクラス同士の順序を自分で決める」のがここでの役割です。
第 6 回で学んだように、std::sort は int や double の vector ならそのまま並びます。ところが Student をそのまま渡すと、こんなコンパイルエラーになります。
std::sort がやる仕事は、「たくさんの要素のうち どちらが前か」を繰り返し判定し、並び替えるだけ。その判定は a < b という式で行います。int や double には最初から < の意味が決まっているので迷いませんが、Student は「名前が小さいほうが前? 点数?」と意味が確定していないので、人間が < の意味を決めてあげる必要があるわけです。
operator< を定義するのは「クラスに < の意味を与える」こと。それがそのまま std::sort(や std::set、std::map)の「並び順の基準」として採用されます。
operator< を 1 つ書くだけで動き出すルールは単純:「a < b が true のとき、a を b より前に置く」。Student を点数の低い順に並べたいなら、それをそのまま書きます。
見慣れない書き方が 3 つあるので整理します。
bool operator<(...) ― 関数名が記号 < なだけ。前回出てきた「演算子は関数名が記号」の形そのものconst Student& other ― 「比較相手」を参照で受け取って変更しない(コピーを避けるのが定石)const ― 「この関数は this(=左辺の Student)を書き換えません」という宣言。const な Student 同士でも比較できるようにするために必要これを書いたうえで、std::sort は何も変えずにそのまま呼ぶだけで動きます。
operator< が他でも効く: std::set<Student> や std::map<Student, int> も、キーの大小判定に同じ < を使います。1 回書けば STL の多くの場面で再利用できるのが強力なところ。
operator< は「そのクラスの本命の順序」を 1 つだけ決める場所です。でも実務では「このときだけ点数の高い順にしたい」「今は氏名順」など、その場限りの並べ方をしたいことがあります。
そういうときは、std::sort の第 3 引数に比較関数を渡すだけ。operator< は書き換えません。
渡す関数(ラムダ)の決まりはシンプル:
const T& が定石)を受け取り、bool を返す。true なら、1 つ目の引数を前に置く。「同じクラスに対して、場面ごとに違う並びを使い分ける」――これが第 3 引数の役割です。さらに手軽に降順にしたい場合は、標準ライブラリの std::greater<T> を渡すこともできます(ただし operator< や > が必要)。
operator< で定義(例:点数、ID、日時)。「場面によって変わる」なら operator< は書かず、毎回ラムダで渡す。どちらが自然かでスタイルを選びます。
「点数の低い順。ただし同点なら出席番号が小さいほうを前」のように、2 つ以上のキーで並べたいときの書き方。これが実務では一番多いパターンです。
std::tie はタプル型の比較ルールを借りる便利道具。前のキーから順に比べ、違いが出た時点で結果を返す(辞書式比較)という、人間の直感どおりの動きです。自前で if を並べるより ずっと短く・バグりにくい。
std::tie(a, b, c) < std::tie(o.a, o.b, o.c) と並べれば何段でも tie-break できます。どれかを逆順にしたいときだけ比較を手書きする必要があります。
<= を書いてしまうstd::sort が要求するのは「狭義の小さい」です。<= で書くと、同じ値同士を「どちらも相手より前」と判定してしまい、並び替えアルゴリズムが狂います(実装によっては実行時クラッシュ)。
a < a は常に false、(2) a < b なら b < a は false、(3) a < b かつ b < c なら a < c。<= は (1)(2) を破るので使えません。
比較関数は大量に呼ばれる(N log N 回)ので、引数の型が重いと一気に遅くなります。Student でも std::string を持っているので値渡しは避け、const Student& で受け取るのが定石。
const を忘れるbool operator<(const Student& o) const の末尾 const がないと、const Student 同士(たとえば const std::vector<Student> から取り出した要素)の比較でエラーになることがあります。比較は読むだけなので必ず const。
operator< は非メンバ関数として書くこともできます(前回第 23 回参照)。どちらでも OK ですが、クラス内部のプライベートメンバを触るならメンバ版、namespace 単位でまとめたいなら非メンバ版、というのが一般的な使い分け。
3 問。正解は選択後に表示されます。
std::vector<Student> に対して std::sort(v.begin(), v.end()); を呼ぶために必要なのは?Student に sort というメンバ関数を定義するStudent に operator< を定義する(または sort の第 3 引数に比較を渡す)std::vector を std::list に変えるstd::sort は「どちらが前か」を < で判定します。自作クラスにはデフォルトの < が無いので、operator< を書くか、比較関数(ラムダなど)を第 3 引数で渡すかのどちらかが必要です。operator< のうち、std::sort が壊れる書き方はどれ?return score < o.score;return std::tie(score, id) < std::tie(o.score, o.id);return score <= o.score;return score > o.score;(降順にしたいだけ)<= は「同じ値に対して両方向とも true」になり、厳格弱順序(SWO)を破ります。> は SWO を満たす(意味として順序が逆になるだけ)ので OK。ただし「昇順にしたいのに > を書いた」というバグにはなります。Student を「その場限りの並び」で使いたいときの最も素直な書き方は?operator< を書き換えて、使い終わったら戻すstd::sort の第 3 引数にラムダで比較関数を渡すStudent を継承した StudentByName を作るstd::vector を作り直してから sortoperator< は「このクラスの本命の順序」1 つだけを表現する場所で、場面ごとに書き換えるものではありません。