目錄
一、簡介
二、三向比較的返回類型
2.1 std::strong_ordering
2.2 std::weak_ordering
2.3 std::partial_ordering
三、對基礎類型的支持
四、自動生成的比較運算符函數
4.1 std::rel_ops的作用
4.2 使用<=>
五、兼容他舊代碼
一、簡介
c++20引入了三路比較操作符(或者三向比較)<=>(three-way comparison operator),也叫太空船(spaceship)操作符。
<=>也是一個二元關系運算符,但它不像其他二元比較操作符那樣返回類型是布爾類型,而是根據用戶指明的三種類型之一:partial_ordering、weak_ordering和strong_ordering,定義于標準庫頭文件<compare>中,默認為strong_ordering類型。
- 偏序partial_ordering表達了比較關系中的偏序關系,即給定類的任意兩個對象不一定可比較。例如給定一棵對象樹,假設父節點比子節點大,<=>得到的結果將為greater,但不是任意兩個節點都可比較,此時它們的關系為unordered。對于偏序關系的排序,使用拓撲排序算法將獲得正確的結果。
- 弱序weak_ordering表達了比較關系中的全序關系,即給定類的任意兩個對象都能比較,將既不大于也不小于的關系定義為等價(equivalent)關系。假設長方形類按照面積比較就是弱序關系,長寬分別為2和6的矩形與長寬分別為3和4的比較,面積都為12(既不大于也不小于)?,那么它們是等價的,但不相等是因為可以通過長寬區分出來它們不一樣。標準庫中的std::sort要求關系至少為弱序的才能正確工作。
- 強序strong_ordering與弱序一樣,當對等價關系進行了約束即為相等(equal)關系。考慮正方形類按照面積比較就是強序關系,因為面積一樣的正方形無法像長方形那樣通過外表能區分出來,即它們是相等的。一些查找算法要求關系為強序才能正確工作。
此外<=>的結果也與字符串比較函數strcmp類似,能夠通過正負判斷關系:當結果大于0表示大于關系,等于0表示等價、等于關系,小于0表示小于關系。
顧名思義,三向比較就是在形如lhs <=> rhs的表達式中,兩個比較的操作數lhs和rhs通過<=>比較可能產生3種結果,該結果可以和0比較,小于0、等于0或者大于0分別對應lhs < rhs、lhs == rhs和lhs > rhs。舉例來說:
#include<iostream>int main(int argc,char* argv[]){bool b = 7<=>11 <0;std::cout<<std::boolalpha<<b<<std::endl;b = 7<=>11 ==0;std::cout<<std::boolalpha<<b<<std::endl;b = 7<=>11 >0;std::cout<<std::boolalpha<<b<<std::endl;b = 7<=>7 ==0;std::cout<<std::boolalpha<<b<<std::endl;return 0;
}
輸出:
true
false
false
true
請注意,運算符<=>的返回值只能與0和自身類型來比較,如果同其他數值比較,編譯器會報錯:
#include<iostream>int main(int argc,char* argv[]){bool b = 7<=>11 <100;//編譯失敗,<=>的結果不能與除0以外的數值比較std::cout<<std::boolalpha<<b<<std::endl;return 0;
}
二、三向比較的返回類型
<=>的返回結果并不是一個普通類型,根據標準三向比較會返回3種類型,分別為std::strong_ordering、std::weak_ordering以及std::partial_ordering,而這3種類型又會分為有3~4種最終結果。
2.1 std::strong_ordering
std::strong_ordering類型有3種比較結果,分別為std::strong_ ordering::less、std::strong_ordering::equal以及std::strong_ ordering::greater。表達式lhs <=> rhs分別表示lhs < rhs、lhs == rhs以及lhs > rhs。std::strong_ordering類型的結果強調的是strong的含義,表達的是一種可替換性,簡單來說,若lhs == rhs,那么在任何情況下rhs和lhs都可以相互替換,也就是fx(lhs) == fx(rhs)。
對于基本類型中的int類型,三向比較返回的是std::strong_ordering,例如:
用MSVC編譯運行以上代碼,會在輸出窗口顯示class std::strong_ ordering,刻意使用MSVC是因為它的typeid(x).name()可以輸出友好可讀的類型名稱。
對于有復雜結構的類型,std::strong_ordering要求其數據成員和基類的三向比較結果都為std::strong_ordering。例如:
#include<iostream>struct B{int a;long b;auto operator <=> (const B&) const = default;
};struct D : B{short c;auto operator <=> (const D&) const = default;
};int main(int argc,char* argv[]){D x1,x2;std::cout<<typeid(decltype(x1 <=> x2)).name()<<std::endl;
}
上面這段代碼用MSVC編譯運行會輸出class std::strong_ordering。
請注意,默認情況下自定義類型是不存在三向比較運算符函數的,需要用戶顯式默認聲明,比如在結構體B和D中聲明auto operator <=> (const B&) const = default;和auto operator <=> (const D&)const = default;。
?如果刪除基類的<=>運算符,派生類顯式定義的<=>將被刪除。
如果刪除派生類的<=>,保留基類的<=>,還可以運行。
?對結構體B而言,由于int和long的比較結果都是std::strong_ordering,因此結構體B的三向比較結果也是std::strong_ordering。同理,對于結構體D,其基類和成員的比較結果是std::strong_ordering,D的三向比較結果同樣是std::strong_ordering。
另外,明確運算符的返回類型,使用std::strong_ ordering替換auto也是沒問題的。
2.2 std::weak_ordering
std::weak_ordering類型也有3種比較結果,分別為std::weak_ ordering::less、std::weak_ordering::equivalent以及std::weak_ ordering::greater。std::weak_ordering的含義正好與std::strong_ ordering相對,表達的是不可替換性。即若有lhs == rhs,則rhs和lhs不可以相互替換,也就是fx(lhs) != fx(rhs)。這種情況在基礎類型中并沒有,但是它常常發生在用戶自定義類中,比如一個大小寫不敏感的字符串類:
#include <compare>
#include <string>
#include <iostream>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString {
public:CIString(const char *s) : str_(s) {}std::weak_ordering operator<=>(const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) <=> 0;//strong_ordering返回為weak_ordering類型,實際上發生了類型轉換}
private:std::string str_;
};int main(int argc,char* argv[])
{auto res = 'a'<=>'a';std::cout << typeid(res).name()<<std::endl; //strong_orderingstd::cout << typeid(res<=>0).name()<<std::endl; //strong_orderingstd::cout << typeid( ((std::weak_ordering)res) ).name()<<std::endl; //strong_ordering可以轉為weak_orderingCIString s1{ "HELLO" }, s2{"hello"};std::cout << std::boolalpha << (s1 <=> s2 == 0)<<std::endl; // 輸出為truestd::cout << typeid(s1<=>s2).name()<<std::endl; //weak_orderingreturn 0;
}
?
以上代碼實現了一個簡單的大小寫不敏感的字符串類,它對于s1和s2的比較結果是std::weak_ordering::equivalent,表示兩個操作數是等價的,但是它們不是相等的也不能相互替換。當std::weak_ordering和std::strong_ ordering同時出現在基類和數據成員的類型中時,該類型的三向比較結果是std::weak_ordering,例如:
#include <compare>
#include <string>
#include <iostream>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString {
public:CIString(const char *s) : str_(s) {}std::weak_ordering operator<=>(const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) <=> 0;//strong_ordering返回為weak_ordering類型,實際上發生了類型轉換}
private:std::string str_;
};struct B{int a=0;long b=0;std::strong_ordering operator <=> (const B&) const = default;
};struct D : B{CIString c{""};auto operator <=> (const D&) const = default;
};int main(int argc,char* argv[])
{D w1,w2;std::cout << std::boolalpha << (w1 <=> w2 == 0)<<std::endl; // 輸出為truestd::cout << std::boolalpha << (w1 <=> w2 == std::weak_ordering::equivalent)<<std::endl; // 輸出為truestd::cout << typeid(w1<=>w2).name()<<std::endl; //weak_orderingreturn 0;
}
用MSVC編譯運行上面這段代碼會輸出class std::weak_ordering,因為D中的數據成員CIString的三向比較結果為std::weak_ordering。請注意,如果顯式聲明默認三向比較運算符函數為std::strong_ordering operator <=> (const D&) const = default;,那么一定會遭遇到一個編譯錯誤。
2.3 std::partial_ordering
std::partial_ordering類型有4種比較結果,分別為std::partial_ ordering::less、std::partial_ordering::equivalent、std::partial_ ordering::greater以及std::partial_ordering::unordered。std:: partial_ordering約束力比std::weak_ordering更弱,它可以接受當lhs == rhs時rhs和lhs不能相互替換,同時它還能給出第四個結果std::partial_ordering::unordered,表示進行比較的兩個操作數沒有關系。比如基礎類型中的浮點數:
#include <iostream>int main(int argc,char* argv[])
{std::cout << typeid(decltype(7.7 <=> 11.1)).name();//輸出partial_orderingreturn 0;
}
用MSVC編譯運行以上代碼會輸出class std::partial_ordering。之所以會輸出class std::partial_ordering而不是std::strong_ordering,是因為浮點的集合中存在一個特殊的NaN,它和其他浮點數值是沒關系的:
#include <iostream>int main(int argc,char* argv[])
{std::cout<<std::boolalpha<< ((0.0/0.0 <=> 1.0) == std::partial_ordering::unordered);//輸出truereturn 0;
}
這段代碼編譯輸出的結果為true。
當std::weak_ordering和std:: partial_ordering同時出現在基類和數據成員的類型中時,該類型的三向比較結果是std::partial_ordering,例如:
#include <compare>
#include <string>
#include <iostream>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString {
public:CIString(const char *s) : str_(s) {}std::weak_ordering operator<=>(const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) <=> 0;//strong_ordering返回為weak_ordering類型,實際上發生了類型轉換}
private:std::string str_;
};struct B{int a=0;long b=0;std::strong_ordering operator <=> (const B&) const = default;
};struct D : B{CIString c{""};float u=0.0;auto operator <=> (const D&) const = default;
};int main(int argc,char* argv[])
{D w1,w2;std::cout << std::boolalpha << (w1 <=> w2 == 0)<<std::endl; // 輸出為truestd::cout << std::boolalpha << (w1 <=> w2 == std::partial_ordering::equivalent)<<std::endl; // 輸出為truestd::cout << typeid(w1<=>w2).name()<<std::endl; //partial_orderingreturn 0;
}
用MSVC編譯運行以上代碼會輸出class std::partial_ordering,因為D中的數據成員u的三向比較結果為std::partial_ordering,同樣,顯式聲明為其他返回類型也會讓編譯器報錯。在C++20的標準庫中有一個模板元函數std::common_comparison_category,它可以幫助我們在一個類型合集中判斷出最終三向比較的結果類型,當類型合集中存在不支持三向比較的類型時,該模板元函數返回void。
再次強調一下,std::strong_ordering、std::weak_ordering和`std::partial_ordering`只能與`0`和類型自身比較。深究其原因,是這3個類只實現了參數類型為自身類型和`nullptr_t的比較運算符函數。
三、對基礎類型的支持
- 3.1.對兩個算術類型的操作數進行一般算術轉換,然后進行比較。其中整型的比較結果為std::strong_ordering,浮點型的比較結果為std::partial_ordering。例如7 <=> 11.1中,整型7會轉換為浮點類型,然后再進行比較,最終結果為std::partial_ordering類型。
- 3.2.對于無作用域枚舉類型和整型操作數,枚舉類型會轉換為整型再進行比較,無作用域枚舉類型無法與浮點類型比較:
enum color {red
};auto r = red <=> 11; //編譯成功
auto r = red <=> 11.1; //編譯失敗
- 3.3.對兩個相同枚舉類型的操作數比較結果,如果枚舉類型不同,則無法編譯。
- 3.4.對于其中一個操作數為bool類型的情況,另一個操作數必須也是bool類型,否則無法編譯。比較結果為std::strong_ordering。
- 3.5.不支持作比較的兩個操作數為數組的情況,會導致編譯出錯,例如:
int arr1[5];
int arr2[5];
auto r = arr1 <=> arr2; // 編譯失敗
- 3.6.對于其中一個操作數為指針類型的情況,需要另一個操作數是同樣類型的指針,或者是可以轉換為相同類型的指針,比如數組到指針的轉換、派生類指針到基類指針的轉換等,最終比較結果為std::strong_ordering:
char arr1[5];
char arr2[5];
char* ptr = arr2;
auto r = ptr <=> arr1;
上面的代碼可以編譯成功,若將代碼中的arr1改寫為int arr1[5],則無法編譯,因為int [5]無法轉換為char *。如果將char * ptr = arr2;修改為void * ptr = arr2;,代碼就可以編譯成功了。
四、自動生成的比較運算符函數
4.1 std::rel_ops的作用
標準庫中提供了一個名為std::rel_ops的命名空間,在用戶自定義類型已經提供了==運算符函數和<運算符函數的情況下,幫助用戶實現其他4種運算符函數,包括!=、>、<=和>=。
?代碼:
#include <compare>
#include <string>
#include <iostream>
#include <utility>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString2 {
public:CIString2(const char* s) : str_(s) {}bool operator < (const CIString2& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) < 0;}bool operator== (const CIString2& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) == 0;}
private:std::string str_;
};int main(int argc,char* argv[])
{using namespace std::rel_ops;CIString2 s1( "hello" ), s2( "world" );bool r = true;r = s1 == s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 != s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 > s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 >= s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 < s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 <= s2;std::cout<<std::boolalpha<<r<<std::endl;return 0;
}
輸出:
false
true
false
false
true
true
4.2 使用<=>
不過因為C++20標準有了三向比較運算符的關系,所以不推薦上面這種做法了。C++20標準規定,如果用戶為自定義類型聲明了三向比較運算符,那么編譯器會為其自動生成<、>、<=和>=這4種運算符函數。對于CIString我們可以直接使用這4種運算符函數:
#include <compare>
#include <string>
#include <iostream>
#include <utility>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString {
public:CIString(const char *s) : str_(s) {}// bool operator== (const CIString& b) const {
// return ci_compare(str_.c_str(), b.str_.c_str()) == 0;
// }std::weak_ordering operator<=>(const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) <=> 0;//strong_ordering返回為weak_ordering類型,實際上發生了類型轉換}
private:std::string str_;
};int main(int argc,char* argv[])
{CIString s1( "hello" ), s2( "world" );bool r = true;// r = s1 == s2;// std::cout<<std::boolalpha<<r<<std::endl;// r = s1 != s2;// std::cout<<std::boolalpha<<r<<std::endl;r = s1 > s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 >= s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 < s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 <= s2;std::cout<<std::boolalpha<<r<<std::endl;return 0;
}
輸出
false
false
true
true
那么這里就會產生一個疑問,很明顯三向比較運算符能表達兩個操作數是相等或者等價的含義,為什么標準只允許自動生成4種運算符函數,卻不能自動生成==和=!這兩個運算符函數呢?實際上這里存在一個嚴重的性能問題。在C++20標準擬定三向比較的早期,是允許通過三向比較自動生成6個比較運算符函數的,而三向比較的結果類型也不是3種而是5種,多出來的兩種分別是std::strong_ equality和std::weak_equality。但是在提案文檔p1190中提出了一個嚴重的性能問題。簡單來說,假設有一個結構體:
struct S {std::vector<std::string> names;auto operator<=>(const S &) const = default;
};
它的三向比較運算符的默認實現這樣的:
template<typename T>
std::strong_ordering operator<=>(const std::vector<T>& lhs, const std::vector<T> & rhs)
{size_t min_size = min(lhs.size(), rhs.size());for (size_t i = 0; i != min_size; ++i) {if (auto const cmp = std::compare_3way(lhs[i], rhs[i]); cmp != 0) {return cmp;}}return lhs.size() <=> rhs.size();
}
這個實現對于<和>這樣的運算符函數沒有問題,因為需要比較容器中的每個元素。但是==運算符就顯得十分低效,對于==運算符高效的做法是先比較容器中的元素數量是否相等,如果元素數量不同,則直接返回false:
template<typename T>
bool operator==(const std::vector<T>& lhs, const std::vector<T>& rhs)
{const size_t size = lhs.size();if (size != rhs.size()) {return false;}for (size_t i = 0; i != size; ++i) {if (lhs[i] != rhs[i]) {return false;}}return true;
}
想象一下,如果標準允許用三向比較的算法自動生成==運算符函數會發生什么事情,很多舊代碼升級編譯環境后會發現運行效率下降了,尤其是在容器中元素數量眾多且每個元素數據量龐大的情況下。很少有程序員會注意到三向比較算法的細節,導致這個性能問題難以排查。基于這種考慮,C++委員會修改了原來的三向比較提案,規定聲明三向比較運算符函數只能夠自動生成4種比較運算符函數。由于不需要負責判斷是否相等,因此std::strong_equality和std::weak_ equality也退出了歷史舞臺。對于==和!=兩種比較運算符函數,只需要多聲明一個==運算符函數,!=運算符函數會根據前者自動生成:
#include <compare>
#include <string>
#include <iostream>
#include <utility>int ci_compare(const char* s1, const char* s2)
{while (tolower(*s1) == tolower(*s2++)) {if (*s1++ == '\0') {return 0;}}return tolower(*s1) - tolower(*--s2);
}class CIString {
public:CIString(const char *s) : str_(s) {}bool operator== (const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) == 0;}std::weak_ordering operator<=>(const CIString& b) const {return ci_compare(str_.c_str(), b.str_.c_str()) <=> 0;//strong_ordering返回為weak_ordering類型,實際上發生了類型轉換}
private:std::string str_;
};int main(int argc,char* argv[])
{CIString s1( "hello" ), s2( "world" );bool r = true;r = s1 == s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 != s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 > s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 >= s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 < s2;std::cout<<std::boolalpha<<r<<std::endl;r = s1 <= s2;std::cout<<std::boolalpha<<r<<std::endl;return 0;
}
false
true
false
false
true
true
五、兼容他舊代碼
現在C++20標準已經推薦使用<=>和==運算符自動生成其他比較運算符函數,而使用<、==以及std::rel_ops生成其他比較運算符函數則會因為std::rel_ops已經不被推薦使用而被編譯器警告。那么對于老代碼,我們是否需要去實現一套<=>和==運算符函數呢?其實大可不必,C++委員會在裁決這項修改的時候已經考慮到老代碼的維護成本,所以做了兼容性處理,即在用戶自定義類型中,實現了<、==運算符函數的數據成員類型,在該類型的三向比較中將自動生成合適的比較代碼。比如:
#include <iostream>struct Legacy {int n=0;bool operator==(const Legacy& rhs) const{return n == rhs.n;}bool operator<(const Legacy& rhs) const{return n < rhs.n;}
};struct TreeWay {Legacy m;std::strong_ordering operator<=>(const TreeWay &) const = default;
};int main(int argc,char* argv[])
{TreeWay t1, t2;bool r = t1 < t2;std::cout<<std::boolalpha<<r<<std::endl;return 0;
}
在上面的代碼中,結構體TreeWay的三向比較操作會調用結構體Legacy中的<和==運算符來完成,其代碼類似于:
struct TreeWay {Legacy m;std::strong_ordering operator<=>(const TreeWay& rhs) const {if (m < rhs.m) return std::strong_ordering::less;if (m == rhs.m) return std::strong_ordering::equal;return std::strong_ordering::greater;}
};
需要注意的是,這里operator<=>必須顯式聲明返回類型為std::strong_ ordering,使用auto是無法通過編譯的。