Jak łatwo zaimplementować w C++ operator porównania dla twojej klasy?

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)

2 thoughts on “Jak łatwo zaimplementować w C++ operator porównania dla twojej klasy?

Leave a Reply

Your email address will not be published.