並べ替えの比較関数を、a <= bと書いても、Releaseで動かすと、普通に並びます。落ちない、遅くならない、警告も出ない。ところが、同じコードをDebugで動かすと、invalid comparatorと言われて、止まります。
どちらが正しいのでしょうか。比べるために、<=の比較関数を、std::sortとstd::setで、DebugとReleaseの両方で動かしました。複数のキーで並べるときの、書き間違いも確かめています。
検証は、Visual Studio 2022付属のMSVC(19.44、x64)で、Release(/O2 /MD)とDebug(/Od /MDd)の両方を動かして行いました。他のコンパイラや、標準ライブラリでは、試していません。
比較関数に求められる約束とは?
std::sortやstd::setに渡す比較関数は、次の約束を守る必要があります。正式には、「狭義の弱順序(strict weak ordering)」と呼ばれる決まりです。
・同じ値どうしを比べたら、falseを返す(comp(a, a)はfalse)
・comp(a, b)がtrueなら、comp(b, a)はfalse(両方がtrueにならない)
<は、この約束を守ります。<=は、同じ値どうしを比べたときtrueを返すので、1つ目の約束を破っています。約束を破った比較関数を渡したときの動作は、標準では「未定義動作」で、何が起きるかは、実装しだいです。
実験①:
要素数を変えて、「全部同じ値」と「0〜9のランダム(重複だらけ)」の2通りのvector<int>を、<=と<で並べました。
▼e1_le.cpp
// 比較関数に < ではなく <= を書いて、std::sort を呼ぶ
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <random>
#include <vector>
#ifdef _DEBUG
#include <crtdbg.h>
static int ReportHook(int, char* message, int*)
{
std::printf("[Debugの診断] %s\n", message);
std::fflush(stdout);
std::exit(77);
return 1;
}
#endif
int main(int argc, char** argv)
{
std::setvbuf(stdout, nullptr, _IONBF, 0);
#ifdef _DEBUG
_CrtSetReportHook(ReportHook);
#endif
int n = std::atoi(argv[1]);
bool same = std::strcmp(argv[2], "same") == 0; // same: 全部同じ値 / dup: 0〜9のランダム(重複だらけ)
bool le = std::strcmp(argv[3], "le") == 0; // le: <= / lt: <
std::mt19937 rng(1);
std::vector<int> v(n);
for (int i = 0; i < n; ++i) v[i] = same ? 5 : (int)(rng() % 10);
if (le)
std::sort(v.begin(), v.end(), [](int a, int b) { return a <= b; });
else
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
std::printf("n=%d %s %s : 最後まで実行できた 整列済み=%s\n", n, same ? "全部同じ値" : "重複だらけ", le ? "<=" : "< ", std::is_sorted(v.begin(), v.end()) ? "はい" : "いいえ");
return 0;
}| 比較 | 要素数・中身 | Release(/O2) | Debug(/Od /MDd) |
|---|---|---|---|
<= | 10・32・33・100・1000個(全部同じ値/重複だらけ) | 全部、最後まで実行でき、正しく整列 | 全部、invalid comparatorで停止 |
<= | 100000個(全部同じ値/重複だらけ) | 最後まで実行でき、正しく整列 | (実行していない) |
< | 上と同じ全パターン | 正しく整列 | 正しく整列 |
[Debugの診断] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\xutility(1602) : Assertion failed: invalid comparatorReleaseでは、要素が10個でも100000個でも、<=のまま、正しく整列しました。一方、Debugは、要素が10個のときでも、<=なら停止です。xutility(1602)のAssertion failed: invalid comparatorは、比較関数が約束を破っていることを、Debugの確認機能が見つけたことを示しています。<に直せば、どちらも、正しく動きました。
【重要】Releaseで、壊れる条件を探した結果
「Releaseでも、いつかは壊れるのでは」と考えて、条件を探しました。要素数34〜5033個、値は1〜4種類だけ、というランダムな配列を作り、<=で、3000回並べています。アクセス違反は、SEH(構造化例外)で拾うようにしました。
▼e3_hunt.cpp
// <= の比較関数で、Release でも壊れる条件があるかを、たくさんの乱数で探す
#include <windows.h>
#include <algorithm>
#include <cstdio>
#include <random>
#include <vector>
// SEH(構造化例外)で、アクセス違反を拾う。オブジェクトを持たない関数に分ける
static int sortLE(int* p, int n)
{
__try
{
std::sort(p, p + n, [](int a, int b) { return a <= b; });
return 0;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
return (int)GetExceptionCode();
}
}
int main()
{
std::setvbuf(stdout, nullptr, _IONBF, 0);
std::mt19937 rng(12345);
const int trials = 3000;
int crashes = 0, unsorted = 0, ok = 0;
DWORD firstCode = 0;
int firstN = 0, firstK = 0;
for (int t = 0; t < trials; ++t)
{
int n = 34 + (int)(rng() % 5000);
int k = 1 + (int)(rng() % 4); // 値は 0 〜 k-1 の、k 種類だけ
std::vector<int> v(n);
for (int i = 0; i < n; ++i) v[i] = (int)(rng() % k);
int rc = sortLE(v.data(), n);
if (rc != 0) { ++crashes; if (!firstCode) { firstCode = rc; firstN = n; firstK = k; } }
else if (!std::is_sorted(v.begin(), v.end())) ++unsorted;
else ++ok;
}
std::printf("<= で std::sort を %d 回(要素 34〜5033 個・値は 1〜4 種類)\n", trials);
std::printf(" 正しく整列できた : %d 回\n", ok);
std::printf(" 整列できていない : %d 回\n", unsorted);
std::printf(" 落ちた : %d 回", crashes);
if (firstCode) std::printf(" (最初の例: n=%d, 値の種類=%d, 例外コード 0x%08X)", firstN, firstK, (unsigned)firstCode);
std::printf("\n");
return 0;
}<= で std::sort を 3000 回(要素 34〜5033 個・値は 1〜4 種類)
正しく整列できた : 3000 回
整列できていない : 0 回
落ちた : 0 回結果は、3000回すべて、落ちずに、正しく整列でした。このMSVCのReleaseでは、<=で壊れる条件を、見つけられませんでした。しかし、これは、「<=を書いても安全」という意味では、ありません。標準が、未定義動作と決めているものは、別の実装や、別のバージョンで、範囲の外を読んで落ちることが、あります(今回は、そうした環境で試していないので、確認はできていません)。Releaseで動いたことは、安全の証拠にならない、ということだけが、分かりました。
実験②:setで
並べ替えのsortなら、Releaseで、動いて見えました。では、比較関数の約束に、より強く頼っている、std::setでは、どうでしょうか。std::less_equalを比較に指定したsetに、5を3回入れました。
▼e4_set.cpp
// set の比較に <= (std::less_equal) を使うと、同じ値が見つからなくなる
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <set>
#ifdef _DEBUG
#include <crtdbg.h>
static int ReportHook(int, char* message, int*)
{
std::printf("[Debugの診断] %s\n", message);
std::fflush(stdout);
std::exit(77);
return 1;
}
#endif
int main()
{
std::setvbuf(stdout, nullptr, _IONBF, 0);
#ifdef _DEBUG
_CrtSetReportHook(ReportHook);
#endif
std::set<int, std::less_equal<int>> s;
s.insert(5);
s.insert(5);
s.insert(5);
std::printf("5 を3回 insert したあと : size = %zu (std::set は重複しないはず)\n", s.size());
std::printf("find(5) : %s\n", s.find(5) != s.end() ? "見つかった" : "見つからない");
std::printf("count(5) : %zu\n", s.count(5));
size_t erased = s.erase(5);
std::printf("erase(5) で消えた数 : %zu / 残り size = %zu\n", erased, s.size());
std::set<int> ok;
ok.insert(5); ok.insert(5); ok.insert(5);
std::printf("(参考) 普通の set<int> : size = %zu, find(5) = %s\n", ok.size(), ok.find(5) != ok.end() ? "見つかった" : "見つからない");
return 0;
}5 を3回 insert したあと : size = 3 (std::set は重複しないはず)
find(5) : 見つからない
count(5) : 0[Debugの診断] C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\xutility(1602) : Assertion failed: invalid comparatorReleaseでは、setなのに、5を3回入れると、size = 3になりました。重複しない、というsetの約束が、守られていません。しかも、find(5)は「見つからない」、count(5)は0で、入れたはずの値が、探せません。そのあとのerase(5)は、60秒以上待っても戻らず、強制終了するしかありませんでした。Debugでは、入れた時点で、invalid comparatorで止まっています。
sortでは、静かに通ったReleaseでも、setでは、はっきりと、壊れました。「同じ値でも、trueを返す」比較関数は、同じ値を、「別のもの」として、扱います。setにとっては、一度入れた5が、二度と、5と一致しなくなる、ということです。
実験③:2つのキーで並べるときの、書き間違い
<=を書かなくても、約束が破れる書き方があります。「点数が高い順。同点なら、名前の順」と考えて、2つのキーを||でつないでしまうものです。
▼e2_multikey.cpp
// 「点数が高い順、同点なら名前順」を、間違った書き方と正しい書き方で比べる
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <random>
#include <string>
#include <tuple>
#include <vector>
#ifdef _DEBUG
#include <crtdbg.h>
static int ReportHook(int, char* message, int*)
{
std::printf("[Debugの診断] %s\n", message);
std::fflush(stdout);
std::exit(77);
return 1;
}
#endif
struct Player { std::string name; int score; };
// 間違い: 「点数が上」または「名前が前」なら、前に置く
static bool buggy(const Player& a, const Player& b) { return a.score > b.score || a.name < b.name; }
// 正しい書き方その1: 違うほうのキーだけで決める
static bool good1(const Player& a, const Player& b)
{
if (a.score != b.score) return a.score > b.score;
return a.name < b.name;
}
// 正しい書き方その2: std::tie で、辞書順に比べる(点数は降順なので、左右を入れ替える)
static bool good2(const Player& a, const Player& b) { return std::tie(b.score, a.name) < std::tie(a.score, b.name); }
// 「点数が高い順、同点なら名前順」に、なっているか
static int violations(const std::vector<Player>& v)
{
int bad = 0;
for (size_t i = 1; i < v.size(); ++i)
if (good1(v[i], v[i - 1])) ++bad;
return bad;
}
int main()
{
std::setvbuf(stdout, nullptr, _IONBF, 0);
#ifdef _DEBUG
_CrtSetReportHook(ReportHook);
#endif
Player a{"zed", 10}, b{"amy", 5};
std::printf("buggy(a,b) = %d, buggy(b,a) = %d (a=zed/10点, b=amy/5点)\n", buggy(a, b), buggy(b, a));
std::printf("good1(a,b) = %d, good1(b,a) = %d\n\n", good1(a, b), good1(b, a));
std::mt19937 rng(7);
std::vector<Player> base;
for (int i = 0; i < 200; ++i)
{
char nm[8];
std::snprintf(nm, sizeof nm, "p%03d", i);
base.push_back({nm, (int)(rng() % 10)});
}
std::shuffle(base.begin(), base.end(), rng);
auto v1 = base;
std::sort(v1.begin(), v1.end(), good1);
std::printf("good1 : 並びの食い違い %3d 件\n", violations(v1));
auto v2 = base;
std::sort(v2.begin(), v2.end(), good2);
std::printf("good2 : 並びの食い違い %3d 件 (good1と同じ並び: %s)\n", violations(v2), (v1.size() == v2.size() && std::equal(v1.begin(), v1.end(), v2.begin(), [](const Player& x, const Player& y) { return x.name == y.name; })) ? "はい" : "いいえ");
auto v3 = base;
std::sort(v3.begin(), v3.end(), buggy);
std::printf("buggy : 並びの食い違い %3d 件\n", violations(v3));
return 0;
}buggy(a,b) = 1, buggy(b,a) = 1 (a=zed/10点, b=amy/5点)
good1(a,b) = 1, good1(b,a) = 0
good1 : 並びの食い違い 0 件
good2 : 並びの食い違い 0 件 (good1と同じ並び: はい)
buggy : 並びの食い違い 64 件1行目の、buggy(a,b) = 1, buggy(b,a) = 1に、注目してください。zed(10点)とamy(5点)を比べると、「zedが先」と「amyが先」が、両方ともtrueになっています。a.score > b.scoreがtrueなので、1つ目の条件で、zedが先になりますが、同時に、名前だけを見る2つ目の条件で、amyも先になるからです。

200人を、乱数で並べ替えて、できた並びを調べました。正しい2通りの書き方では、並びの食い違いは0件、間違いの書き方では、64件でした。落ちも、警告も出ずに、結果だけが、静かに崩れます。Debugでは、このときも、invalid comparatorで止まりました。
直し方:
▼直し方
// 1つのキー: 昇順は <、降順は >(<= や >= は使わない)
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
// 複数のキー(その1): 違うキーが見つかった時点で、そのキーだけで決める
bool good1(const Player& a, const Player& b)
{
if (a.score != b.score) return a.score > b.score; // 点数の高い順
return a.name < b.name; // 同点なら、名前順
}
// 複数のキー(その2): std::tie で、タプルの辞書順比較にまかせる
// (降順にしたいキーは、左右を入れ替える)
bool good2(const Player& a, const Player& b)
{
return std::tie(b.score, a.name) < std::tie(a.score, b.name);
}実験③のとおり、どちらの書き方でも、並びの食い違いは0件でした。「同じとき」は、falseを返すと考えるのが、いちばん、間違えにくい覚え方です。
よくある失敗例と対処法
①「同じ値のときも、並べたい」と思って、
同じ値の順番は、そもそも、std::sortが保証しないものです。<=にしても、同じ値の順番は、そろいません。順番を保ちたいなら、std::stable_sortを使います。
②降順にしたくて、!(a < b)と書く
!(a < b)は、a >= bと同じ意味で、<=と同じ約束違反です。降順は、a > bと書きます。
③複数キーの比較を、||でつなぐ
実験③のとおり、a.x < b.x || a.y < b.yは、約束を破ります。違うキーが見つかった時点で、そのキーだけで決める形(ifで分ける)か、std::tieを使います。
④Releaseで動いたので、そのままにする
実験①②のとおり、Releaseでは、静かに通ります。Debugで、invalid comparatorが出ないかを、確かめてから、Releaseに進みます。
注意点
- 比較関数は、同じ値どうしで
falseを返す必要がある。<=・>=は、この約束を破る - このMSVCでは、
<=でも、Releaseのstd::sortは、3000回、落ちずに、正しく整列した。Debugは、要素10個でもinvalid comparatorで停止した std::setにstd::less_equalを使うと、Releaseでもsize = 3・findで見つからない・eraseが戻らない、という結果になった- 複数キーを
||でつなぐと、「aが先」と「bが先」が、両方trueになる。Releaseでは、200人中64か所で、並びが崩れた - 今回の検証は、MSVC 19.44(Visual Studio 2022付属、x64)だけで行った。他のコンパイラ・標準ライブラリで、同じ結果になるとは、限らない
この記事のポイント
- 比較関数は、
<の形で書く。同じ値のときは、false - Debugの
invalid comparatorは、Releaseでは見えない、約束違反の警告 - 複数キーは、
ifで「違うほう」だけを見るか、std::tieを使う setやmapの比較関数が壊れると、入れた値が、見つからなくなる
Debugが止まって、Releaseが通るとき、先に疑うのは、Debugの方ではなく、自分のコードです。動いている側のほうが、約束を破ったまま、通り抜けているだけ、ということが、この実験でも、確かめられました。


