Mélyvíz: Építsünk űrhajót C++-ban!
A C++ minden eddigi verziójában hat operátor szerepelt, amelyek összehasonlításra és rendezésre használhatóak: ==, !=, <, >, <=, >=
. A C++20-ban viszont megjelent az „űrhajó operátor” <=>
, vagy hivatalos nevén a háromértékű összehasonlító operátor (three-way comparison operator). Miért van erre szükség?
Megszoktuk, hogy két „valami” összehasonlításakor három különböző eredmény egyikét kapjuk: vagy a > b
, vagy b > a
, vagy a == b
. Ez a természetes számok körében igaz is, mert a természetes számok rendezett halmazt alkotnak a <=
relációra nézve. Ez azt jelenti, hogy ebben a halmazban bármely két elem összehasonlítható és az összehasonlító reláció reflexív, tranzitív és antiszimmetrikus. A reflexív azt jelenti, hogy saját magával azonos az elem, pl.: 2 <= 2
. A tranzitivitás azt jelenti, hogy ha a <= b
és b <= c
, akkor a <= c
is igaz, pl.: 1 <= 2
és 2 <= 5
, akkor 1 <= 5
. Az antiszimmetria pedig azt jelenti, hogy ha a <= b
és b <= a
is igaz, akkor a == b
kell legyen.
Viszont vannak olyan „valamik”, amiknél nem igaz ez a hármasság, vagyis, hogy bármely két elem <
, >
vagy ==
relációban van egymással. Ennek az az oka, hogy ezek a „valamik” nem rendezett, csak részben rendezett halmazt alkotnak. A részben rendezett halmazoknál nem elvárás, hogy minden elem összehasonlítható legyen és a C++-ban ilyen a lebegőpontos számok halmaza. Létezik ugyanis a NaN
(Not a Number) amikor a művelet eredménye nem valós szám lesz. Ha például négyzetgyököt vonunk egy negatív számból vagy nullával osztunk, akkor NaN
-t kapunk eredményül. És a NaN
-ra 1.f < NaN
, 1.f == NaN
, és 1.f > NaN
állítások mindegyike hamis!
Szóval valahogy egységesen kellene kezelni a rendezett és a részben rendezett halmazokat, amennyire ez lehetséges. Na erre lesz jó az új operátor!
Először is három fajta rendezést különböztettek meg:
- erős (strong)
- gyenge (weak)
- részben rendezett (partial)
Az erős rendezés olyan „valamiknél” jó, amik rendezett halmazt alkotnak. Itt a reláció háromféle eredményt adhat vissza: strong_ordering::greater
, strong_ordering::equal
és strong_ordering::less
.
A gyenge rendezés igazából egy speciális teljes rendezés, ahol nem egyenlőséget vizsgálunk, hanem ekvivalenciát. Például ha kis- és nagybetűre nem érzékeny sztring osztályt írunk, akkor a „Dog”, „DOG”, „dog” és a többi hasonló szó mind egy ekvivalencia osztályba kerülnek. Itt is háromféle eredményt kaphatunk: weak_ordering::equivalent, weak_ordering::less
és weak_ordering::greater
.
A részben rendezett halmazoknál lehetnek nem összehasonlítható elemek, ezért itt négy lehetséges érték van: partial_ordering::less, partial_ordering::greater, partial_ordering::equivalent
és partial_ordering::unordered
. És ezzel megoldottuk a NaN
problémáját, hiszen ha 1.f <=> NaN
írunk, akkor partial_ordering::unordered
eredményt kapunk, ami észszerűbb, mint hogy a NaN
nem kisebb, nem nagyobb és nem egyenlő az 1.f
-fel.
Van még egy előnye a <=> relációnak. Ehhez kezdjük kicsit messzebbről a magyarázatot. A C++ nyelvben műveleti jel átdefiniálással (operator overloading) lehetőségünk van arra, hogy a saját típusainkhoz is használjuk a nyelv beépített műveleti jeleit, akár a fentebb felsorolt hat relációs jelet is. Például csináljunk egy saját, kis- és nagybetűt nem megkülönböztető string osztályt MyString
néven. A típusból létrehozott objektumokat szeretnénk összehasonlítani mind saját típusú másik objektumokkal, mind a const char *
típusú változókkal, akkor összesen 18 darab összehasonlító függvényt kell írnunk:
- MyString, MyString-gel hat művelet (
==, !=, <, >, <=, >=
), - MyString, const char *-gal hat művelet,
- const char *, MyString-gel hat művelet.
Persze valójában csak az ==
és a <
jel jelentését kell kitalálnunk és megírnunk, a többi függvény már építhet ezekre, de ettől még mindet definiálni kell. Ez volt a C++17-ig a helyzet. C++20-tól kezdve már csak négy függvényre van szükségünk:
class CIString {
string s;
public:
bool operator==(const CIString& b) const {
return s.size() == b.s.size() &&
ci_compare(s.c_str(), b.s.c_str()) == 0;
}
std::weak_ordering operator<=>(const CIString& b) const {
return ci_compare(s.c_str(), b.s.c_str()) <=> 0;
}
bool operator==(char const* b) const {
return ci_compare(s.c_str(), b) == 0;
}
std::weak_ordering operator<=>(const char* b) const {
return ci_compare(s.c_str(), b) <=> 0;
}
};
Végül nézzük meg, hogy mi történik a C++20-ban a 2 < 4 kiértékelése esetén! Először ilyet csinál belőle a fordító: (2 <=> 4) < 0
, amiből strong_ordering::less < 0
lesz, ami pedig true
.
Az új operátor által adott eredményeket össze lehet hasonlítani a nullával, és ekkor a hat hagyományos relációs jel által adott true
vagy false
értékeket kapjuk:
strong_ordering::less < 0 // true
strong_ordering::less == 0 // false
strong_ordering::less != 0 // true
strong_ordering::greater >= 0 // true
partial_ordering::less < 0 // true
partial_ordering::greater > 0 // true
partial_ordering::unordered < 0 // false
partial_ordering::unordered == 0 // false
partial_ordering::unordered > 0 // false
Források:
Comparisons - C++20
Részbenrendezett halmaz
Rendezett halmaz
NaN in C++ - What is it and how to check for it?
A szerzőről: Progsuli