Biblioteka standardowa C++ w wielu miejscach oczekuje od typów, z którymi pracuje implementacji operatora< (alternatywą jest podanie własnego komparatora). Jest tak chociażby w std::sort, std::map lub std::binary_serch. Weźmy za przykład następującą klasę:
struct person { string first_name; string last_name; string city; int year_born; bool is_male; }; |
Na przykład aby posortować std::vector<person>, powinniśmy zaimplementować operator<, który spełnia strict weak ordering:
- zawsze !(a < a),
- jeśli a < b, to !(b < a),
- jeśli !(a < b) oraz !(b < a) to a i b są ekwiwalentne,
- jeśli a < b oraz b < c to a < c.
Na nowicjusza
Czyli wedle zasady napakujmy ile się da kodu, jeśli ciężko będzie się to czytać to tylko dlatego, że jesteśmy takimi niesamowitymi hakerami:
bool operator<(person const& l, person const& r) { if ((l.last_name < r.last_name) || (!(r.last_name < l.last_name) && l.first_name < r.first_name) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && l.year_born < r.year_born) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && !(r.year_born < l.year_born) && l.is_male < r.is_male) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && !(r.year_born < l.year_born) && l.is_male < r.is_male && l.city < r.city)) { return true; } else { return false; } } |
Ten kod jest nie tylko nieczytelny, jest także błędny (przykład). Stosując metodę Kopiego-Pejsta programista zapomniał odwrócić warunek, tak wygląda wersja poprawna poprawiona:
bool operator<(person const& l, person const& r) { if ((l.last_name < r.last_name) || (!(r.last_name < l.last_name) && l.first_name < r.first_name) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && l.year_born < r.year_born) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && !(r.year_born < l.year_born) && l.is_male < r.is_male) || (!(r.last_name < l.last_name) && !(r.first_name < l.first_name) && !(r.year_born < l.year_born) && !(r.is_male < l.is_male) && // o tutaj l.city < r.city)) { return true; } else { return false; } } |
Oczywiście nadal nie można temu w żaden sposób zarzucić czytelności, a weryfikacja poprawności jest żmudna.
Trochę lepiej
Czyli rządek ifów:
bool operator<(person const& l, person const& r) { if(l.last_name < r.last_name) return true; if(l.first_name < r.first_name) return true; if(l.year_born < r.year_born) return true; if(l.is_male < r.is_male) return true; if(l.city < r.city) return true; return false; } |
Do ideału daleko, ale czytelność dużo wyższa. No i nadal jest poprawnie.
Idiomatycznie
Należy użyć std::tie. Jest to szablon funkcji zwracający std::tuple l-referencji1 do parametrów. std::tuple ma zdefiniowany operator< z porządkiem leksykograficznym dla elementów, więc cały ciężar poprawnej implementacji spada na bibliotekę standardową:
bool operator<(person const& l, person const& r) { return std::tie(l.last_name, l.first_name, l.year_born, l.is_male, l.city) < std::tie(r.last_name, r.first_name, r.year_born, r.is_male, r.city); } |
Krótko, zwięźle, czytelnie, poprawnie.
C++20 (może)
Jest proposal aby dodać operator<=>. Wtedy wystarczyłoby coś w stylu:
bool operator<=>(person const& l, person const& r) = default; // possible C++20 |
1referencje do lvalue. W uproszczeniu “po prostu” referencje (takie jak w C++03)
Fajny post! Dzięki za stopkę ;).
Dzięki za post na temat nowoczesnego C++, tego szukałem – działa 😉