Często operatorzy przeciążają
Większość pracy operatorów przeciążających polega na kodowaniu płyt kotłowych. Nic dziwnego, skoro operatory są jedynie cukrem syntaktycznym, ich faktyczną pracę można wykonać (i często przekazuje się) zwykłym funkcjom. Ale ważne jest, aby dobrze zrozumieć ten kod kotła. Jeśli się nie powiedzie, albo kod twojego operatora się nie skompiluje, albo kod twoich użytkowników nie skompiluje się, albo kod twoich użytkowników będzie działał zaskakująco.
Operator przypisania
Wiele można powiedzieć o przydziale. Jednak większość z nich została już powiedziana w słynnym często zadawanym pytaniu na temat kopiowania i wymiany GMan , więc pominę większość z nich tutaj, podając jedynie operatora idealnego przypisania:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Operatory Bitshift (używane do strumieniowego we / wy)
Operatory bitshift <<
i >>
chociaż nadal używane w sprzęcie sprzętowym dla funkcji manipulacji bitami, które dziedziczą po C, stały się bardziej powszechne jako przeciążone operatory wejścia i wyjścia strumienia w większości aplikacji. Aby uzyskać wskazówki dotyczące przeciążania jako operatorów manipulacji bitami, zobacz sekcję poniżej na temat binarnych operatorów arytmetycznych. Aby wdrożyć własny format niestandardowy i logikę analizowania, gdy obiekt jest używany z iostreams, kontynuuj.
Operatory strumieniowe, wśród najczęściej przeciążonych operatorów, są operatorami binarnych poprawek, dla których składnia nie określa, czy powinny być członkami, czy nie. Ponieważ zmieniają swój lewy argument (zmieniają stan strumienia), powinny, zgodnie z praktycznymi zasadami, być implementowane jako członkowie typu ich lewego operandu. Jednak ich lewe operandy są strumieniami ze standardowej biblioteki i chociaż większość operatorów wyjściowych i wejściowych strumienia zdefiniowanych przez standardową bibliotekę jest rzeczywiście zdefiniowana jako członkowie klas strumieniowych, kiedy implementujesz operacje wyjściowe i wejściowe dla własnych typów, nie można zmienić typów strumieni biblioteki standardowej. Dlatego musisz zaimplementować te operatory dla własnych typów jako funkcje nie będące członkami. Kanoniczne formy tych dwóch są następujące:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Podczas implementacji operator>>
ręczne ustawienie stanu strumienia jest konieczne tylko wtedy, gdy sam odczyt się powiedzie, ale wynik nie jest zgodny z oczekiwaniami.
Operator wywołania funkcji
Operator wywołanie funkcji, stosuje się do tworzenia obiektów funkcyjnych, znane również jako funktorów należy zdefiniować jako człon funkcji, tak że zawsze jest niejawnie this
argument funkcji składowych. Poza tym może być przeciążony, aby przyjąć dowolną liczbę dodatkowych argumentów, w tym zero.
Oto przykład składni:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Stosowanie:
foo f;
int a = f("hello");
W całej standardowej bibliotece C ++ obiekty funkcyjne są zawsze kopiowane. Dlatego twoje własne obiekty funkcyjne powinny być tanie do kopiowania. Jeśli obiekt funkcji musi bezwzględnie wykorzystywać dane, których kopiowanie jest kosztowne, lepiej jest przechowywać te dane gdzie indziej i odwoływać się do obiektu funkcji.
Operatory porównania
Binarne operatory porównania przyrostków powinny być, zgodnie z ogólnymi zasadami, zaimplementowane jako funkcje nie będące członkami 1 . Negacja pojedynczego prefiksu !
powinna (zgodnie z tymi samymi regułami) zostać zaimplementowana jako funkcja członka. (ale przeładowanie go zwykle nie jest dobrym pomysłem).
Algorytmy biblioteki standardowej (np. std::sort()
) I typy (np. std::map
) Zawsze będą oczekiwać tylko operator<
obecności. Jednak użytkownicy tego typu będą oczekiwać, że wszyscy inni operatorzy również będą obecni , więc jeśli zdefiniujesz operator<
, pamiętaj o przestrzeganiu trzeciej podstawowej zasady przeciążania operatora, a także zdefiniuj wszystkie inne operatory porównania boolowskiego. Kanoniczny sposób ich wdrożenia jest następujący:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Ważną rzeczą do odnotowania tutaj jest to, że tylko dwóch z tych operatorów faktycznie robi cokolwiek, inni po prostu przekazują swoje argumenty do jednego z tych dwóch, aby wykonali rzeczywistą pracę.
Składnia przeciążenia pozostałych binarnych operatorów boolowskich ( ||
, &&
) jest zgodna z regułami operatorów porównania. Jednak jest bardzo mało prawdopodobne, aby znaleźć uzasadnione zastosowanie dla tych 2 .
1 Podobnie jak w przypadku wszystkich podstawowych zasad, czasami mogą istnieć powody, aby je złamać. Jeśli tak, nie zapominaj, że operand po lewej stronie binarnych operatorów porównania, który będzie dla funkcji składowych *this
, również musi być const
. Tak więc operator porównania zaimplementowany jako funkcja członka musiałby mieć ten podpis:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Uwaga const
na końcu.)
2 Należy zauważyć, że wbudowana wersja semantyki skrótu ||
i jej &&
użycie. Podczas gdy te zdefiniowane przez użytkownika (ponieważ są cukrami składniowymi dla wywołań metod), nie używają semantyki skrótów. Użytkownik będzie oczekiwać, że operatorzy będą mieli semantykę skrótów, a ich kod może od tego zależeć, dlatego NIGDY nie zaleca się ich definiowania.
Operatory arytmetyczne
Jednoargumentowe operatory arytmetyczne
Jednostkowe operatory inkrementacji i dekrementacji mają zarówno przedrostek, jak i postfiks. Aby odróżnić jeden od drugiego, warianty Postfiksa wymagają dodatkowego argumentu typu dummy int. Jeśli przeciążasz przyrost lub spadek, pamiętaj, aby zawsze implementować zarówno wersję przedrostkową, jak i późniejszą. Oto kanoniczna implementacja przyrostu, dekrementacja przebiega według tych samych zasad:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Zauważ, że wariant Postfiksa jest zaimplementowany pod względem prefiksu. Pamiętaj również, że postfix robi dodatkową kopię. 2)
Przeciążenie jednoargumentowego minus i plus nie jest zbyt powszechne i prawdopodobnie najlepiej go unikać. W razie potrzeby prawdopodobnie powinny być przeciążone jako funkcje składowe.
2 Zauważ też, że wariant postfiksowy działa więcej i dlatego jest mniej wydajny w użyciu niż wariant prefiksowy. Jest to dobry powód, aby ogólnie preferować przyrost prefiksu od przyrostu postfiksu. Podczas gdy kompilatory zwykle optymalizują dodatkową pracę przyrostu postfiksów dla typów wbudowanych, mogą nie być w stanie zrobić tego samego dla typów zdefiniowanych przez użytkownika (co może być czymś tak niewinnie wyglądającym jak iterator listy). Kiedy już się przyzwyczaisz i++
, bardzo trudno jest pamiętać o zrobieniu ++i
tego, gdy i
nie jest on wbudowanym typem (plus trzeba zmienić kod przy zmianie typu), więc lepiej jest nawyk zawsze używając przyrostka prefiksu, chyba że postfiks jest wyraźnie potrzebny.
Binarne operatory arytmetyczne
W przypadku binarnych operatorów arytmetycznych nie zapomnij przestrzegać trzeciej podstawowej zasady przeciążania operatora: jeśli podasz +
, podaj także +=
, jeśli podasz -
, nie pomiń -=
itp. Mówi się, że Andrew Koenig jako pierwszy zauważył, że przypisanie złożone operatory mogą być używane jako baza dla ich nieskomplikowanych odpowiedników. Oznacza to, że operator +
jest wdrażany pod względem +=
, -
jest wdrażany pod względem -=
itp.
Zgodnie z naszymi praktycznymi zasadami, +
a jego towarzysze powinni być członkami niebędącymi członkami, a ich odpowiedniki przypisania złożonego ( +=
itp.), Zmieniając lewy argument, powinni być członkami. Oto przykładowy kod dla +=
i +
; inne binarne operatory arytmetyczne powinny być implementowane w ten sam sposób:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
zwraca wynik według odwołania, a operator+
zwraca kopię wyniku. Oczywiście, zwracanie referencji jest zwykle bardziej wydajne niż zwracanie kopii, ale w przypadku operator+
kopiowania nie ma mowy. Kiedy piszesz a + b
, oczekujesz, że wynik będzie nową wartością, dlatego musisz operator+
zwrócić nową wartość. 3
Zauważ też, że operator+
lewy operand pobiera kopię, a nie stałą. Powód tego jest taki sam jak powód podania operator=
argumentu na kopię.
Operatory manipulacji bitami ~
&
|
^
<<
>>
powinny być implementowane w taki sam sposób, jak operatory arytmetyczne. Jednak (z wyjątkiem przeciążenia <<
oraz >>
danych wyjściowych i wejściowych) istnieje bardzo niewiele uzasadnionych przypadków użycia w przypadku ich przeciążenia.
3 Ponownie, lekcja, którą należy z tego wyciągnąć, a += b
jest na ogół bardziej wydajna niż a + b
i powinna być preferowana, jeśli to możliwe.
Subskrybowanie tablicy
Operator indeksu tablicy jest operatorem binarnym, który musi być zaimplementowany jako członek klasy. Służy do typów podobnych do kontenerów, które umożliwiają dostęp do ich elementów danych za pomocą klucza. Kanoniczna forma ich dostarczenia jest następująca:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
O ile nie chcesz, aby użytkownicy twojej klasy mogli zmieniać elementy danych zwracane przez operator[]
(w takim przypadku możesz pominąć wariant inny niż const), zawsze powinieneś podać oba warianty operatora.
Jeśli wiadomo, że typ_wartości odnosi się do typu wbudowanego, wariant const operatora powinien lepiej zwrócić kopię zamiast odwołania const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operatory dla typów podobnych do wskaźników
Aby zdefiniować własne iteratory lub inteligentne wskaźniki, musisz przeciążyć jednoargumentowy operator dereferencji prefiksu *
i operator dostępu do wskaźnika wskaźnika binarnego ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Zauważ, że one również prawie zawsze będą wymagały zarówno wersji const, jak i wersji non-const. Dla ->
operatora, jeśli value_type
jest typu class
(lub struct
lub union
), inny operator->()
jest wywoływany rekurencyjnie, dopóki nie operator->()
zwróci wartości typu nieklasowego.
Jednostkowy adres operatora nigdy nie powinien być przeciążony.
Dla operator->*()
zobaczyć to pytanie . Jest rzadko używany, a zatem rzadko przeciążony. W rzeczywistości nawet iteratory go nie przeciążają.
Przejdź do Operatorów konwersji