CppCon 2015 學習:Comparison is not simple, but it can be simpler.

What is comparison?

這段文字是從計算機科學、編譯器設計或系統優化的角度來定義和評價“比較(comparison)”這個操作:

1. Pervasive(無處不在)

比較操作在編程中極為常見,存在于:

  • 分支語句(if, switch
  • 循環控制(for, while
  • 數據結構操作(搜索、排序)
  • 算法邏輯(分治、剪枝)
  • 優化判斷(向量化是否可行)
    即:程序中幾乎每個決策點都涉及“比較”。

2. Expensive(代價高)

盡管比較看起來是個簡單操作,但在高性能場景中:

  • 它會導致 分支預測失敗(branch misprediction)
  • 導致 指令流水線中斷(pipeline flush)
  • 影響 向量化 或 SIMD 并行效率
  • 增加了 CPU 的條件判斷路徑,難以并行
    在編譯器設計中,減少比較次數或替代成無分支計算(如 mask/bitwise 計算)是常見優化手段。

3. Critical(關鍵性)

比較操作往往決定程序的行為:

  • 決定執行路徑(邏輯分支)
  • 決定輸出結果(排序、選擇)
  • 決定性能瓶頸(復雜度判斷)
    編譯器需要正確理解比較的語義,才能安全地進行重排序、合并、替換等優化。

4. A query against an equivalence or order relation

從理論角度來說,比較操作其實是一個 數學關系判斷

  • Equivalence(等價關系):例如 a == b
  • Order relation(序關系):例如 a < b, a >= b
    在類型系統、邏輯編譯、形式驗證中,這類關系有著嚴格定義(自反性、傳遞性、對稱性等)。

總結:

“比較”是計算中無處不在、不可或缺、但又代價高昂的操作,其本質是對等價或序關系的查詢。

它是優化的核心點,尤其在高性能編程(如向量化、GPU、并行化)和編譯器設計中,如何處理比較決定了程序性能與行為的關鍵路徑。

**“等價關系(equivalence relation)”**,它是數學和計算機科學中非常基礎、非常重要的概念,特別在類型系統、編譯器優化、形式驗證、邏輯推理等方面廣泛應用。

等價關系的定義

一個等價關系(記作 ~)是在一個集合上的二元關系,滿足以下三個基本性質:

1. Reflexive(自反性)

  • 對于任意元素 a
    a ~ a
  • 意思是:每個元素和它自己是等價的。

2. Symmetric(對稱性)

  • 如果 a ~ b,那么也有 b ~ a
  • 即:如果 a 等價于 b,那么 b 也等價于 a。

3. Transitive(傳遞性)

  • 如果 a ~ bb ~ c,那么也有 a ~ c
  • 即:等價關系可以“串起來”,形成閉環。

這三個條件一起,就定義了一個弱等價關系(weak equivalence)

強等價:Congruence(全等 / 可替換性)

除了基本的等價關系,**“相等(=)”**在程序里通常還意味著 更強的等價性,叫作 一致性(congruence),它除了滿足上面三條,還要求:

4. Substitutability(可替代性)

  • 如果 a = b,那么對于任何函數 f()
    f(a) = f(b)
  • 意思是:你可以在任何上下文中把 a 替換成 b,程序行為不會變。
    這在程序分析、編譯器優化中很重要,比如:
int x = 5;
int y = 5;
// 如果 x == y,并且是“可替代”的等價,
// 那么 x + 1 == y + 1 是合法的優化

總結

性質描述
自反性a ~ a
對稱性a ~ b → b ~ a
傳遞性a ~ b 且 b ~ c → a ~ c
可替代性a = b → f(a) = f(b)(等價在任意上下文中成立)

在編譯器/程序優化中的應用:

  • 公共子表達式消除(CSE)
    如果 a = b,可以避免重復計算。
  • 內聯/常量傳播
    如果函數參數值相同,可以直接傳播。
  • 循環不變代碼移動(LICM)
    可替換的值可以移出循環體。
  • 類型系統判斷
    類型之間是否可轉換、安全替換。

序關系(order relation)”的分類與定義,特別是嚴格的偏序關系(strict partial order)、**嚴格的弱序(strict weak order)以及嚴格全序(strict total order)**的性質。

下面為你詳細解釋這些概念:

什么是 Order Relation(序關系)?

序關系是用來對元素“排序”的一種二元關系(通常記作 <)。我們關心的排序可以是:

  • 偏序(partial order)
  • 全序(total order)
  • 弱序(weak order)

1. Strict Partial Order(嚴格偏序)

一個嚴格偏序關系 < 必須滿足以下 3 個性質:

性質含義
Irreflexive(反自反性)不存在元素 a 使得 a < a。
Asymmetric(非對稱性)如果 a < b,那么絕不可能 b < a。
Transitive(傳遞性)如果 a < b 且 b < c,那么一定有 a < c。
示例:

集合 {1, 2, 3} 中的 < 就是嚴格偏序:

  • 1 < 2,2 < 3 → 1 < 3(傳遞)
  • 不存在 2 < 2(反自反)
  • 1 < 2 ? not 2 < 1(非對稱)

2. Strict Weak Order(嚴格弱序)

它是在嚴格偏序的基礎上,增加了“可比較性”的某種弱形式

  • 元素之間的比較可以形成“有序分區”。
  • 所有元素可以分成不相交的等價類(用 ~ 表示),這些等價類之間是有序的。
新增性質:
性質含義
Ordered partitions如果 a < b,則對于任何 c,不是 a < c,就是 c < b(或兩者都)
用途:
標準庫排序函數(如 std::sort)要求的比較函數必須滿足嚴格弱序(例如小于 <)。

3. Strict Total Order(嚴格全序)

這是最強的序關系 —— 所有元素都可以被比較。

新增性質:
性質含義
Trichotomy(三分律)對于任意 a 和 b,恰好有以下三個關系之一成立:
  • a < b
  • a = b
  • b < a |
    所有基本類型(如 int、float)默認使用的是嚴格全序。

對比總結

特性Partial OrderWeak OrderTotal Order
自反性
非對稱性
傳遞性
三分律
可比較性(部分可比)(類間可比)(全部可比)

在編程和算法中的應用:

應用場景所需關系類型
std::sort() 的比較函數Strict Weak Order
數據結構中的優先級(如 heap)Partial or Total Order
排序網絡或拓撲排序Partial Order
編譯器依賴圖分析、任務調度Partial Order

不同算法和應用場景中所需的比較順序(comparison orders)類型。下面是對每一條的詳細解釋與理解:

“What comparison orders are needed?” 的詳細解釋:

1. Topological sort → Partial Order(偏序)

解釋:

拓撲排序是用于有向無環圖(DAG)中的節點排序,表示“先做誰,后做誰”的依賴關系。

需要的順序類型:

  • 只要求部分可比(并非任意兩個元素都可比)
  • 關系必須傳遞、非對稱、反自反
    → 所以是嚴格偏序(strict partial order)

示例:

Task A < Task B < Task C
But Task D is unrelated (not comparable)

2. Normal sort → Weak Order(弱序)

解釋:

標準排序算法(如 std::sort)依賴比較器(如 <)來確定元素之間的順序。

需要的順序類型:

  • 排序中,不同元素可能“等價”(排序時不強制區分)
  • 元素分成等價類,類之間可以排序
    → 所以需要嚴格弱序(strict weak order)

特點:

  • a < b → a 排在 b 前面
  • 若 a 和 b 互不小于 ? 它們是等價的(不是相等)
  • 要求傳遞性、非對稱性、有序分區

3. Indexing → Total Order(全序)

解釋:

用于搜索結構(如二分查找、平衡樹、哈希樹等)時,需要所有元素可以比較出大小先后

需要的順序類型:

  • 所有元素必須兩兩可比較
  • 滿足三分律(trichotomy):要么 a < b,要么 a = b,要么 b < a
    → 所以需要嚴格全序(strict total order)

4. Memoization → Weak Order + Equality

解釋:

Memoization(記憶化)會緩存之前計算過的輸入與結果。

需要的順序類型:

  • 要比較“輸入是否重復” → 需要等價比較(a == b)
  • 若使用結構化輸入,通常用 map/set 來查找緩存,依賴于一個弱序來組織

具體要求:

  • Weak Order: 把等價的輸入組織在一起
  • Equality: 判斷緩存命中是否成立(鍵是否相等)

5. Partitioning the domain → Type-specific needs

解釋:

將問題空間(如一個數組、輸入域)劃分為子區域(如分區處理、并行計算),具體做法依賴于元素類型。

需要的順序類型:

  • 不固定!需要根據類型特性來決定
    • 可能是基于范圍的全序(例如:數值分區)
    • 或基于標簽、屬性的等價關系(例如:分組)

總結對照表:

應用場景所需比較關系類型
拓撲排序(Topological Sort)Strict Partial Order
一般排序(std::sort)Strict Weak Order
索引結構(二叉樹、B樹)Strict Total Order
記憶化緩存(Memoization)Equality + Weak Order
計算域分區(Partitioning)Type-specific(依類型而定)

你提供的內容是在討論比較操作中的異常行為,尤其是當比較對象是**浮點數(floating-point)**時,尤其涉及 NaN(Not a Number) 的情況。下面是詳細的解釋和代碼層面上的理解。

“What can go wrong?” 理解總結:

1. 算法可能失敗

原因: 算法往往基于某種假設(如弱序、等價關系、總序等),一旦比較操作不滿足這些假設,就可能導致算法行為不正確或不確定。

重點問題:NaN 破壞排序的有序性

問題描述:

浮點數中的 NaN 不滿足常規的比較規則:

1 < 3         true
1 < NaN       false
NaN < 3       false
NaN == NaN    false

結論:

由于 NaN 和任何數比較都不成立,因此 operator< 不滿足弱序(weak order)所需的條件,導致排序邏輯出錯。

弱序(Weak Order)要求:

為能用于排序算法(如 std::sort),比較函數 < 必須滿足嚴格弱序(strict weak ordering)

  1. 非自反性:沒有 a < a
  2. 傳遞性:如果 a < bb < c,則 a < c
  3. 等價類傳遞性:如果 !(a < b)!(b < a),則 a 和 b 處于同一等價類
    NaN 無法滿足這些,因為:
  • !(NaN < x) 對所有 x 成立
  • !(x < NaN) 也對所有 x 成立
  • NaN != xNaN != NaN ? 它無法構成等價類,也不等于任何值

示例:NaN排序結果不確定

你給的幾組可能的排序結果都不同:

-NaN, -1.0, +0.0, +1.0, +NaN
+NaN, -1.0, +0.0, +1.0, -NaN
-1.0, -NaN, +0.0, +NaN, +1.0
...

說明排序不具備穩定性、確定性、可重復性,因為 NaN 與其它值“不可比”。

舉個代碼例子 (C++ or Python)

C++ 示例(NaN 參與排序):

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
int main() {std::vector<double> v = { -1.0, std::nan(""), 0.0, 1.0, std::nan("") };std::sort(v.begin(), v.end());for (double d : v) {if (std::isnan(d))std::cout << "NaN ";elsestd::cout << d << " ";}
}

可能輸出結果會因編譯器或排序策略而異。

Python 示例:

import math
arr = [float('-nan'), -1.0, 0.0, 1.0, float('nan')]
arr.sort()
print(arr)

也會得到不確定排序,比如 [nan, -1.0, 0.0, 1.0, nan],具體位置不一定相同。

解決方法建議:

1. 排序前剔除或特殊處理 NaN:

std::sort(vec.begin(), vec.end(), [](double a, double b) {if (std::isnan(a)) return false;if (std::isnan(b)) return true;return a < b;
});

2. 明確約定 NaN 排在開頭或結尾

總結:

項目是否滿足
1 < NaNNaN < 3
構成弱序(weak order)
NaN == NaN
導致排序不穩定
推薦處理方式自定義比較器或剔除 NaN

帶符號的零(signed zero)在排序和等價比較中的特殊行為。我們來逐步分析這段話的含義,并結合代碼示例說明。

問題解析:Sorting with Signed Zero

背景知識(IEEE 754 浮點標準)

  • 在 IEEE 754 中:
    • +0.0-0.0兩個不同的位模式
    • +0.0 == -0.0true
    • 但在某些數學運算中它們行為不同,例如:
      1.0 / +0.0 == +1.0 / -0.0 == -

問題核心:為什么 == 不是真正的等價(congruence)?

一個“等價關系”必須滿足:

  1. 自反性:a == a
  2. 對稱性:a == b → b == a
  3. 傳遞性:a == b, b == c → a == c
  4. 可替代性(substitutability):
    如果 a == b,則對所有函數 f,應有 f(a) == f(b)

但是:

+0.0 == -0.0           //  true
1.0 / +0.0 == +// 
1.0 / -0.0 == -// 
  • 因此:雖然 +0.0 == -0.0,但它們不能替代使用不滿足 substitutability
  • 所以,這種 == 不是一個等價關系(不是 congruence)

對排序的影響:

你舉例的排序結果如:

-1.0, -0.0, -0.0, +0.0, +1.0
-1.0, -0.0, +0.0, -0.0, +1.0
-1.0, +0.0, -0.0, ...

說明即使 -0.0 == +0.0,排序時仍可能因符號差異不穩定,導致多種有效但不一致的排序順序。

C++ 示例:帶符號零排序

#include <iostream>
#include <vector>
#include <algorithm>
int main() {std::vector<double> values = { -1.0, -0.0, +0.0, +1.0 };std::sort(values.begin(), values.end());for (double x : values) {if (std::signbit(x))std::cout << "-0.0 ";else if (x == 0.0)std::cout << "+0.0 ";elsestd::cout << x << " ";}std::cout << "\n";
}

輸出可能是:

-1.0 -0.0 +0.0 +1.0

但順序不保證,除非你加上自定義比較器。

自定義比較器解決排序一致性問題:

bool less_with_sign(double a, double b) {if (a == b)return std::signbit(a) && !std::signbit(b);  // -0.0 comes before +0.0return a < b;
}

總結歸納:

觀察項是否滿足
-0.0 == +0.0true
1 / -0.0 != 1 / +0.0true
== 提供 substitutability不滿足
== 是數學意義上的等價關系?不是 congruence
會影響排序穩定性?
推薦的排序方式?使用自定義 comparator

你這部分內容是探討:在使用帶符號的零(-0.0+0.0)時,為什么常見的比較(如 ==)在**“記憶化(memoization)”場景下是危險的**,其核心在于——== 不具備“可替代性(substitutability)”,所以不能被當作數學意義上的等價關系(congruence)來使用。

理解關鍵:什么是 Memoization?

Memoization(記憶化) 是一種將函數調用結果緩存起來以避免重復計算的技術。
典型邏輯如下:

std::unordered_map<double, double> cache;
double compute(double x) {if (cache.find(x) != cache.end())return cache[x];double result = 1.0 / x;cache[x] = result;return result;
}

問題:-0.0 == +0.0,但值不同?

在 IEEE 754 中:

-0.0 == +0.0            // true
1.0 / -0.0 == -// true
1.0 / +0.0 == +// true

所以你這段話的含義是:

-0.0, +0.0, -0.0 → 1/x → -Inf, +Inf, -Inf

但是如果用 == 匹配緩存,會變成:

-0.0, +0.0, -0.0 → 結果全部緩存為第一次結果(可能是 -Inf)

或者全部為 +Inf,取決于先緩存哪個!

這就是違背了**“相等值可替代”**原則:如果 a == b,則 f(a) == f(b) 應該成立。但這里卻:

-0.0 == +0.0
f(-0.0) != f(+0.0)

所以 == 不是一個等價關系,不能用于 memoization 的 key!

正確做法(區分 -0.0 與 +0.0)

你可以使用:

方法一:使用 std::bit_castmemcmp 進行 bit-level 比較

#include <bit>
#include <unordered_map>
#include <cmath>
struct FloatBitsHash {std::size_t operator()(double x) const {return std::bit_cast<std::size_t>(x);}
};
struct FloatBitsEqual {bool operator()(double a, double b) const {return std::bit_cast<std::size_t>(a) == std::bit_cast<std::size_t>(b);}
};
std::unordered_map<double, double, FloatBitsHash, FloatBitsEqual> cache;

這樣 -0.0+0.0 將會被區分緩存,確保 memoization 正確性。

總結

項目解釋
-0.0 == +0.0是 true,但只表示“數值上相等”
1/-0.0 != 1/+0.0行為不同(-∞ vs +∞)
不能用于 memoization key因為不滿足 substitutability,不是真正的“等價”
正確的比較方法使用 bit-level 比較(如 std::bit_cast)以區分符號
結論== 在浮點上下文中不是數學意義上的 congruence,因此有風險

為什么 NaN 會破壞 memoization 的效果。下面是詳細的理解與代碼分析總結:

問題背景:Memoization with NaN

什么是 Memoization?

記憶化是一種緩存機制,通過“輸入 → 輸出”映射避免重復計算。
但對浮點類型(如 double),當輸入是 NaN 時,memoization 會失效!

問題解釋:NaN != NaN

在 IEEE 754 浮點規范中:

double a = std::nan("");
a == a;   // false

這導致的問題:

在使用 std::unordered_map<double, double> 做 memoization 時,哈希表使用 operator== 作為默認比較函數。
因為:

NaN != NaN

? 每一個 NaN 都無法與已有 NaN 匹配
? 所以每次新 NaN 輸入都 認為是“新的”請求
? 所有 NaN 請求都 無法命中緩存,造成:

兩個嚴重后果:

  1. 計算無法復用 —— 明明計算過,但緩存沒生效
  2. 內存泄漏風險 —— 每個 NaN 會存一個獨立 key,緩存持續增長

示例演示

#include <iostream>
#include <unordered_map>
#include <cmath>
std::unordered_map<double, double> cache;
double compute(double x) {if (cache.find(x) != cache.end()) {std::cout << "Cache hit\n";return cache[x];}std::cout << "Cache miss\n";double result = 1.0 / x;cache[x] = result;return result;
}
int main() {double nan1 = std::nan("1");double nan2 = std::nan("2");compute(nan1);  // misscompute(nan1);  // miss again!compute(nan2);  // also a miss!
}

即使 nan1 是同一個變量,兩次調用也會是“miss”。

解決方案:自定義 Hash 和 Equal

方法一:基于位模式

#include <bit>
struct FloatHash {std::size_t operator()(double x) const {return std::bit_cast<std::size_t>(x);}
};
struct FloatEqual {bool operator()(double a, double b) const {return std::bit_cast<std::size_t>(a) == std::bit_cast<std::size_t>(b);}
};
std::unordered_map<double, double, FloatHash, FloatEqual> cache;

這樣做的好處:

  • +0.0-0.0 被區分
  • NaNNaN 可以匹配(如果 bit pattern 一樣)
  • 可以避免“NaN 存不住”的問題

總結表格

問題點原因后果解決方式
NaN != NaN不滿足 reflexivity無法命中緩存Bit-level 比較方式
== 不是等價關系(equiv)缺乏 substitutability導致緩存系統邏輯錯誤使用自定義 == 或 bit pattern
多個 NaN 被分別緩存NaN 無法彼此相等內存持續增長,計算無法復用自定義 hash 和 equal

內容是在進一步說明 NaN(Not a Number) 的復雜性和對計算行為的影響,尤其是在排序和記憶化(memoization)等需要比較的場景中。以下是詳細理解與分析:

有八千萬億(8 quadrillion)種 NaN?

是真的!

IEEE 754 雙精度浮點數(double 中:

  • 有 64 位,其中 11 位用于 exponent,52 位用于 fraction(尾數)
  • NaN 的定義是:所有 exponent 位為全 1(2047),fraction ≠ 0
    由于 fraction 有 52 位,理論上可組合出:
2^52 - 1 ≈ 4.5 × 10^15 ≈ 4.5 quadrillion

再考慮符號位(正/負 NaN),總數達到 約 9 quadrillion(千兆)NaN 值
所以你看到的「8 quadrillion NaNs」是合理估計。

IEEE 754 的規則:操作保留 NaN 身份

IEEE 754 要求:

運算中傳遞下來的 NaN,不改變其 payload(有效載荷)。

舉例:

double nan1 = std::nan("1");
double nan2 = std::nan("2");
std::vector<double> v = { nan1, 0.0, nan2 };
for (double x : v) {std::cout << 1.0 / x << "\n";
}

結果:

  • 1.0 / nan1nan1
  • 1.0 / 0.0+Inf
  • 1.0 / nan2nan2
    所以:NaN 的 identity 在傳播中被保留

Sorting Behavior Breakdown

排序策略行為說明
default通常實現無法處理 NaN,可能把它們移動或混亂處理。示例:NaN1, NaN2, +Inf, NaN2(注意 NaN2 重復)
weak order所有 NaN 被視為“等價”,會全部歸類為 NaN1(如:NaN1, NaN1, +Inf, NaN1)
total order明確保留每個 NaN identity,遵循完整比較規則(如:NaN1, NaN2, +Inf, NaN2)

問題關鍵點總結

問題點原因后果
存在海量 NaN 值NaN 的尾數部分可編碼大量值NaN identity 會影響排序、比較、緩存行為
IEEE 保留 NaN identity為保留調試信息、錯誤傳播標識在排序或映射操作中行為難預測
NaN 不可比較NaN == NaN 為 false無法用于哈希、等價判斷、memo key
默認排序無法穩定處理 NaN排序算法假定 weak/total order導致結果不一致或重復

推薦實踐建議

  1. 處理 NaN 排序:
    • 使用自定義比較函數,例如把所有 NaN 放到列表最后或統一為一個 sentinel。
  2. 用于緩存場景(memoization)時:
    • 使用 bitwise 比較(例如 memcmp())或強制統一所有 NaN(canonical NaN)。
  3. 避免重復傳播 NaN:
    • 若不需要 payload,可清理為統一值(std::nan(""))或返回默認數值。

浮點數并不是唯一導致比較失敗的問題,其他類型和程序員的假設也可能帶來問題。

具體理解如下:

1. 其他基本類型通常也是全序的,但不一定總是

  • 例如,整數類型通常有全序關系(< 總是定義良好)
  • 但某些奇怪或特殊平臺,可能出現不符合預期的比較行為(比如某些嵌入式平臺或非標準硬件)

2. 程序員假設比較字段有全序關系

  • 很多代碼寫比較操作時,默認字段是完全排序的(total order)
  • 但如果比較函數或運算符不滿足全序性質,就會導致算法失敗或邏輯bug

3. 用戶自定義類型的比較關系很少有明確文檔說明

  • 很多類或結構體,尤其用戶定義的類型,沒嚴格規定比較操作的關系性質(是否滿足反射性、傳遞性、對稱性等)
  • 這會導致排序或查找等算法結果不可預測

4. 多字段比較算法容易出錯

  • 比較多個字段時,如果比較邏輯不符合全序,排序或數據結構(如有序集合、哈希表)可能失敗
  • 例如:部分字段比較無序,或者運算符不滿足三段論(傳遞性)

5. 有些情況下程序員故意讓 operator< 不符合全序

  • 可能為了性能,或者業務邏輯特殊需求
  • 但這會破壞基于比較的算法假設,導致潛在隱患

總結

浮點數不是唯一“坑”——程序員對比較的設計、平臺的實現,以及類型本身的定義,都可能導致比較不滿足全序,進而影響算法正確性。

真正的問題是什么?

  • 工具算法(utility algorithms)默認使用運算符(比如 <==)來做比較,
  • 但這些運算符本身可能并不滿足算法所需的數學性質(如全序關系或等價關系),
  • 而且也不應該強制要求這些運算符必須滿足這些數學性質。

詳細解釋:

  1. 算法依賴于比較操作符:
    許多標準算法(排序、查找、哈希、memoization等)默認直接調用對象的比較運算符。
  2. 比較運算符不保證數學性質:
    但實際中,運算符可能不滿足反射性、對稱性、傳遞性等條件,特別是像浮點數中的 NaN 情況,或用戶自定義類型沒有嚴格定義比較規則。
  3. 不應強加保證給運算符:
    由于運算符可能有不同的語義或實現限制,不能硬性要求它們一定滿足算法所需的所有性質。

結論

問題在于算法設計和運算符設計之間的脫節。算法默認“信任”運算符符合數學性質,但現實中并非如此。需要更靈活和明確的比較接口設計,或者在算法層面引入更健壯的比較策略。

代碼分析

代碼1:

bool operator>=(T a, T b) {return !(a < b);
}
  • 該實現基于 operator<,認為 a >= b 等價于 “不是 a < b”。
  • 這在全序關系下是正確的。
  • 但當 T 僅是部分有序(partial order)時,比如浮點數(float)中存在 NaN,operator< 不能形成全序
  • 因此,這種實現會失敗。例如:
    • 對于 float 中的 NaN,a < ba >= b 都可能為 false,導致邏輯錯誤。
      代碼2:
bool operator>=(T a, T b) {return a > b || a == b;
}
  • 該實現明確用 operator>operator== 來實現“a >= b”。
  • 在部分有序的情況下,operator>operator== 仍然可以更可靠地區分情況。
  • 例如,浮點數中對于 NaN,a == bfalse,但是不會錯誤地判斷 a >= b
  • 這使得該實現對部分有序類型更健壯。

進一步理解

  • **部分有序(Partial order)**的類型,比較操作不滿足所有全序的性質,例如浮點數中 NaN 導致比較不完全。
  • 使用基于單一 operator< 的邏輯(!(a < b))不總是正確的。
  • 需要結合 operator>operator== 來確保比較符合實際語義。

總結

實現方式適用范圍是否可靠備注
return !(a < b);全序對全序關系有效
`return a > ba == b;`部分有序更加健壯適用于包含 NaN 的浮點數等

現在可以做什么?

  • 主動排查已知問題的缺陷
    找出代碼中因比較運算導致的潛在 bug,特別是涉及浮點數 NaN 和特殊值的地方。
  • 顯式檢查數據中的 NaN
    在比較之前,主動檢測數據是否包含 NaN,避免異常比較導致錯誤結果。
  • 驗證運算符的性質
    確保你寫的比較運算符滿足你需要的數學性質(如反射性、對稱性、傳遞性等),不要盲目依賴默認運算符。
  • 完善文檔說明
    明確記錄自定義比較函數的行為和預期性質,便于團隊理解和維護。
  • 編寫新代碼避免比較錯誤
    在新代碼中采用設計良好的比較邏輯,避免使用默認的運算符,減少錯誤風險。
  • 避免與實用算法結合使用不可靠的默認運算符
    不要用不保證數學性質的默認運算符和 std::less 來驅動標準庫算法。
  • 編寫和傳遞明確定義好的比較器
    為標準算法(如 std::sortstd::set 等)明確提供符合預期的比較函數,保證算法行為正確。

關鍵點

  • 認識到默認運算符可能不滿足算法需求,需自定義清晰的比較策略。
  • 對浮點數等特殊類型特別小心,尤其處理 NaN 和 ±0.0。
  • 文檔和測試同樣重要,保障代碼健壯和可維護。

以下是針對浮點數 double 實現的六個布爾比較函數示例,分別體現了部分、有序(弱)、全序以及相應的等價性判斷。重點處理 NaN 和 ±0.0 等特殊情況:

#include <cmath>    // std::isnan, std::signbit
// 判斷兩個數是否都是零(包含正負零)
bool both_zero(double a, double b) {return a == 0.0 && b == 0.0;
}
// 1. 部分序(partial order)小于
// 如果任一參數是NaN,則返回false(表示不可比較)
// 否則返回 d < f
bool partial_less(double d, double f) {if (std::isnan(d) || std::isnan(f)) {return false; // 含NaN時不可比較}return d < f;
}
// 2. 弱序(weak order)小于
// 將所有NaN視為相等,不認為NaN小于或大于其他數
// ±0.0被視為相等,不存在大小關系
bool weak_less(double d, double f) {bool d_nan = std::isnan(d);bool f_nan = std::isnan(f);if (d_nan && f_nan) return false; // NaN間相等if (d_nan) return false;           // NaN不小于任何數if (f_nan) return true;            // 任何數小于NaNif (both_zero(d, f)) return false; // ±0.0視為相等return d < f;
}
// 3. 全序(total order)小于
// 定義對所有浮點數(含NaN和±0.0)的完整排序關系
// NaN被視為大于所有非NaN數,區分±0.0(-0.0 < +0.0)
bool total_less(double d, double f) {if (std::isnan(d)) {if (std::isnan(f)) {// 簡單處理,認為所有NaN相等return false;}return false; // NaN > 非NaN}if (std::isnan(f)) {return true;  // 非NaN < NaN}// 對±0.0區分符號,-0.0被認為小于+0.0if (both_zero(d, f)) {bool d_neg = std::signbit(d);bool f_neg = std::signbit(f);return d_neg && !f_neg;}return d < f;
}
// 4. 部分無序判斷(partial unordered)
// 如果任一參數為NaN,則認為兩者不可比較(無序)
bool partial_unordered(double d, double f) {return std::isnan(d) || std::isnan(f);
}
// 5. 弱等價關系(weak equivalence)
// NaN之間相等,±0.0視為相等,其他按==判斷
bool weak_equivalence(double d, double f) {bool d_nan = std::isnan(d);bool f_nan = std::isnan(f);if (d_nan && f_nan) return true;  // NaN等價if (d_nan || f_nan) return false; // NaN與非NaN不等價if (both_zero(d, f)) return true; // ±0.0等價return d == f;
}
// 6. 全等價(total equality)
// 嚴格比較位級相等,但區分±0.0符號
// NaN統一視為相等(也可擴展為比較NaN payload)
bool total_equal(double d, double f) {if (std::isnan(d) && std::isnan(f)) {return true; // 簡單處理,所有NaN相等}if (both_zero(d, f)) {// ±0.0只有符號相同時才等價return std::signbit(d) == std::signbit(f);}return d == f;
}

說明

  • partial_less:對NaN和非比較情況返回false,不滿足全序。

  • weak_less:將所有NaN視為相等(非排序),同時把±0.0視為相等。

  • total_less:強制對所有浮點數(包括NaN和±0.0)排序,保證全序。

  • partial_unordered:判斷兩個數是否無法比較(包含NaN)。

  • weak_equivalence:弱等價關系,NaN都視為相等,±0.0相等。

  • total_equal:嚴格等價,包括區分±0.0,NaN則默認相等(也可擴展為按payload區分)。

  • 保持一致性
    在定義比較和等價關系時,應保持邏輯一致性,避免混淆。

  • 用“小于”定義等價
    等價關系(a~b)可以用“小于”操作符來定義:

    a~b 當且僅當 不是 a < b 且 不是 b < a

  • 跨層級保持一致
    比如,全序(total_less)成立時,弱序(weak_less)也應成立,保證層級間關系不沖突。

  • 與操作符保持一致
    用戶自定義的 operator< 應該與 weak_less 保持一致,避免在不同代碼路徑出現沖突。

  • 遵循標準
    例如,遵守 IEEE 754 的 totalOrder 規范,保證浮點數比較的標準化和可預測性。
    這些原則有助于確保比較函數的正確性、穩定性以及算法行為的預期。

處理浮點數比較的方法:

  1. operator< 實現了部分序(partial_less)
    • 標準的 < 運算符只保證部分序性質,不能處理 NaN 等特殊情況。
  2. 基于 partial_less 寫 partial_unordered
    • 利用 partial_less 實現一個判斷“無序”(unordered,通常指包含 NaN)的方法。
  3. 編寫 total_less 遵守 IEEE 754 totalOrder
    • total_less 是一個全序比較函數,必須嚴格按照 IEEE 754 totalOrder 規則,保證所有浮點數(包括 NaN、正負零等)都能排序。
  4. 利用 total_less 實現 total_equal
    • total_equal 通過 total_less 組合定義,實現全序下的相等判斷。
  5. 定義 weak_less
    • 弱序比較,適用于某些排序和分區場景。
  6. 利用 weak_less 實現 weak_equivalence
    • 弱等價關系,基于 weak_less 實現。
      總結:
      用分層方法(partial、weak、total)實現浮點比較,逐步完善,避免 operator< 直接作為唯一標準,保證對所有浮點值(特別是特殊值)的一致且正確的處理。

描述了 IEEE 754 totalOrder 標準中浮點數排序的嚴格順序,具體順序如下:

  1. 正的靜默NaN(positive quiet NaNs)
  2. 正的信號NaN(positive signaling NaNs)
  3. 正無窮(positive infinity)
  4. 正實數(positive reals)
  5. 正零(positive zero)
  6. 負零(negative zero)
  7. 負實數(negative reals)
  8. 負無窮(negative infinity)
  9. 負的信號NaN(negative signaling NaNs)
  10. 負的靜默NaN(negative quiet NaNs)
    總結:
    IEEE 754 totalOrder 給出了一個從“最大”到“最小”或從“正向”到“負向”浮點數的全序排序,包含所有普通數和特殊值(如NaN和正負零),這是實現浮點數全序比較的標準依據。

這段內容是把 IEEE 754 totalOrder 中的順序劃分成若干個 弱序(weak order) 的等價類(partition),使得某些浮點值在弱序下被視為等價。具體劃分如下:

  • 所有正的NaN等價(無論是靜默NaN還是信號NaN,都被視為同一個等價類)
  • 正無窮
  • 正實數
  • 所有零等價(包括+0.0和-0.0,視為等價)
  • 負實數
  • 負無窮
  • 所有負的NaN等價
    總結:
    通過這種劃分,可以在弱序的上下文里,避免NaN和±0的復雜差異,將它們歸到等價類,簡化比較和排序的邏輯,同時保證排序算法可以使用這個弱序關系。

這段內容包含兩部分:

1. 利用常見情況優化 weak_less 函數

示例代碼:

bool weak_less(double d, double e) {if (d < e) return true;     // 常見情況直接比較if (e >= d) return false;   // 排除另一種常見情況// 處理其它特殊情況
}
  • 這里的思路是利用浮點數比較中最常見的情況(正常比較)快速返回結果。
  • 避免每次都進入復雜判斷,提升性能。
  • 只有在常規比較不確定時才進行更復雜的處理(例如NaN、±0等特殊情況)。

2. 如何處理郵政編碼(Zip+4)排序

  • Lexical sort(字典序排序)可以提供一個總序(total order),即每個字符串都有一個唯一的位置。
  • 但字典序排序不能解決“同一個地址不同寫法”的問題,比如“5位郵政編碼”和“9位郵政編碼”的關系。
  • 5位郵政編碼應該和它的所有9位擴展碼視為等價(ordered partition),否則不能表示“等價”的分組。
  • 只有把所有9位郵政編碼和它對應的5位郵編視為同一個等價類,才構成嚴格弱序(strict weak order),便于排序和分區。
  • 這種處理方式在比較完整地址時尤其有用。
    總結:
  • 浮點數比較中,先處理常見情況提升效率。
  • 郵政編碼排序需要分層處理,確保等價分組和排序邏輯的合理性,滿足嚴格弱序要求。

這段內容講的是**復合類型(composite types)**的比較方法,重點如下:

如何比較復合類型?

1. Unanimous(全體一致)比較法
  • 意思是:所有字段都必須滿足某種關系(比如都小于)才能得出結論。
  • 產生部分序(partial order)
  • 只有當所有字段都同意時,才能得出“前后”關系。
2. Uncontended(不爭議)比較法
  • 如果任何字段是部分有序的,這種方法就失敗了(不能保證結果)。
  • 否則仍然產生部分序。
  • 這方法類似于對字段比較結果的“非爭議”判斷。
3. Lexicographical(字典序)比較法
  • 這是最常用的復合類型比較方式。
  • 如果任何字段是部分有序,這方法失敗(不能保證全部比較正確)。
  • 否則結果是和所有字段中最弱的順序相同。
  • 字段的順序決定比較順序,先比較第一個字段,如果相等才比較第二個,依此類推。

總結:

  • 復合類型比較很大程度上依賴于字段之間的比較關系
  • 如果字段比較是部分序,那么復合類型的比較也只能是部分序,某些方法甚至失敗。
  • 字典序方法在字段順序上很重要,會影響最終的排序結果。

標準庫(C++標準)可以做什么?

  • 保持向后兼容,不破壞現有代碼。
  • 支持遷移,從直接用操作符比較,轉向使用工具(utility)中的比較函數。
  • 為所有標準類型提供順序(order)和等價(equivalence)函數
  • 但模板類型參數使實現變得復雜。

還有什么問題?

  • 現有代碼常常寫成:
    if (s < t) { ... }
    else if (s > t) { ... }
    else { ... }
    
    但如果用字符串比較函數:
    int cmp = s.compare(t);
    if (cmp < 0) { ... }
    else if (cmp > 0) { ... }
    else { ... }
    
  • 多層抽象時,這種兩兩判斷的復雜度會是指數級的,比如O((2 * fields)^layers),非常低效。
  • 應該支持三值比較器(trinary comparators),用一個函數直接返回“less, equal, greater”三種狀態,降低復雜度至O(fields^layers)。

比較器種類及函數

  • 有三種枚舉類型對應比較等級:
    • partial_ordering : { less, unordered, greater }
    • weak_ordering : { less, equivalent, greater }
    • total_ordering : { less, equal, greater }
  • 提供對應函數接口:
    • partial_order(const T&, const T&)
    • weak_order(const T&, const T&)
    • total_order(const T&, const T&)

如何簡化編碼?

  • 標準庫提供:
    • partial_order 的顯式默認實現(Unanimous方法總是可用)
    • weak_order 的顯式默認實現(Lexicographical方法,需要弱順序字段)
    • total_order 的顯式默認實現(Lexicographical方法,需要全順序字段)
  • 從高級比較(total_order)隱式推導出低級比較(weak_order, partial_order)。

未來發展方向

  • 比較器歸屬工具算法(utility algorithms),由算法層面來調用比較器。
  • 開發者編寫所有布爾比較函數(bool comparators)
  • 標準庫提供標準布爾比較器與三值比較器(trinary comparators),并重載算法以支持它們。
  • 操作符屬于應用領域(application domain),不強制標準定義操作符必須符合所有比較性質
  • 標準庫逐步將算法默認從使用操作符,遷移到使用三值比較器

相關標準提案

  • 參考 C++ 提案 N4367 及其后續文檔,致力于引入和推廣三值比較和新的比較支持。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/86056.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/86056.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/86056.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RocketMQ入門5.3.2版本(基于java、SpringBoot操作)

一、RocketMQ概述 RocketMQ是一款由阿里巴巴于2012年開源的分布式消息中間件&#xff0c;旨在提供高吞吐量、高可靠性的消息傳遞服務。主要特點有&#xff1a; 靈活的可擴展性 海量消息堆積能力 支持順序消息 支持多種消息過濾方式 支持事務消息 支持回溯消費 支持延時消…

VR線上展廳特點分析與優勢

VR線上展廳&#xff1a;特點、優勢與實際應用 VR線上展廳&#xff0c;作為虛擬現實&#xff08;VR&#xff09;技術在展示行業的創新應用&#xff0c;正逐步改變著傳統的展覽方式。通過模擬真實的物理環境&#xff0c;為參觀者提供身臨其境的展覽體驗&#xff0c;成為展示行業…

QT 5.9.2+VTK8.0實現等高線繪制

項目下載鏈接&#xff1a;QT5.9.2VTK8.0實現等高線繪制資源-CSDN文庫 示例如下&#xff1a; 主要代碼如下&#xff1a; #include "vtkRenderer.h" #include "vtkRenderWindow.h" #include "vtkRenderWindowInteractor.h" #include "vtkPo…

MySQL:忘記root密碼

修改配置文件&#xff1a; vi /etc/my.cnf## 修改配置文件 ##[mysqld] skip - grant - tables## 重啟 ##/etc/init.d/mysqld restart ## 或service mysqld restart## 登錄mysqld -u root -p -h 127.0.0.1USE mysql; UPDATE user SET Password password(123456) WHERE User r…

JSP、HTML和Tomcat

9x9上三角乘法表 乘法表的實現 <% page contentType"text/html;charsetUTF-8" language"java" %> <!DOCTYPE html> <html> <head><title>99 上三角乘法表</title><style>body {font-family: monospace;padding…

常用枚舉技巧:基礎(一)

文章目錄 常用枚舉技巧&#xff1a;基礎&#xff08;一&#xff09;LeetCode 1. 兩數之和思路Golang 代碼 LeetCode 2441. 與對應負數同時存在的最大正整數思路Golang 代碼 LeetCode 1512. 好數對的數目思路Golang 代碼 LeetCode 2001. 可互換矩形的對數思路Golang 代碼 LeetCo…

從混亂到秩序:探索管理系統如何徹底改變工作流程

內容摘要 在許多企業與組織中&#xff0c;工作流程混亂是阻礙發展的“絆腳石”。員工們常常被繁瑣的步驟、模糊的職責和溝通不暢等問題搞得焦頭爛額&#xff0c;工作效率低下&#xff0c;錯誤頻發。而與之形成鮮明對比的是&#xff0c;一些引入了先進管理系統的團隊&#xff0…

使用SSE解決獲取狀態不一致問題

使用SSE解決獲取狀態不一致問題 1. 問題描述2. SSE介紹2.1 SSE 的工作原理2.2 SSE 的事件格式規范2.3 SSE與其他技術對比2.4 SSE 的優缺點 3. 實戰代碼 1. 問題描述 目前做的一個功能是上傳多個文件&#xff0c;這個上傳文件是整體功能的一部分&#xff0c;文件在上傳的過程中…

華為×小鵬戰略合作:破局智能駕駛深水區的商業邏輯深度解析

當中國智能電動車競爭進入下半場&#xff0c;頭部玩家的合縱連橫正在重構產業格局。華為與小鵬汽車近日官宣的“戰略合作”&#xff0c;表面看是技術互補的常規操作&#xff0c;實則暗藏改寫行業游戲規則的深層商業邏輯。 一、技術破壁&#xff1a;從“單點突破”到“全棧協同”…

Tailwind CSS 實戰:基于 Kooboo 構建 AI 對話框頁面(六):圖片上傳交互功能

在 《Tailwind CSS 實戰&#xff1a;基于 Kooboo 構建 AI 對話框頁面&#xff08;五&#xff09;》 中&#xff0c;完成了語音交互功能的優化。本文作為該系列教程的第六篇&#xff0c;將聚焦于圖片上傳功能的開發。通過集成圖片上傳與預覽能力&#xff0c;我們將進一步完善 AI…

40. 自動化異步測試開發之編寫異步業務函數、測試函數和測試類(類寫法)

40. 自動化異步測試開發之編寫異步業務函數、測試函數和測試類&#xff08;類寫法&#xff09; 一、類結構設計解析 1.1 基類設計 class Base:async_driver None # &#x1f697; 存儲瀏覽器驅動實例async def get(self, url: str http://secure.smartbearsoftware.com/.…

面向開發者的提示詞工程④——文本推斷(Inferring)

文章目錄 前言一、情感&#xff08;正向/負向&#xff09;二、識別情感類型三、識別憤怒四、從客戶評論中提取產品和公司名稱五、一次完成多項任務 前言 面向開發者的提示詞工程——導讀 在這節課中&#xff0c;你將從產品評論和新聞文章中推斷情感和主題。 舉了個商品評論的例…

java day15 (數據庫)

進入數據庫的學習 DB 因為數據太多了&#xff0c;方便統一管理的軟件 操作就不用改代碼了&#xff0c;直接改數據庫則可&#xff1b; 命令就是sql語句 這些都是關系型數據庫&#xff0c;sql可以控制全部&#xff0c;至于具體的環境我以前就有安裝過了&#xff1b; 理解&am…

國標GB28181設備管理軟件EasyGBS遠程視頻監控方案助力高效安全運營

一、方案背景? 在商業快速擴張的背景下&#xff0c;連鎖店門店數量激增&#xff0c;分布范圍廣。但傳統人工巡檢、電話匯報等管理方式效率低下&#xff0c;存在信息滯后、管理盲區&#xff0c;難以掌握店鋪運營情況&#xff0c;影響企業效率與安全。網絡遠程視頻監控系統可有…

Python 字典(dict)的高級用法與技巧

今天我們繼續深入講解 Python 字典的 高級用法與技巧&#xff0c;包括&#xff1a; defaultdict&#xff1a;帶默認值的字典Counter&#xff1a;快速統計工具字典排序&#xff1a;按鍵或值排序合并字典&#xff08;傳統方式和 Python 3.9 新語法&#xff09;嵌套字典的安全訪問…

動靜態庫的使用(Linux)

1.庫 通俗來說&#xff0c;庫就是現有的&#xff0c;可復用的代碼&#xff0c;例如&#xff1a;在C/C語言編譯時&#xff0c;就需要依賴相關的C/C標準庫。本質上來說庫是一種可執行代碼的二進制形式&#xff0c;可以被操作系統載入內存執行。通常我們可以在windows下看到一些后…

R2ec: 構建具有推理能力的大型推薦模型,顯著提示推薦系統性能!!

摘要&#xff1a;大型推薦模型通過編碼或項目生成將大型語言模型&#xff08;LLMs&#xff09;擴展為強大的推薦工具&#xff0c;而近期在LLM推理方面的突破也同步激發了在推薦領域探索推理的動機。目前的研究通常將LLMs定位為外部推理模塊&#xff0c;以提供輔助性思考來增強傳…

【Java后端基礎 005】ThreadLocal-線程數據共享和安全

&#x1f4da;博客主頁&#xff1a;代碼探秘者 ?專欄&#xff1a;文章正在持續更新ing… ?C語言/C&#xff1a;C&#xff08;詳細版&#xff09; 數據結構&#xff09; 十大排序算法 ?Java基礎&#xff1a;JavaSE基礎 面向對象大合集 JavaSE進階 Java版數據結構JDK新特性…

Tesseract配置參數詳解及適用場景(PyTesseract進行OCR)

在使用 PyTesseract 進行 OCR 時&#xff0c;合理配置參數是提高識別準確率的關鍵。以下是 Tesseract 常用參數的詳細解釋和適用場景。 一、關鍵參數 &#xff08;1&#xff09;頁面分割模式&#xff08;Page Segmentation Mode, --psm&#xff09; 控制 Tesseract 如何分析…

【Zephyr 系列 12】BLE + NVS + 低功耗融合實戰:打造可配置藍牙信標系統

??關鍵詞:Zephyr、BLE 廣播、信標、NVS 參數、低功耗、狀態機、周期喚醒 ??適合人群:希望實現 BLE 信標類產品(定位標簽、資產管理)的開發者 ??預計篇幅:約 5200+ 字 ?? 項目目標 構建一個可廣泛應用于資產標簽、定位信標、設備標識等場景的藍牙廣播模塊,具備:…