Jakie są podstawowe zasady i idiomy dotyczące przeciążania operatora?


2141

Uwaga: odpowiedzi udzielono w określonej kolejności , ale ponieważ wielu użytkowników sortuje odpowiedzi według głosów, a nie czasu, w którym zostały udzielone, oto indeks odpowiedzi w kolejności, w której mają one największy sens:

(Uwaga: ma to być wpis do często zadawanych pytań na temat C ++ w programie Stack Overflow . Jeśli chcesz skrytykować pomysł podania w tym formularzu odpowiedzi na najczęściej zadawane pytania, to miejsce na publikację na meta, które to wszystko rozpoczęło, byłoby odpowiednim miejscem. Odpowiedzi na to pytanie jest monitorowane w czacie C ++ , gdzie pomysł FAQ powstał w pierwszej kolejności, więc twoje odpowiedzi prawdopodobnie zostaną przeczytane przez tych, którzy wpadli na ten pomysł).


63
Jeśli chcemy kontynuować z tagiem C ++ - FAQ, w ten sposób należy sformatować wpisy.
John Dibling,

Napisałem krótką serię artykułów dla niemieckiej społeczności C ++ o przeciążeniu operatora: Część 1: przeciążenie operatora w C ++ obejmuje semantykę, typowe użycie i specjalizacje dla wszystkich operatorów. Twoje odpowiedzi nakładają się na ciebie tutaj, jednak istnieją dodatkowe informacje. Części 2 i 3 zawierają samouczek korzystania z Boost.Operators. Czy chciałbyś, żebym je przetłumaczył i dodał jako odpowiedzi?
Arne Mertz

Aha, dostępne jest także tłumaczenie na język angielski: podstawy i powszechna praktyka
Arne Mertz

Odpowiedzi:


1042

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 thisargument 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 constna 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 ++itego, gdy inie 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 += bjest na ogół bardziej wydajna niż a + bi 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_typejest typu class(lub structlub 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


89
operator->()jest naprawdę niesamowicie dziwny. Zwrócenie a nie jest wymagane value_type*- w rzeczywistości może zwrócić inny typ klasy, pod warunkiem, że ma on typoperator->() , który zostanie następnie wywołany. To rekurencyjne wywoływanie operator->()s odbywa się do momentu wystąpienia value_type*typu zwracanego. Szaleństwo! :)
j_random_hacker

2
Nie chodzi tylko o skuteczność. Chodzi o to, że nie możemy tego zrobić w tradycyjny-idiomatyczny sposób w (bardzo) kilku przypadkach: gdy definicja obu operandów musi pozostać niezmieniona podczas obliczania wyniku. I jak powiedziałem, istnieją dwa klasyczne przykłady: mnożenie macierzy i mnożenie wielomianów. Możemy zdefiniować *w kategoriach, *=ale byłoby to niezręczne, ponieważ jedna z pierwszych operacji *=utworzyłaby nowy obiekt, wynik obliczeń. Następnie po pętli for-ijk zamienilibyśmy ten obiekt tymczasowy za pomocą *this. to znaczy. 1. kopia, 2. operator *, 3. zamiana
Luc Hermitte

6
Nie zgadzam się z wersjami const / non-const operatorów podobnych do wskaźników, np. „Const value_type & operator * () const;` - to tak, jakbyśmy T* constzwracali a const T&przy dereferencji, co nie jest prawdą. Lub innymi słowy: wskaźnik const nie implikuje const pointee. W rzeczywistości naśladowanie nie jest trywialne T const *- co jest przyczyną wszystkich const_iteratorrzeczy w standardowej bibliotece. Wniosek: podpis powinien byćreference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
Jeden komentarz: Sugerowana implementacja binarnych operatorów arytmetycznych nie jest tak wydajna, jak mogłaby być. Uwaga operatorów symulacji Se Boost Uwaga: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Można uniknąć jeszcze jednej kopii, jeśli użyjesz lokalnej kopii pierwszego parametru, wykonaj + = i zwróć kopia lokalna. Umożliwia to optymalizację NRVO.
Manu343726,

3
Jak wspomniałem na czacie, L <= Rmożna również wyrazić jako !(R < L)zamiast !(L > R). Może zaoszczędzić dodatkową warstwę wstawiania w trudnych do optymalizacji wyrażeniach (i tak też implementuje to Boost.Operators).
TemplateRex,

494

Trzy podstawowe zasady przeciążania operatora w C ++

Jeśli chodzi o przeciążanie operatorów w C ++, należy przestrzegać trzech podstawowych zasad . Podobnie jak w przypadku wszystkich takich zasad, istnieją wyjątki. Czasami ludzie odstępują od nich, a wynikiem nie jest zły kod, ale takich pozytywnych odchyleń jest niewiele. Przynajmniej 99 na 100 takich odchyleń, które widziałem, było nieuzasadnionych. Jednak równie dobrze mogło to być 999 na 1000. Więc lepiej trzymaj się następujących zasad.

  1. Ilekroć znaczenie operatora nie jest oczywiście jasne i niekwestionowane, nie należy go przeciążać. Zamiast tego podaj funkcję o dobrze wybranej nazwie.
    Zasadniczo, pierwsza i najważniejsza zasada przeciążania operatorów, w samym jej sercu, mówi: nie rób tego . To może wydawać się dziwne, ponieważ istnieje wiele informacji na temat przeciążania operatorów, a więc wiele artykułów, rozdziałów książek i innych tekstów zajmuje się tym wszystkim. Ale pomimo tych pozornie oczywistych dowodów, istnieje tylko zaskakująco niewiele przypadków, w których przeciążenie operatora jest właściwe . Powodem jest to, że tak naprawdę trudno jest zrozumieć semantykę zastosowania operatora, chyba że użycie operatora w dziedzinie aplikacji jest dobrze znane i niekwestionowane. W przeciwieństwie do powszechnego przekonania, rzadko tak jest.

  2. Zawsze trzymaj się dobrze znanej semantyki operatora.
    C ++ nie nakłada żadnych ograniczeń na semantykę przeciążonych operatorów. Twój kompilator chętnie zaakceptuje kod, który implementuje+operatorbinarnydo odejmowania od jego prawego operandu. Jednak użytkownicy takiego operatora nigdy nie podejrzewają ekspresjęa + bodjąćaodb. Oczywiście zakłada to, że semantyka operatora w domenie aplikacji jest niekwestionowana.

  3. Zawsze zapewniaj wszystko z zestawu powiązanych operacji.
    Operatorzy są ze sobą powiązani i innymi operacjami. Jeśli Twój typ obsługujea + b, użytkownicy również będą mogli zadzwonića += b. Jeśli obsługuje inkrementację prefiksu++a, będą również oczekiwać,a++że zadziała. Jeśli będą w stanie sprawdzić, czy naa < bpewno będą oczekiwać, że będą w stanie sprawdzić, czya > b. Jeśli potrafią skopiować i skonstruować twój typ, oczekują, że przypisanie również zadziała.


Przejdź do decyzji między członkiem a członkiem niebędącym członkiem .


16
Jedyne, co wiem, co narusza którekolwiek z nich, to boost::spiritlol.
Billy ONeal,

66
@Billy: Według niektórych nadużywanie +konkatenacji łańcuchów jest naruszeniem, ale teraz stało się już dobrze ustaloną praktyką, przez co wydaje się naturalne. Chociaż pamiętam klasę strun domowych, które widziałem w latach 90., które używały &do tego celu binarnych (odnosząc się do BASIC dla ustalonych praktyk). Ale tak, włożenie go do standardowej biblioteki lib po prostu osadza w kamieniu. To samo dotyczy nadużyć <<i >>IO, BTW. Dlaczego przesunięcie w lewo byłoby oczywistą operacją wyjściową? Ponieważ wszyscy dowiedzieliśmy się o tym, kiedy zobaczyliśmy nasze pierwsze „Cześć, świecie!” podanie. I bez żadnego innego powodu.
sbi

5
@curiousguy: Jeśli musisz to wyjaśnić, nie jest to oczywiście jasne i niekwestionowane. Podobnie jeśli musisz omówić lub obronić przeciążenie.
sbi

5
@sbi: „peer review” to zawsze dobry pomysł. Dla mnie źle wybrany operator nie różni się od źle wybranej nazwy funkcji (widziałem wielu). Operator to tylko funkcje. Nie więcej nie mniej. Zasady są takie same. Aby zrozumieć, czy pomysł jest dobry, najlepszym sposobem jest zrozumienie, ile czasu zajmuje zrozumienie. (Stąd ocena rówieśnicza jest koniecznością, ale rówieśnicy muszą być wybierani między ludźmi wolnymi od dogmatów i uprzedzeń.)
Emilio Garavaglia

5
@sbi Dla mnie jedynym absolutnie oczywistym i niepodważalnym faktem operator==jest to, że powinna to być relacja równoważności (IOW, nie powinieneś używać nie sygnalizującego NaN). Istnieje wiele użytecznych relacji równoważności na kontenerach. Co oznacza równość? „ arówna się b” oznacza to ai bma tę samą wartość matematyczną. Pojęcie wartości matematycznej (innej niż NaN) floatjest jasne, ale wartość matematyczna kontenera może mieć wiele różnych (rekurencyjnych) przydatnych definicji. Najsilniejsza definicja równości brzmi „są to te same obiekty” i jest bezużyteczna.
ciekawy

265

Ogólna składnia przeciążenia operatora w C ++

Nie można zmienić znaczenia operatorów dla wbudowanych typów w C ++, operatory mogą być przeciążone tylko dla typów zdefiniowanych przez użytkownika 1 . Oznacza to, że co najmniej jeden operand musi być typu zdefiniowanego przez użytkownika. Podobnie jak w przypadku innych przeciążonych funkcji, operatory mogą zostać przeciążone dla określonego zestawu parametrów tylko raz.

Nie wszystkie operatory mogą być przeciążone w C ++. Wśród operatorów, których nie można przeciążać, są: . :: sizeof typeid .*i jedyny trójskładnikowy operator w C ++,?:

Wśród operatorów, które mogą być przeciążone w C ++ są:

  • operatory arytmetyczne: + - * / %i += -= *= /= %=(wszystkie dwójki); + -(unarny prefiks); ++ --(jednorazowy prefiks i postfiks)
  • manipulacja bitami: & | ^ << >>i &= |= ^= <<= >>=(wszystkie binarne poprawki); ~(jednorazowy prefiks)
  • algebra boolowska: == != < > <= >= || &&(wszystkie binarne infix); !(jednorazowy prefiks)
  • zarządzanie pamięcią: new new[] delete delete[]
  • niejawne operatory konwersji
  • miscellany: = [] -> ->* , (wszystkie binarne infix); * &(wszystkie jednoargumentowe) ()(wywołanie funkcji, n-aryfiks)

Jednak fakt, że możesz przeciążać je wszystkie, nie oznacza, że powinieneś to zrobić. Zobacz podstawowe zasady przeciążania operatora.

W C ++ operatory są przeciążone w postaci funkcji o specjalnych nazwach . Podobnie jak w przypadku innych funkcji, przeciążone operatory można zasadniczo zaimplementować albo jako funkcję składową typu ich lewego operandu, albo jako funkcje nie będące członkami . To, czy masz swobodę wyboru, czy jedno z nich zależy, zależy od kilku kryteriów. 2 Jednoargumentowy operator @3 , zastosowany do obiektu x, jest wywoływany jako operator@(x)lub jako x.operator@(). Operator binarnej poprawki @, zastosowany do obiektów xi y, jest wywoływany jako operator@(x,y)lub jako x.operator@(y). 4

Operatory, które są implementowane jako funkcje nie będące członkami, są czasami przyjacielami typu operandu.

1 Termin „zdefiniowany przez użytkownika” może być nieco mylący. C ++ rozróżnia typy wbudowane od typów zdefiniowanych przez użytkownika. Do tych pierwszych należą na przykład int, char i double; do tych ostatnich należą wszystkie typy struct, class, union i enum, w tym te ze standardowej biblioteki, mimo że jako takie nie są zdefiniowane przez użytkowników.

2 Jest to omówione w dalszej części tego FAQ.

3 Nie @jest poprawnym operatorem w C ++, dlatego używam go jako symbolu zastępczego.

4 Jedynego operatora trójskładnikowego w C ++ nie można przeciążać, a jedyny operator n-ary musi zawsze być implementowany jako funkcja składowa.


Przejdź do trzech podstawowych zasad przeciążania operatora w C ++ .


~jest przedrostkiem jednoargumentowym, a nie dwójkowym.
mrkj

1
.*brakuje na liście operatorów nieobciążalnych.
celticminstrel

1
@Mateen Chciałem użyć symbolu zastępczego zamiast prawdziwego operatora, aby wyjaśnić, że nie chodzi o specjalnego operatora, ale dotyczy wszystkich. A jeśli chcesz być programistą C ++, powinieneś nauczyć się zwracać uwagę nawet na małe znaki. :)
sbi

1
@HR: Gdybyś przeczytał ten przewodnik, wiedziałbyś, co jest nie tak. Ogólnie sugeruję, abyś przeczytał pierwsze trzy odpowiedzi związane z pytaniem. Nie powinno to trwać dłużej niż pół godziny twojego życia i daje ci podstawowe zrozumienie. Specyficzna dla operatora składnia, którą możesz sprawdzić później. Twój konkretny problem sugeruje, że próbujesz przeciążać operator+()jako funkcję członka, ale nadał mu podpis funkcji bezpłatnej. Zobacz tutaj .
sbi

1
@sbi: Przeczytałem już trzy pierwsze posty i dziękuję za ich zrobienie. :) Spróbuję rozwiązać problem, inaczej uważam, że lepiej zadać go na osobne pytanie. Jeszcze raz dziękuję za ułatwienie nam życia! : D
Hosein Rahnama

251

Decyzja między członkiem a podmiotem niebędącym członkiem

Operatory binarne =(przypisanie), [](subskrypcja tablicowa), ->(dostęp do członka), a także ()operator n-ary (wywołanie funkcji), muszą być zawsze implementowane jako funkcje składowe , ponieważ wymaga tego składnia języka.

Inni operatorzy mogą być implementowani jako członkowie lub jako członkowie niebędący członkami. Niektóre z nich jednak zwykle muszą być zaimplementowane jako funkcje nie będące członkami, ponieważ ich lewy operand nie może być przez ciebie modyfikowany. Najważniejsze z nich to operatory wejściowe i wyjściowe, <<a >>których lewe operandy to klasy strumienia ze standardowej biblioteki, których nie można zmienić.

W przypadku wszystkich operatorów, w których musisz wybrać ich implementację jako funkcję członka lub funkcję nie będącą członkiem, użyj następujących ogólnych zasad, aby zdecydować:

  1. Jeśli jest to operator jednoargumentowy , zaimplementuj go jako funkcję członka .
  2. Jeśli operator binarny traktuje oba operandy jednakowo (pozostawia je niezmienione), zaimplementuj ten operator jako funkcję nie będącą członkiem .
  3. Jeśli operator binarny nie traktuje obu swoich operandów jednakowo (zwykle zmieni lewy operand), może być użyteczne uczynienie go członkiem funkcji typu lewego operandu, jeśli będzie musiał uzyskać dostęp do jego prywatnych części.

Oczywiście, podobnie jak w przypadku wszystkich zasad, istnieją wyjątki. Jeśli masz typ

enum Month {Jan, Feb, ..., Nov, Dec}

i chcesz przeciążyć dla niego operatory inkrementacji i dekrementacji, nie możesz tego robić jako funkcji składowej, ponieważ w C ++ typy wyliczeniowe nie mogą mieć funkcji składowych. Więc musisz go przeciążyć jako bezpłatną funkcję. A operator<()dla szablonu klasy zagnieżdżonego w szablonie klasy jest znacznie łatwiej pisać i czytać, gdy jest wykonywany jako funkcja składowa wbudowana w definicję klasy. Ale są to rzeczywiście rzadkie wyjątki.

( Jeśli jednak zrobisz wyjątek, nie zapomnij o kwestii const-ness dla operandu, który dla funkcji składowych staje się domyślnym thisargumentem. Jeśli operator jako funkcja nie będąca członkiem wziąłby swój argument znajdujący się najdalej z lewej strony jako constodniesienie , ten sam operator, co funkcja członka, musi mieć constna końcu *thisznak, aby móc się constodwoływać).


Przejdź do wspólnych operatorów, aby przeładować .


9
Element Herb Sutter w Effective C ++ (a może to C ++ Standards Coding?) Mówi, że należy preferować funkcje nieprzyjazne, które nie są członkami, niż funkcje członka, aby zwiększyć enkapsulację klasy. IMHO, powód enkapsulacji ma pierwszeństwo przed twoją ogólną zasadą, ale nie obniża wartości jakości twojej ogólnej zasady.
paercebal,

8
@paercebal: Efektywne C ++ opracował Meyers, C ++ Standardy kodowania Sutter. Do którego się odnosisz? W każdym razie nie podoba mi się pomysł, powiedzmy, operator+=()nie bycia członkiem. Musi zmienić operand po lewej stronie, więc z definicji musi kopać głęboko w swoich wnętrznościach. Co zyskałbyś, nie czyniąc go członkiem?
sbi

9
@sbi: Pozycja 44 w C ++ Standardy kodowania (Sutter) Wolę pisać nieprzyjazne funkcje , oczywiście, ma to zastosowanie tylko wtedy, gdy możesz napisać tę funkcję tylko przy użyciu publicznego interfejsu klasy. Jeśli nie możesz (lub możesz, ale to bardzo pogorszyłoby wydajność), musisz uczynić go członkiem lub przyjacielem.
Matthieu M.,

3
@sbi: Ups, Effective, Exceptional ... Nic dziwnego, że pomieszałem nazwy. W każdym razie zysk polega na jak największym ograniczeniu liczby funkcji, które mają dostęp do obiektu danych prywatnych / chronionych. W ten sposób zwiększasz enkapsulację swojej klasy, ułatwiając jej utrzymanie / testowanie / ewolucję.
paercebal,

12
@sbi: Jeden przykład. Załóżmy, że kodujesz klasę String za pomocą zarówno metody , jak operator +=i appendmetody. appendMetoda jest bardziej kompletna, ponieważ można dołączyć podciąg parametru z indeksu i do indeksu n -1: append(string, start, end)Wydaje się logiczne, aby mieć +=rozmowę z append start = 0i end = string.size. W tym momencie append może być metodą członkowską, ale operator +=nie musi być członkiem, a uczynienie go nie-członkiem zmniejszyłoby ilość kodu grającego z dodatkami String, więc to dobrze ... ^ _ ^ ...
paercebal,

165

Operatory konwersji (znane również jako konwersje zdefiniowane przez użytkownika)

W C ++ możesz tworzyć operatory konwersji, które pozwalają kompilatorowi na konwersję między twoimi typami i innymi zdefiniowanymi typami. Istnieją dwa typy operatorów konwersji, jawne i jawne.

Niejawne operatory konwersji (C ++ 98 / C ++ 03 i C ++ 11)

Niejawny operator konwersji umożliwia kompilatorowi niejawną konwersję (podobnie jak konwersję między inti long) wartości typu zdefiniowanego przez użytkownika na inny typ.

Oto prosta klasa z niejawnym operatorem konwersji:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Domniemane operatory konwersji, podobnie jak konstruktory jednoargumentowe, są konwersjami zdefiniowanymi przez użytkownika. Kompilatory udzielą jednej konwersji zdefiniowanej przez użytkownika podczas próby dopasowania wywołania do przeciążonej funkcji.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Na początku wydaje się to bardzo pomocne, ale problem polega na tym, że niejawna konwersja rozpoczyna się nawet wtedy, gdy nie jest to oczekiwane. W poniższym kodzie void f(const char*)zostanie wywołany, ponieważ my_string()nie jest to wartość , więc pierwszy nie pasuje:

void f(my_string&);
void f(const char*);

f(my_string());

Początkujący łatwo się mylą, a nawet doświadczeni programiści C ++ są czasem zaskoczeni, ponieważ kompilator wybiera przeciążenie, którego nie podejrzewali. Problemy te można złagodzić dzięki jawnym operatorom konwersji.

Jawne operatory konwersji (C ++ 11)

W przeciwieństwie do niejawnych operatorów konwersji, jawne operatory konwersji nigdy się nie uruchomią, jeśli się ich nie spodziewasz. Oto prosta klasa z jawnym operatorem konwersji:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Zwróć uwagę na explicit . Teraz, gdy próbujesz wykonać nieoczekiwany kod od niejawnych operatorów konwersji, pojawia się błąd kompilatora:

prog.cpp: W funkcji 'int main ()':
prog.cpp: 15: 18: błąd: brak pasującej funkcji dla wywołania „f (my_string)”
prog.cpp: 15: 18: uwaga: kandydatami są:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: uwaga: nieznana konwersja argumentu 1 z „my_string” na „my_string &”
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: uwaga: nieznana konwersja argumentu 1 z „my_string” na „const char *”

Aby wywołać jawny operator rzutowania, musisz użyć static_castrzutowania w stylu C lub rzutowania w stylu konstruktora (tj T(value).).

Jest jednak jeden wyjątek: kompilator może niejawnie przekonwertować się na bool. Ponadto kompilatorowi nie wolno wykonywać kolejnej niejawnej konwersji po konwersji bool(kompilator może wykonywać 2 niejawne konwersje jednocześnie, ale maksymalnie 1 konwersję zdefiniowaną przez użytkownika).

Ponieważ kompilator nie rzuci „przeszłości” bool, jawne operatory konwersji usuwają teraz potrzebę używania idiomu Safe Bool . Na przykład inteligentne wskaźniki przed C ++ 11 używały idiomu Safe Bool, aby zapobiec konwersji na typy całkowe. W C ++ 11 inteligentne wskaźniki zamiast tego używają jawnego operatora, ponieważ kompilator nie może niejawnie konwertować na typ integralny po jawnej konwersji typu na bool.

Kontynuuj do przeciążenia newidelete .


148

Przeciążenie newidelete

Uwaga: dotyczy to tylko składni przeciążenia,newadeletenie implementacji takich przeciążonych operatorów. Myślę, że semantyka przeciążanianew i deletezasługuję na własne FAQ , w temacie przeciążania operatora nigdy nie mogę tego oddać sprawiedliwie.

Podstawy

W C ++, gdy piszesz nowe wyrażenie, tak jak new T(arg)przy ocenie tego wyrażenia, zdarzają się dwie rzeczy: Najpierw operator newwywoływana jest pamięć surowa, a następnie Twywoływany jest odpowiedni konstruktor, aby przekształcić tę surową pamięć w poprawny obiekt. Podobnie, gdy usuwasz obiekt, najpierw wywoływany jest jego destruktor, a następnie pamięć jest przywracana operator delete.
C ++ pozwala dostroić obie te operacje: zarządzanie pamięcią oraz budowę / zniszczenie obiektu w przydzielonej pamięci. To ostatnie odbywa się poprzez pisanie konstruktorów i destruktorów dla klasy. Precyzyjne zarządzanie pamięcią odbywa się poprzez napisanie własnego operator newi operator delete.

Pierwsza z podstawowych zasad przeciążania operatora - nie rób tego - dotyczy szczególnie przeciążenia newi delete. Niemal jedynym powodem przeciążenia tych operatorów są problemy z wydajnością i ograniczenia pamięci , aw wielu przypadkach inne działania, takie jak zmiany w używanych algorytmach , zapewniają znacznie wyższy stosunek kosztów do zysków niż próba poprawienia zarządzania pamięcią.

C ++ biblioteki standardowej jest wyposażony w zestaw predefiniowanych newi deleteoperatorów. Najważniejsze z nich to:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Pierwsze dwa przydzielają / zwalniają pamięć dla obiektu, a dwa ostatnie dla tablicy obiektów. Jeśli udostępnisz własne wersje, nie zostaną one przeciążone, ale zastąpią te ze standardowej biblioteki.
Jeśli przeładujesz operator new, zawsze powinieneś również przeładować dopasowanie operator delete, nawet jeśli nigdy nie zamierzasz do niego dzwonić. Powodem jest to, że jeśli konstruktor wyrzuci podczas oceny nowego wyrażenia, system wykonawczy zwróci pamięć do operator deletepasującej do tej, operator newktóra została wywołana w celu przydzielenia pamięci do utworzenia obiektu. Jeśli nie podasz pasującego operator deletenazywana jest domyślna, co prawie zawsze jest błędne.
W przypadku przeciążenia newi deletenależy również rozważyć przeciążenie wariantów macierzy.

Umieszczenie new

C ++ pozwala nowym operatorom i operatorom usuwać dodatkowe argumenty.
Tak zwane umieszczanie nowe pozwala utworzyć obiekt pod określonym adresem, który jest przekazywany do:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

Standardowa biblioteka zawiera odpowiednie przeciążenia operatorów nowych i usuwających:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Zauważ, że w podanym powyżej przykładowym kodzie umieszczenia nowego operator deletenigdy nie jest wywoływany, chyba że konstruktor X zgłosi wyjątek.

Możesz także przeciążać newi deleteinnymi argumentami. Podobnie jak w przypadku dodatkowego argumentu dotyczącego umieszczenia nowego, argumenty te są również wymienione w nawiasach za słowem kluczowym new. Ze względów historycznych takie warianty są często nazywane umieszczaniem nowych, nawet jeśli ich argumenty nie dotyczą umieszczenia obiektu pod określonym adresem.

Specyficzne dla klasy nowe i usuń

Najczęściej będziesz chciał dostroić zarządzanie pamięcią, ponieważ pomiar wykazał, że instancje określonej klasy lub grupy powiązanych klas są często tworzone i niszczone, a domyślne zarządzanie pamięcią systemu wykonawczego dostosowane do ogólna wydajność, zajmuje się nieefektywnie w tym konkretnym przypadku. Aby to poprawić, możesz przeciążyć nową i usunąć dla określonej klasy:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

W ten sposób przeciążone, nowe i delete zachowują się jak statyczne funkcje składowe. Dla obiektów my_classThe std::size_targumentem zawsze będzie sizeof(my_class). Jednak operatory te są również wywoływane dla dynamicznie przydzielanych obiektów klas pochodnych , w którym to przypadku może być większy.

Globalnie nowe i usuń

Aby przeciążyć globalną nowość i usunąć, wystarczy zastąpić predefiniowane operatory biblioteki standardowej naszą własną. Jednak rzadko trzeba to robić.


11
Nie zgadzam się również z tym, że zastąpienie globalnego operatora nowym i usunięciem zwykle służy wydajności: wręcz przeciwnie, zwykle służy do śledzenia błędów.
Yttrill,

1
Należy również pamiętać, że jeśli używasz przeciążonego nowego operatora, musisz również podać operator delete z pasującymi argumentami. Mówisz to w sekcji o globalnym new / delete, gdzie nie jest to zbyt interesujące.
Yttrill,

13
@ Ytrill, mylisz rzeczy. Sens zostaje przeciążony. „Przeciążenie operatora” oznacza, że ​​znaczenie jest przeciążone. Nie oznacza to, że dosłownie funkcje są przeciążone, a w szczególności nowy operator nie przeciąża wersji Standardu. @sbi nie twierdzi inaczej. Często nazywa się to „przeciążaniem nowego”, podobnie jak często mówi się „przeciążanie operatora dodawania”.
Johannes Schaub - litb

1
@sbi: Zobacz (lub lepiej, link do) gotw.ca/publications/mill15.htm . To tylko dobra praktyka w stosunku do osób, które czasem używają nothrownowych.
Alexandre C.,

1
„Jeśli nie podasz kasującego operatora usuwającego, domyślny zostanie nazwany” -> W rzeczywistości, jeśli dodasz jakieś argumenty i nie utworzysz pasującego kasowania, żadne operatory kasowania nie zostanie w ogóle wywołane i masz przeciek pamięci. (15.2.2, pamięć zajmowana przez obiekt jest zwalniana tylko wtedy, gdy zostanie znalezione odpowiednie ... usunięcie operatora)
dascandy

46

Dlaczego operator<<funkcja przesyłania strumieniowego obiektów do std::coutlub do pliku nie może być funkcją składową ?

Powiedzmy, że masz:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

W związku z tym nie można używać:

Foo f = {10, 20.0};
std::cout << f;

Ponieważ operator<<funkcja przeciążenia jest przeciążona Foo, LHS operatora musi być Fooobiektem. Co oznacza, że ​​będziesz musiał użyć:

Foo f = {10, 20.0};
f << std::cout

co jest bardzo nieintuicyjne.

Jeśli zdefiniujesz ją jako funkcję nie będącą członkiem,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Będziesz mógł używać:

Foo f = {10, 20.0};
std::cout << f;

co jest bardzo intuicyjne.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.