ITHub

Mélyvíz: Építsünk űrhajót C++-ban!

Mélyvíz: Építsünk űrhajót C++-ban!
Szabolcsi Judit
Szabolcsi Judit
| ~5 perc olvasás

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:

  1. erős (strong)
  2. gyenge (weak)
  3. 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​​​​​​​