C++ Learning

第28回 自作クラスを std::sort で並べる

クラス基本を一通り学んだ直後の 実戦編std::sort は第 6 回で見たとおり「STL の看板商品」ですが、そのままでは自作クラスを並べられませんStudentPoint のような自分で定義した型を sort するには、「どちらが前か」を決める情報が 1 行必要です。operator< を 1 つ書けば動き出す ― この体験が、クラスと STL が接続する最初の瞬間です。

このページで押さえること
✅ 最低限ここだけ覚える
  • 自作クラスはそのままでは std::sort で並ばない
  • operator< を 1 つ定義すれば並ぶようになる
  • 別の順序にしたいときは比較関数を第 3 引数で渡す
  • 「A < B」が真なら A を前に置く、のが約束
⭐ 余裕があれば読む
  • 複数キーでの tie-break(成績 → 出席番号)
  • std::greater<T> で降順
  • 比較関数の形(const T& 2 引数・bool 返す)
  • 厳格弱順序(Strict Weak Ordering)の基本ルール

1. まず触ってみる ― Student を点数順に並べる

Student(氏名と点数)を何人か作って、std::sort点数の低い順に並べてみましょう。並べ方を選ぶと、実行されるコードと結果が切り替わります。

▶ Student を並べ替える

上のデモで選んだ並べ方を実現するコードは、おおむね次のような形になります。細部はこの先のセクションで順に説明していきます。

first_class_sort.cppC++ 最小例
#include <algorithm> #include <vector> #include <string> struct Student { std::string name; int score; // 「この並びでソートされたい」を 1 行で宣言 bool operator<(const Student& other) const { return score < other.score; } }; int main() { std::vector<Student> v = { {"Bob", 80}, {"Alice", 60}, {"Carol", 70} }; std::sort(v.begin(), v.end()); // 上の operator< が使われる // → Alice(60), Carol(70), Bob(80) }
要点は 1 行だけ: bool operator<(const Student&) const を書いておけば、std::sort はそれを使って並びを決めます。「自分のクラス同士の順序を自分で決める」のがここでの役割です。

2. なぜ自作クラスはそのままでは並ばない?

第 6 回で学んだように、std::sortintdoublevector ならそのまま並びます。ところが Student をそのまま渡すと、こんなコンパイルエラーになります。

そのまま sortコンパイルエラー
struct Student { std::string name; int score; }; std::vector<Student> v = { ... }; std::sort(v.begin(), v.end()); // error: no match for 'operator<' // (operand types are Student and Student)
どっちが小さいか」が分からないので並べられない
int なら動くOK
std::vector<int> v = {3, 1, 2}; std::sort(v.begin(), v.end()); // → {1, 2, 3} // int には最初から < がある: // a < b は「a のほうが小さい」で自明
組み込み型には < が言語に標準装備

std::sort がやる仕事は、「たくさんの要素のうち どちらが前か」を繰り返し判定し、並び替えるだけ。その判定は a < b という式で行います。intdouble には最初から < の意味が決まっているので迷いませんが、Student は「名前が小さいほうが前? 点数?」と意味が確定していないので、人間が < の意味を決めてあげる必要があるわけです。

ここで前回(第 23 回)の演算子オーバーロードと繋がります。 operator< を定義するのは「クラスに < の意味を与える」こと。それがそのまま std::sort(や std::setstd::map)の「並び順の基準」として採用されます。

3. operator< を 1 つ書くだけで動き出す

ルールは単純:「a < btrue のとき、ab より前に置く」。Student点数の低い順に並べたいなら、それをそのまま書きます。

operator< を定義メンバ関数で
struct Student { std::string name; int score; bool operator<(const Student& other) const { return score < other.score; // 点数の低いほうが前 } };

見慣れない書き方が 3 つあるので整理します。

これを書いたうえで、std::sort何も変えずにそのまま呼ぶだけで動きます。

使う側呼び出しは同じ
std::vector<Student> v = { {"Bob", 80}, {"Alice", 60}, {"Carol", 70} }; std::sort(v.begin(), v.end()); // 結果: // v[0] = {"Alice", 60} // v[1] = {"Carol", 70} // v[2] = {"Bob", 80}
同じ operator< が他でも効く: std::set<Student>std::map<Student, int> も、キーの大小判定に同じ < を使います。1 回書けば STL の多くの場面で再利用できるのが強力なところ。

4. 別の順序で並べる ― 比較関数を第 3 引数で渡す

operator< は「そのクラスの本命の順序」を 1 つだけ決める場所です。でも実務では「このときだけ点数の高い順にしたい」「今は氏名順」など、その場限りの並べ方をしたいことがあります。

そういうときは、std::sort第 3 引数に比較関数を渡すだけ。operator< は書き換えません。

高い順ラムダで逆順
std::sort(v.begin(), v.end(), [](const Student& a, const Student& b) { return a.score > b.score; // 高いほうが前 }); // → Bob(80), Carol(70), Alice(60)
ラムダは第 46 回で詳しくやります
氏名順別のキーで並べる
std::sort(v.begin(), v.end(), [](const Student& a, const Student& b) { return a.name < b.name; // 辞書順 }); // → Alice, Bob, Carol

渡す関数(ラムダ)の決まりはシンプル:

「同じクラスに対して、場面ごとに違う並びを使い分ける」――これが第 3 引数の役割です。さらに手軽に降順にしたい場合は、標準ライブラリの std::greater<T> を渡すこともできます(ただし operator<> が必要)。

設計の指針: クラスとして 「自然な順序が 1 つある」なら operator< で定義(例:点数、ID、日時)。「場面によって変わる」なら operator< は書かず、毎回ラムダで渡す。どちらが自然かでスタイルを選びます。
ここから先は応用
ここまでで「自作クラスを sort する」核心は押さえました。残りは複数キーで並べたいときの書き方と、厳格弱順序まわりの罠です。急ぐなら次章(確認問題)に進んで OK。

5. 複数キーで tie-break する

「点数の低い順。ただし同点なら出席番号が小さいほうを前」のように、2 つ以上のキーで並べたいときの書き方。これが実務では一番多いパターンです。

tie-break の定石std::tie を使う
#include <tuple> // std::tie のために必要 struct Student { std::string name; int id; int score; bool operator<(const Student& o) const { return std::tie(score, id) < std::tie(o.score, o.id); // 先頭のキー (score) で比較、同点なら次の id で比較 } };

std::tie はタプル型の比較ルールを借りる便利道具。前のキーから順に比べ、違いが出た時点で結果を返す(辞書式比較)という、人間の直感どおりの動きです。自前で if を並べるより ずっと短く・バグりにくい

tie を使わない書き方冗長
bool operator<(const Student& o) const { if (score != o.score) return score < o.score; return id < o.id; }
tie を使う書き方推奨
bool operator<(const Student& o) const { return std::tie(score, id) < std::tie(o.score, o.id); }
キーが 3 つ以上でも同じ: std::tie(a, b, c) < std::tie(o.a, o.b, o.c) と並べれば何段でも tie-break できます。どれかを逆順にしたいときだけ比較を手書きする必要があります。

6. よくある罠と設計のコツ

❌ 罠 1: <= を書いてしまう

std::sort が要求するのは「狭義の小さい」です。<= で書くと、同じ値同士を「どちらも相手より前」と判定してしまい、並び替えアルゴリズムが狂います(実装によっては実行時クラッシュ)。

NG壊れる
bool operator<(const Student& o) const { return score <= o.score; // ← 危険 }
同点だと a<b も b<a も true に
OK正しい
bool operator<(const Student& o) const { return score < o.score; // 狭義 }
同点は「どちらも前でない」
厳格弱順序(Strict Weak Ordering): STL が要求するルール。具体的には (1) a < a は常に false(2) a < b なら b < a は false(3) a < b かつ b < c なら a < c<= は (1)(2) を破るので使えません。

❌ 罠 2: 引数を値渡しにしてコピーが発生

比較関数は大量に呼ばれる(N log N 回)ので、引数の型が重いと一気に遅くなります。Student でも std::string を持っているので値渡しは避け、const Student& で受け取るのが定石。

❌ 罠 3: 末尾の const を忘れる

bool operator<(const Student& o) const の末尾 const がないと、const Student 同士(たとえば const std::vector<Student> から取り出した要素)の比較でエラーになることがあります。比較は読むだけなので必ず const

✅ コツ:非メンバ版で書いても同等

operator<非メンバ関数として書くこともできます(前回第 23 回参照)。どちらでも OK ですが、クラス内部のプライベートメンバを触るならメンバ版、namespace 単位でまとめたいなら非メンバ版、というのが一般的な使い分け。

非メンバ版どちらでも動く
struct Student { std::string name; int score; }; bool operator<(const Student& a, const Student& b) { return a.score < b.score; }

確認クイズ

3 問。正解は選択後に表示されます。

Q1. std::vector<Student> に対して std::sort(v.begin(), v.end()); を呼ぶために必要なのは?

Studentsort というメンバ関数を定義する
Studentoperator< を定義する(または sort の第 3 引数に比較を渡す)
std::vectorstd::list に変える
何もしなくても自動で並ぶ
std::sort は「どちらが前か」を < で判定します。自作クラスにはデフォルトの < が無いので、operator< を書くか、比較関数(ラムダなど)を第 3 引数で渡すかのどちらかが必要です。

Q2. 次の 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。ただし「昇順にしたいのに > を書いた」というバグにはなります。

Q3. Student を「その場限りの並び」で使いたいときの最も素直な書き方は?

operator< を書き換えて、使い終わったら戻す
std::sort の第 3 引数にラムダで比較関数を渡す
Student を継承した StudentByName を作る
新しい std::vector を作り直してから sort
その場限りの並びはラムダ(または関数オブジェクト)を第 3 引数で渡すのが定番。operator< は「このクラスの本命の順序」1 つだけを表現する場所で、場面ごとに書き換えるものではありません。