21.5 — Przeciążanie operatorów przy użyciu funkcji składowych

W lekcji 21.2 -- Przeciążając operatory arytmetyczne za pomocą funkcji zaprzyjaźnionych, nauczyłeś się, jak przeciążać operatory arytmetyczne za pomocą funkcji zaprzyjaźnionych. Nauczyłeś się również, że możesz przeciążać operatory jak normalne funkcje. Wiele operatorów można przeciążać w inny sposób: jako funkcję składową.

Przeciążanie operatorów za pomocą funkcji składowej jest bardzo podobne do przeciążania operatorów za pomocą funkcji zaprzyjaźnionej. Podczas przeciążania operatora za pomocą funkcji składowej:

  • Przeciążony operator musi zostać dodany jako funkcja składowa lewego operandu.
  • Lewy operand staje się ukrytym *tym obiektem
  • Wszystkie pozostałe operandy stają się parametrami funkcji.

Dla przypomnienia, oto jak przeciążyliśmy operator+ za pomocą funkcji zaprzyjaźnionej:

#include <iostream>

class Cents
{
private:
    int m_cents {};

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

    // Overload Cents + int
    friend Cents operator+(const Cents& cents, int value);

    int getCents() const { return m_cents; }
};

// note: this function is not a member function!
Cents operator+(const Cents& cents, int value)
{
    return Cents(cents.m_cents + value);
}

int main()
{
	const Cents cents1 { 6 };
	const Cents cents2 { cents1 + 2 };
	std::cout << "I have " << cents2.getCents() << " cents.\n";
 
	return 0;
}

Konwersja przeciążonego operatora znajomego do elementu przeciążonego operatora jest łatwe:

  1. Przeciążony operator jest zdefiniowany jako członek zamiast znajomego (Cents::operator+ zamiast znajomego operator+)
  2. Lewy parametr został usunięty, ponieważ ten parametr staje się teraz ukrytym *tym obiektem.
  3. W treści funkcji można usunąć wszystkie odniesienia do lewego parametru (np. cents.m_cents staje się m_cents, co pośrednio odwołuje się do *this obiektu).

Teraz ten sam operator został przeciążony przy użyciu metody funkcji składowej:

#include <iostream>

class Cents
{
private:
    int m_cents {};

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

    // Overload Cents + int
    Cents operator+(int value) const;

    int getCents() const { return m_cents; }
};

// note: this function is a member function!
// the cents parameter in the friend version is now the implicit *this parameter
Cents Cents::operator+ (int value) const
{
    return Cents { m_cents + value };
}

int main()
{
	const Cents cents1 { 6 };
	const Cents cents2 { cents1 + 2 };
	std::cout << "I have " << cents2.getCents() << " cents.\n";
 
	return 0;
}

Zauważ, że użycie operatora nie zmienia się (w obu przypadkach cents1 + 2), po prostu zdefiniowaliśmy tę funkcję inaczej. Nasza dwuparametrowa funkcja zaprzyjaźniona staje się jednoparametrową funkcją członkowską, przy czym skrajny lewy parametr w wersji zaprzyjaźnionej (centy) staje się ukrytym *tym parametrem w wersji funkcji składowej.

Przyjrzyjmy się bliżej, jak obliczane jest wyrażenie cents1 + 2 .

W wersji funkcji zaprzyjaźnionej wyrażenie cents1 + 2 staje się operatorem wywołania funkcji+(centy1, 2). Należy pamiętać, że istnieją dwa parametry funkcji. To jest proste.

W wersji funkcji składowej wyrażenie cents1 + 2 staje się wywołaniem funkcji cents1.operator+(2). Należy zauważyć, że istnieje teraz tylko jeden jawny parametr funkcji, a cents1 stał się przedrostkiem obiektu. Jednakże w lekcji 15.1 — Ukryty „ten” wskaźnik i łączenie funkcji składowych wspomnieliśmy, że kompilator niejawnie konwertuje przedrostek obiektu na ukryty parametr położony skrajnie po lewej stronie o nazwie *this. W rzeczywistości cents1.operator+(2) staje się operator+(&​cents1, 2) jest prawie identyczna z wersją zaprzyjaźnioną.

W obu przypadkach daje ten sam wynik, tylko w nieco inny sposób.

Jeśli więc możemy przeciążyć operatora jako znajomego lub członka, jakiego powinniśmy użyć? Aby odpowiedzieć na to pytanie, musisz wiedzieć jeszcze kilka rzeczy.

Nie wszystko można przeciążać jako funkcję zaprzyjaźnioną

Operatory przypisania (=), indeksu dolnego ([]), wywołania funkcji (()) i wyboru elementów (->) muszą być przeciążane jako funkcje składowe, ponieważ język tego wymaga.

Nie wszystko można przeciążać jako elementy członkowskie funkcja

W lekcji 21.4 -- Przeciążając operatory I/O, przeciążyliśmy operator<< dla naszej klasy Point przy użyciu metody funkcji znajomego. Oto przypomnienie, jak to zrobiliśmy:

#include <iostream>
 
class Point
{
private:
    double m_x {};
    double m_y {};
    double m_z {};
 
public:
    Point(double x=0.0, double y=0.0, double z=0.0)
        : m_x { x }, m_y { y }, m_z { z }
    {
    }
 
    friend std::ostream& operator<< (std::ostream& out, const Point& point);
};
 
std::ostream& operator<< (std::ostream& out, const Point& point)
{
    // Since operator<< is a friend of the Point class, we can access Point's members directly.
    out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ")";
 
    return out;
}
 
int main()
{
    Point point1 { 2.0, 3.0, 4.0 };
 
    std::cout << point1;
 
    return 0;
}

Nie możemy jednak przeciążać operatora<< jako funkcji składowej. Dlaczego nie? Ponieważ przeciążony operator musi zostać dodany jako członek lewego operandu. W tym przypadku lewy operand jest obiektem typu std::ostream. std::ostream został naprawiony jako część standardowej biblioteki. Nie możemy zmodyfikować deklaracji klasy, aby dodać przeciążenie jako funkcję składową std::ostream.

To wymaga, aby operator<< był przeciążony jako normalna funkcja (preferowana) lub przyjaciel.

Podobnie, chociaż możemy przeciążać operator+(Cents, int) jako funkcję składową (tak jak zrobiliśmy powyżej), nie możemy przeciążać operator+(int, Cents) jako funkcję składową, ponieważ int nie jest klasą, do której możemy dodawać członków.

Zazwyczaj nie będziemy mogli użyć przeciążenia elementu członkowskiego, jeśli lewy operand albo nie jest klasą (np. int), albo jest klasa, której nie możemy modyfikować (np. std::ostream).

Kiedy używać przeciążenia funkcji normalnej, zaprzyjaźnionej lub składowej

W większości przypadków język pozostawia użytkownikowi decyzję, czy chcesz użyć wersji przeciążenia funkcji normalnej/przyjacielskiej, czy składowej. Jednak jeden z nich jest zwykle lepszym wyborem niż drugi.

W przypadku operatorów binarnych, które nie modyfikują lewego operandu (np. operator+), zazwyczaj preferowana jest wersja funkcji normalnej lub zaprzyjaźnionej, ponieważ działa ona dla wszystkich typów parametrów (nawet jeśli lewy operand nie jest obiektem klasy lub jest klasą, której nie można modyfikować). Wersja funkcji normalnej lub zaprzyjaźnionej ma dodatkową zaletę w postaci „symetrii”, ponieważ wszystkie operandy stają się jawnymi parametrami (zamiast lewego operandu staje się *this, a prawy operand staje się jawnym parametrem).

W przypadku operatorów binarnych, które modyfikują lewy operand (np. operator+=), zazwyczaj preferowana jest wersja funkcji składowej. W takich przypadkach operand skrajnie lewy będzie zawsze typem klasy, a modyfikowany obiekt stanie się tym, na który wskazuje *, jest to naturalne. Ponieważ skrajny prawy operand staje się jawnym parametrem, nie ma wątpliwości co do tego, kto jest modyfikowany, a kto oceniany.

Operandy jednoargumentowe są zwykle przeciążone również jako funkcje składowe, ponieważ wersja składowa nie ma parametrów.

Poniższe praktyczne zasady mogą pomóc w określeniu, która forma jest najlepsza w danej sytuacji:

  • Jeśli przeciążasz przypisanie (=), indeks dolny ([]), wywołanie funkcji (()) lub wybór elementu członkowskiego (->), zrób to jako funkcję składową.
  • Jeśli przeciążasz operator jednoargumentowy, zrób to jako funkcję składową.
  • Jeśli przeciążasz operator binarny, który nie modyfikuje jego lewego operandu (np. operator+), wykonaj więc jako normalną funkcję (preferowana) lub funkcję zaprzyjaźnioną.
  • Jeśli przeciążasz operator binarny, który modyfikuje jego lewy operand, ale nie możesz dodać elementów do definicji klasy lewego operandu (np. operator<<, który ma lewy operand typu ostream), zrób to jako normalną funkcję (preferowaną) lub funkcję zaprzyjaźnioną.
  • Jeśli przeciążasz operator binarny, który modyfikuje jego lewy operand (np. operator+=) i możesz modyfikować definicję lewego operandu, rób to jako funkcję składową.
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:  
134 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze