21.7 — Przeciążenie operatorów porównania

W lekcji 6.7 -- Operatory relacyjne i zmiennoprzecinkowe porównania, omówiliśmy sześć operatorów porównania. Przeciążanie tych operatorów porównania jest stosunkowo proste (widzisz, co tam zrobiłem?), ponieważ działają według tych samych wzorców, co widzieliśmy przy przeciążaniu innych operatorów.

Ponieważ wszystkie operatory porównania to operatory binarne, które nie modyfikują swoich lewych operandów, nasze przeciążone operatory porównania zamienimy na zaprzyjaźnione funkcje.

Oto przykładowa klasa Car z przeciążonym operatorem== i operatorem!=.

#include <iostream>
#include <string>
#include <string_view>

class Car
{
private:
    std::string m_make;
    std::string m_model;

public:
    Car(std::string_view make, std::string_view model)
        : m_make{ make }, m_model{ model }
    {
    }

    friend bool operator== (const Car& c1, const Car& c2);
    friend bool operator!= (const Car& c1, const Car& c2);
};

bool operator== (const Car& c1, const Car& c2)
{
    return (c1.m_make == c2.m_make &&
            c1.m_model == c2.m_model);
}

bool operator!= (const Car& c1, const Car& c2)
{
    return (c1.m_make != c2.m_make ||
            c1.m_model != c2.m_model);
}

int main()
{
    Car corolla{ "Toyota", "Corolla" };
    Car camry{ "Toyota", "Camry" };

    if (corolla == camry)
        std::cout << "a Corolla and Camry are the same.\n";

    if (corolla != camry)
        std::cout << "a Corolla and Camry are not the same.\n";

    return 0;
}

Kod tutaj powinien mieć postać proste.

A co z operatorem< i operatorem>? Co oznaczałoby, że Samochód jest większy lub mniejszy od innego Samochodu? Zwykle nie myślimy o samochodach w ten sposób. Ponieważ wyniki operator< i operator> nie byłyby od razu intuicyjne, lepszym rozwiązaniem może być pozostawienie tych operatorów niezdefiniowanych.

Najlepsza praktyka

Definiuj tylko przeciążone operatory, które mają intuicyjny sens dla Twojej klasy.

Istnieje jednak jeden powszechny wyjątek od powyższego zalecenia. A gdybyśmy chcieli posortować listę samochodów? W takim przypadku możemy przeciążyć operatory porównania, aby zwrócić element (lub elementy), według którego najprawdopodobniej chcesz posortować. Na przykład przeciążony operator< dla Cars może sortować alfabetycznie według marki i modelu.

Niektóre klasy kontenerów w standardowej bibliotece (klasy przechowujące zbiory innych klas) wymagają przeciążonego operatora<, aby elementy były posortowane.

Oto inny przykład przeciążania wszystkich 6 operatorów porównania logicznego:

#include <iostream>

class Cents
{
private:
    int m_cents;
 
public:
    Cents(int cents)
	: m_cents{ cents }
	{}
 
    friend bool operator== (const Cents& c1, const Cents& c2);
    friend bool operator!= (const Cents& c1, const Cents& c2);

    friend bool operator< (const Cents& c1, const Cents& c2);
    friend bool operator> (const Cents& c1, const Cents& c2);

    friend bool operator<= (const Cents& c1, const Cents& c2);
    friend bool operator>= (const Cents& c1, const Cents& c2);
};

bool operator== (const Cents& c1, const Cents& c2)
{
    return c1.m_cents == c2.m_cents;
}

bool operator!= (const Cents& c1, const Cents& c2)
{
    return c1.m_cents != c2.m_cents;
}

bool operator> (const Cents& c1, const Cents& c2)
{
    return c1.m_cents > c2.m_cents;
}

bool operator< (const Cents& c1, const Cents& c2)
{
    return c1.m_cents < c2.m_cents;
}

bool operator<= (const Cents& c1, const Cents& c2)
{
    return c1.m_cents <= c2.m_cents;
}

bool operator>= (const Cents& c1, const Cents& c2)
{
    return c1.m_cents >= c2.m_cents;
}

int main()
{
    Cents dime{ 10 };
    Cents nickel{ 5 };
 
    if (nickel > dime)
        std::cout << "a nickel is greater than a dime.\n";
    if (nickel >= dime)
        std::cout << "a nickel is greater than or equal to a dime.\n";
    if (nickel < dime)
        std::cout << "a dime is greater than a nickel.\n";
    if (nickel <= dime)
        std::cout << "a dime is greater than or equal to a nickel.\n";
    if (nickel == dime)
        std::cout << "a dime is equal to a nickel.\n";
    if (nickel != dime)
        std::cout << "a dime is not equal to a nickel.\n";

    return 0;
}

Jest to również całkiem proste.

Minimalizacja porównania redundancja

W powyższym przykładzie zwróć uwagę, jak podobna jest implementacja każdego z przeciążonych operatorów porównania. Przeciążone operatory porównania mają zwykle wysoki stopień redundancji, a im bardziej złożona implementacja, tym większa będzie redundancja.

Na szczęście wiele operatorów porównania można zaimplementować przy użyciu innych operatorów porównania:

  • operator!= można zaimplementować jako !(operator==)
  • operator> można zaimplementować jako operator< w kolejności parametrów odwrócony
  • operator>= można zaimplementować jako !(operator<)
  • operator<= można zaimplementować jako !(operator>)

Oznacza to, że musimy tylko zaimplementować logikę dla operator== i operator<, a następnie pozostałe cztery operatory porównania można zdefiniować w kategoriach tych dwóch! Oto zaktualizowany przykład Centsa ilustrujący to:

#include <iostream>

class Cents
{
private:
    int m_cents;

public:
    Cents(int cents)
        : m_cents{ cents }
    {}

    friend bool operator== (const Cents& c1, const Cents& c2) { return c1.m_cents == c2.m_cents; }
    friend bool operator!= (const Cents& c1, const Cents& c2) { return !(operator==(c1, c2)); }

    friend bool operator< (const Cents& c1, const Cents& c2) { return c1.m_cents < c2.m_cents; }
    friend bool operator> (const Cents& c1, const Cents& c2) { return operator<(c2, c1); }

    friend bool operator<= (const Cents& c1, const Cents& c2) { return !(operator>(c1, c2)); }
    friend bool operator>= (const Cents& c1, const Cents& c2) { return !(operator<(c1, c2)); }

};

int main()
{
    Cents dime{ 10 };
    Cents nickel{ 5 };

    if (nickel > dime)
        std::cout << "a nickel is greater than a dime.\n";
    if (nickel >= dime)
        std::cout << "a nickel is greater than or equal to a dime.\n";
    if (nickel < dime)
        std::cout << "a dime is greater than a nickel.\n";
    if (nickel <= dime)
        std::cout << "a dime is greater than or equal to a nickel.\n";
    if (nickel == dime)
        std::cout << "a dime is equal to a nickel.\n";
    if (nickel != dime)
        std::cout << "a dime is not equal to a nickel.\n";

    return 0;
}

W ten sposób, jeśli kiedykolwiek będziemy musieli coś zmienić, wystarczy zaktualizować operator== i operator< zamiast wszystkich sześciu operatorów porównania!

Operator statku kosmicznego <=> C++20

C++20 wprowadza operatora statku kosmicznego (operator<=>), co pozwala nam zredukować liczbę funkcji porównania, które musimy zapisać maksymalnie do 2, a czasem tylko 1!

Nota autora

Zamierzamy wkrótce dodać nową lekcję na ten temat. Do tego czasu uważaj to za coś, co wzbudzi Twoje zainteresowanie — ale będziesz musiał udać się poza teren placówki, aby dowiedzieć się więcej.

Czas quizu

  1. Dodaj sześć operatorów porównania do klasy Fraction, aby skompilować następujący program:
#include <iostream>
#include <numeric> // for std::gcd

class Fraction
{
private:
	int m_numerator{};
	int m_denominator{};

public:
	Fraction(int numerator = 0, int denominator = 1) :
		m_numerator{ numerator }, m_denominator{ denominator }
	{
		// We put reduce() in the constructor to ensure any new fractions we make get reduced!
		// Any fractions that are overwritten will need to be re-reduced
		reduce();
	}

	void reduce()
	{
		int gcd{ std::gcd(m_numerator, m_denominator) };
		if (gcd)
		{
			m_numerator /= gcd;
			m_denominator /= gcd;
		}
	}

	friend std::ostream& operator<<(std::ostream& out, const Fraction& f1);
};

std::ostream& operator<<(std::ostream& out, const Fraction& f1)
{
	out << f1.m_numerator << '/' << f1.m_denominator;
	return out;
}

int main()
{
	Fraction f1{ 3, 2 };
	Fraction f2{ 5, 8 };

	std::cout << f1 << ((f1 == f2) ? " == " : " not == ") << f2 << '\n';
	std::cout << f1 << ((f1 != f2) ? " != " : " not != ") << f2 << '\n';
	std::cout << f1 << ((f1 < f2) ? " < " : " not < ") << f2 << '\n';
	std::cout << f1 << ((f1 > f2) ? " > " : " not > ") << f2 << '\n';
	std::cout << f1 << ((f1 <= f2) ? " <= " : " not <= ") << f2 << '\n';
	std::cout << f1 << ((f1 >= f2) ? " >= " : " not >= ") << f2 << '\n';
	return 0;
}

Jeśli korzystasz z kompilatora w wersji starszej niż C++17, możesz zastąpić std::gcd tą funkcją:

#include <cmath>
 
int gcd(int a, int b) {
    return (b == 0) ? std::abs(a) : gcd(b, a % b);
}

Pokaż rozwiązanie

  1. Dodaj przeciążony operator<< i operator< do klasy Car na górze lekcji, aby skompilował się następujący program:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

int main()
{
  std::vector<Car> cars{
    { "Toyota", "Corolla" },
    { "Honda", "Accord" },
    { "Toyota", "Camry" },
    { "Honda", "Civic" }
  };

  std::sort(cars.begin(), cars.end()); // requires an overloaded operator<

  for (const auto& car : cars)
    std::cout << car << '\n'; // requires an overloaded operator<<

  return 0;
}

Ten program powinien wygenerować następujące informacje wyjście:

(Honda, Accord)
(Honda, Civic)
(Toyota, Camry)
(Toyota, Corolla)

Jeśli potrzebujesz odświeżenia std::sort, porozmawiamy o tym na lekcji 18.1 -- Sortowanie tablicy przy użyciu sortowania przez wybór.

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
286 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze