Pytanie Jakie są podstawowe zasady i idiomy dotyczące przeciążania operatorów?


Uwaga: odpowiedzi zostały podane w konkretne zamówienie, ale ponieważ wielu użytkowników sortuje odpowiedzi według głosów, a nie czasu, który im podano, oto jest indeks odpowiedzi w kolejności, w jakiej mają sens:

(Uwaga: ma to być wpis do Często zadawane pytania dotyczące C ++ w Stack Overflow. Jeśli chcesz krytykować pomysł dostarczania FAQ w tej formie, to publikacja na meta, która zaczęła to wszystko byłby to miejsce. Odpowiedzi na to pytanie są monitorowane w Czat w C ++, gdzie najpierw powstał pomysł FAQ, więc twoja odpowiedź jest bardzo prawdopodobne, że zostanie odczytana przez tych, którzy wpadli na ten pomysł.)  


1845
2017-12-12 12:44


pochodzenie


Jeśli mamy kontynuować tag 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ążanie operatorów w C ++ obejmuje semantykę, typowe zastosowanie i specjalności dla wszystkich operatorów. To ma pewne nachodzenie na twoje odpowiedzi tutaj, niemniej jednak jest kilka dodatkowych informacji. Części 2 i 3 tworzą samouczek do korzystania z Boost.Operators. Czy chcesz je przetłumaczyć i dodać jako odpowiedzi? - Arne Mertz
Aha, dostępne jest również tłumaczenie na język angielski: podstawy i powszechna praktyka - Arne Mertz


Odpowiedzi:


Typowi operatorzy przeciążają

Większość prac przy przeciążaniu operatorów to kod płyty kotła. Nic w tym dziwnego, skoro operatorzy są jedynie cukrem składniowym, ich rzeczywista praca mogłaby zostać wykonana (i często jest przekazywana) do zwykłych funkcji. Ważne jest jednak, aby uzyskać prawidłowy kod kotła. Jeśli ci się nie uda, twój kod operatora nie zostanie skompilowany lub kod użytkownika nie zostanie skompilowany lub kod Twojego użytkownika będzie zachowywał się zaskakująco.

Operator przypisania

Jest wiele do powiedzenia na temat zadania. Jednak większość z nich została już powiedziane w Najczęściej zadawane pytania dotyczące kopiowania i wymiany GMana, więc pominę tutaj większość z nich, wymieniając tylko idealnego operatora przypisania:

X& X::operator=(X rhs)
{
  swap(rhs);
  return *this;
}

Operatory Bitshift (używane dla Stream I / O)

Operatorzy bitshift << i >>, chociaż nadal używane w interfejsie sprzętowym do funkcji manipulacji bitami, które dziedziczą po C, stały się bardziej rozpowszechnione jako przeciążone operatory wejścia i wyjścia strumienia w większości aplikacji. Aby uzyskać informacje na temat przeciążania wskazań jako operatory manipulacji bitami, zobacz sekcję poniżej na temat binarnych operatorów arytmetycznych. Aby zaimplementować własny niestandardowy format i logikę przetwarzania, gdy obiekt jest używany w programie iostreams, kontynuuj.

Operatory strumieni, wśród najczęściej przeciążonych operatorów, są operatorami binarnych infiksów, dla których składnia nie precyzuje, czy powinni oni być członkami, czy nie. Ponieważ zmieniają lewy argument (zmieniają stan strumienia), powinny, zgodnie z regułami, zostać wdrożone jako członkowie ich lewego typu operandu. Jednak ich lewe operandy są strumieniami ze standardowej biblioteki i podczas gdy większość operatorów wyjściowych i wejściowych strumienia zdefiniowanych przez bibliotekę standardową jest rzeczywiście definiowana jako członkowie klas strumieni, kiedy implementujesz operacje wyjściowe i wejściowe dla własnych typów, nie można zmienić typów strumieni standardowej biblioteki. Dlatego musisz wdrożyć te operatory dla swoich własnych typów jako funkcje nie będące członkami. Kanoniczne formy tych dwóch to:

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 wdrażania operator>>, ręczne ustawienie stanu strumienia jest konieczne tylko wtedy, gdy samo czytanie się powiedzie, ale wynik nie jest tym, czego należałoby się spodziewać.

Operator połączenia funkcji

Operator wywołania funkcji, używany do tworzenia obiektów funkcji, zwanych także funktorami, musi być zdefiniowany jako członek funkcja, więc zawsze ma niejawny this argument funkcji składowych. Poza tym można go przeciążyć, przyjmując 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 bibliotece standardowej C ++ obiekty funkcyjne są zawsze kopiowane. Twoje własne obiekty funkcji powinny być zatem tanie w kopiowaniu. Jeśli obiekt funkcji bezwzględnie wymaga użycia danych, które są kosztowne do skopiowania, lepiej jest przechowywać te dane w innym miejscu i mieć do nich odwołanie do obiektu funkcji.

Operatory porównania

Operatory porównania binarnych inflikacji powinny, zgodnie z zasadami, być implementowane jako funkcje nie będące członkami1. Jednoargencka negacja przedrostkowa ! powinien (zgodnie z tymi samymi zasadami) zostać wdrożony jako funkcja członkowska. (ale zwykle nie jest dobrym pomysłem, aby go przeciążyć).

Algorytmy standardowej biblioteki (np. std::sort()) i typy (np. std::map) zawsze będzie się tylko spodziewać operator< być obecnym. Jednakże użytkownicy twojego typu będą oczekiwać obecności wszystkich innych operatorówtakże, więc jeśli je zdefiniujesz operator<, pamiętaj, aby postępować zgodnie z trzecią podstawową zasadą przeciążania operatorów, a także zdefiniować wszystkie inne operatory porównania boolowskiego. Kanoniczny sposób ich implementacji 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żne jest, aby zauważyć, że tylko dwóch z tych operatorów faktycznie robi cokolwiek, inni tylko przekazują swoje argumenty do któregokolwiek z tych dwóch, aby wykonać rzeczywistą pracę.

Składnia dla przeciążenia pozostałych binarnych operatorów boolowskich (||, &&) przestrzega reguł operatorów porównania. Jednak tak jest bardzo mało prawdopodobne, aby znaleźć rozsądny przypadek użycia dla tych2.

1  Podobnie jak w przypadku wszystkich praktycznych zasad, czasami mogą istnieć powody, aby je również złamać. Jeśli tak, nie zapominaj, że lewy operand operatorów porównania binarnego, który dla funkcji członka będzie *this, musi być const, także. Operator porównania zaimplementowany jako funkcja członkowska musiałby mieć ten podpis:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(Zanotuj const na końcu.)

2  Należy zauważyć, że wbudowana wersja || i && użyj semantyki skrótów. Podczas gdy zdefiniowane przez użytkownika (ponieważ są to cukier syntaktyczny dla wywołań metod), nie stosuj semantyki skrótowej. Użytkownik oczekuje, że operatorzy będą mieć semantykę skrótów, a ich kod może na nim polegać. Dlatego NIGDY nie zaleca się ich definiowania.

Operatory arytmetyczne

Jednoargumentowe operatory arytmetyczne

Jednoargumentowe operatory inkrementacji i dekrementacji mają zarówno smak przedrostkowy, jak i postfiksowy. Aby powiedzieć jeden z drugiego, postfiksowe warianty przyjmują dodatkowy atrapowy argument int. Jeśli przeciążasz przyrost lub dekrementację, pamiętaj, aby zawsze implementować wersje przedrostkowe i przyrostowe. Oto kanoniczna implementacja inkrementacji, dekrementacja podlega tym samym regułom:

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. Zauważ też, że Postfix robi dodatkową kopię.2

Przeciążanie unarnego minusa i plusa nie jest bardzo powszechne i prawdopodobnie najlepiej go uniknąć. W razie potrzeby powinny być prawdopodobnie przeciążone jako funkcje członkowskie.

2  Zauważ też, że wariant Postfix ma więcej pracy, a zatem jest mniej wydajny w użyciu niż wariant prefiksu. Jest to dobry powód, by generalnie preferować przyrost prefiksu w stosunku do przyrostu przyrostowego. Podczas gdy kompilatory zazwyczaj optymalizują dodatkową pracę przyrostka przyrostowego dla wbudowanych typów, 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ę przyzwyczaiłeś i++, bardzo trudno jest o tym pamiętać ++i zamiast tego kiedy i nie jest typem wbudowanym (plus musisz zmienić kod przy zmianie typu), więc lepiej jest robić nawyk zawsze używając przyrostka prefiksu, chyba że postfix jest jawnie potrzebny.

Binarne operatory arytmetyczne

W przypadku binarnych operatorów arytmetycznych nie należy zapominać o przeciążeniu trzeciego podstawowego operatora reguł: Jeśli podasz +, również dostarcz +=, jeśli podasz -, nie pomijaj -=itp. Andrew Koenig był pierwszym, który zaobserwował, że operatory przypisania złożone mogą być używane jako baza dla ich nie-złożonych odpowiedników. To znaczy, operator + jest realizowany pod kątem +=, - jest realizowany pod kątem -= itp.

Zgodnie z naszymi zasadami, + i jego towarzysze nie powinni być członkami, podczas gdy ich odpowiedniki przydziału mieszanego (+= itp.), zmieniając lewy argument, powinien być członkiem. 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 jego wynik na odniesienie, podczas gdy operator+ zwraca kopię swojego wyniku. Oczywiście zwrot referencji jest zwykle bardziej skuteczny niż zwrot kopii, ale w przypadku operator+nie ma możliwości obejścia tego kopiowania. Kiedy piszesz a + b, oczekujesz, że wynik będzie nową wartością, dlatego operator+ musi zwrócić nową wartość.3 Zwróć też uwagę na to operator+ bierze swój lewy operand przez kopię zamiast przez odniesienie do stałych. Przyczyna tego jest taka sama jak przyczyna powodu operator= biorąc argument za kopię.

Operatory manipulacji bitami ~  &  |  ^  <<  >> powinny być realizowane w taki sam sposób, jak operatory arytmetyczne. Jednak (z wyjątkiem przeciążenia << i >> dla wyjścia i wejścia) istnieje bardzo mało rozsądnych przypadków użycia w celu ich przeciążenia.

3  Ponownie, lekcja, którą należy z tego wyciągnąć, jest taka a += b jest na ogół skuteczniejsza niż a + b i powinny być preferowane, jeśli to możliwe.

Uzupełnienie tablicy

Operator indeksu tablicy jest operatorem binarnym, który musi być zaimplementowany jako członek klasy. Jest używany dla typów kontenerowych, które umożliwiają dostęp do ich elementów danych za pomocą klucza. Kanoniczna forma ich dostarczania jest następująca:

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

Chyba, że ​​nie chcesz, aby użytkownicy Twojej klasy mogli zmieniać elementy danych zwracane przez operator[] (w takim przypadku można pominąć wariant niestały), należy zawsze podać oba warianty operatora.

Jeśli value_type jest znane jako odwołujące się do typu wbudowanego, to wariant const operatora powinien zwrócić kopię zamiast odwołania do stałej.

Operatory dla typów podobnych do wskaźnika

Aby zdefiniować własne iteratory lub inteligentne wskaźniki, musisz przeciążać operatora unferencyjnego preselekcji * i binarny wskaźnik dostępu do elementu 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 te również prawie zawsze będą wymagać zarówno wersji stałej, jak i niestałej. Dla -> operator, jeśli value_type jest z class (lub struct lub union) wpisz, inny operator->() nazywa się rekurencyjnie, aż do operator->() zwraca wartość typu nieklasowanego.

Jednorazowy operator adresu nigdy nie powinien być przeciążony.

Dla operator->*() widzieć to pytanie. Jest rzadko używany i dlatego rzadko jest przeciążany. W rzeczywistości nawet iteratory nie przeciążają go.


Kontynuuj Operatory konwersji


898
2017-12-12 12:47



operator->() jest aktualne niezwykle dziwne. Nie jest wymagane, aby zwrócić value_type* - w rzeczywistości może zwrócić inny typ klasy, pod warunkiem, że typ klasy ma operator->(), które następnie zostaną nazwane później. To rekursywne wołanie operator->()s dochodzi do value_type* pojawia się typ zwrotu. Szaleństwo! :) - j_random_hacker
Nie zgadzam się z wersjami const / non const operatorów wskaźnikowych, np. `const value_type & operator * () const;` - byłoby to jak posiadanie T* const zwracanie const T& w przypadku dereferencji, co nie jest prawdą. Lub innymi słowy: wskaźnik const nie oznacza const pointee. W rzeczywistości naśladowanie nie jest trywialne T const * - co jest przyczyną całego const_iterator rzeczy w standardowej bibliotece. Wniosek: podpis powinien być reference_type operator*() const; pointer_type operator->() const - Arne Mertz
Jedna uwaga: sugerowana implementacja binarnych operatorów arytmetycznych nie jest tak efektywna, jak może być. Nagłówek operatorów Se Boost w postaci symetrii: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Jeszcze jednej kopii można uniknąć, jeśli użyjesz lokalnej kopii pierwszego parametru, wykonaj + = i zwróć kopię lokalną. Umożliwia to optymalizację NRVO. - Manu343726
Jak wspomniałem na czacie, L <= R można również wyrazić jako !(R < L) zamiast !(L > R). Może uratować dodatkową warstwę inline w trudnych do optymalizacji wyrażeniach (a także w jaki sposób Boost.Operators ją implementuje). - TemplateRex
@thomthom: jeśli klasa nie ma publicznie dostępnego API do uzyskania stanu, musisz uczynić wszystko, co potrzebuje dostępu do swojego stanu, członka lub friend klasy. Dotyczy to oczywiście również wszystkich operatorów. - sbi


Trzy podstawowe reguły przeciążania operatorów w C ++

Jeśli chodzi o przeciążanie operatorów w C ++, są trzy podstawowe zasady, których powinieneś przestrzegać. Podobnie jak w przypadku wszystkich takich zasad, istnieją rzeczywiście wyjątki. Czasami ludzie odeszli od nich i wynik nie był złym kodem, ale takie pozytywne odchylenia są bardzo nieliczne. Przynajmniej 99 na 100 takich odchyleń, które widziałem, było nieuzasadnionych. Jednak równie dobrze może być 999 na 1000. Więc lepiej trzymać się poniższych zasad.

  1. Ilekroć znaczenie operatora nie jest oczywiste i bezdyskusyjne, nie powinno być przeciążone.  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ż jest wiele informacji o przeciążeniu operatorów, więc wiele artykułów, rozdziałów w książkach i innych tekstów zajmuje się tym wszystkim. Ale pomimo tych pozornie oczywistych dowodów, jest tylko zaskakująco niewiele przypadków, w których operator jest przeciążony. Powodem jest to, że w rzeczywistości trudno jest zrozumieć semantykę stojącą za zastosowaniem operatora, chyba że użycie operatora w domenie aplikacji jest dobrze znane i bezdyskusyjne. Wbrew powszechnemu przekonaniu, tak rzadko się zdarza.

  2. Zawsze trzymaj się dobrze znanej semantyki operatora.
    C ++ nie stanowi ograniczenia semantyki przeciążonych operatorów. Twój kompilator z przyjemnością zaakceptuje kod implementujący plik binarny + operator, aby odjąć od jego prawego argumentu. Jednak użytkownicy takiego operatora nigdy nie będą podejrzewać tego wyrażenia a + b do odjęcia a od b. Oczywiście zakłada to, że semantyka operatora w dziedzinie aplikacji jest niekwestionowana.

  3. Zawsze dostarczaj wszystkich z zestawu powiązanych operacji.
    Operatory są ze sobą powiązanei do innych operacji. Jeśli twój typ obsługuje a + b, użytkownicy będą oczekiwać połączenia a += b, także. Jeśli obsługuje przyrost prefiksu ++a, będą oczekiwać a++ do pracy również. Jeśli mogą sprawdzić, czy a < b, na pewno będą oczekiwać, że będą w stanie sprawdzić, czy a > b. Jeśli mogą kopiować - skonstruować swój typ, oczekują, że przypisanie również zadziała.


Kontynuuj Decyzja między Członkiem a Członkiem niebędącym członkiem.


441
2017-12-12 12:45



Jedyne, o czym wiem, że narusza którąkolwiek z tych rzeczy, to boost::spirit lol. - Billy ONeal
@Billy: Według niektórych, nadużywa + w przypadku konkatenacji ciągów znaków jest to naruszenie, ale stało się już dobrze ugruntowaną praktyką, więc wydaje się naturalne. Chociaż pamiętam klasę smyczkową z domowego parzenia, którą widziałem w latach 90., która używała binarnego &w tym celu (w odniesieniu do BASIC dla ustalonej praxis). Ale, tak, umieszczenie go w std lib po prostu ułożyło to w kamień. To samo dotyczy nadużywania << i >> dla IO, BTW. Dlaczego przesunięcie w lewo było oczywistą operacją wyjściową? Ponieważ wszyscy dowiedzieliśmy się o tym, kiedy zobaczyliśmy nasz pierwszy "Hello, world!" podanie. I bez żadnego innego powodu. - sbi
@curiousguy: Jeśli musisz to wyjaśnić, nie jest to oczywiste i bezdyskusyjne. Podobnie, jeśli chcesz omówić lub obronić przeciążenie. - sbi
@sbi: "peer review" jest zawsze dobrym pomysłem. Dla mnie źle dobrany operator nie różni się od źle wybranej nazwy funkcji (widziałem wiele). Operator to tylko funkcje. Nie więcej nie mniej. Zasady są takie same. Aby zrozumieć, czy idea jest dobra, najlepiej zrozumieć, ile czasu potrzeba na zrozumienie. (Dlatego też wzajemna ocena jest koniecznością, ale rówieśników należy wybierać między ludźmi wolnymi od dogmatów i uprzedzeń.) - Emilio Garavaglia
@sbi Dla mnie jedyny absolutnie oczywisty i niepodważalny fakt 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 w kontenerach. Co oznacza równość? "a równa się b" Oznacza to, że a i b mają tę samą wartość matematyczną. Pojęcie wartości matematycznej (nie-NaN) float jest jasne, ale wartość matematyczna kontenera może mieć wiele różnych użytecznych definicji rekurencyjnych (typu rekurencyjnego). Najsilniejszą definicją równości jest "są to te same obiekty" i jest ona bezużyteczna. - curiousguy


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

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

Nie wszyscy operatorzy mogą być przeciążeni w C ++. Wśród operatorów, których nie można przeciążyć, należą: .  ::  sizeof  typeid  .* i jedynym potrójnym operatorem w C ++, ?: 

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

  • operatory arytmetyczne: +  -  *  /  % i +=  -=  *=  /=  %= (wszystkie infix binarne); +  - (jednokrotny prefiks); ++  -- (jednokrotny przedrostek i postfiks)
  • manipulacja bitami: &  |  ^  <<  >> i &=  |=  ^=  <<=  >>= (wszystkie infix binarne); ~ (jednokrotny prefiks)
  • algebra boolowska: ==  !=  <  >  <=  >=  ||  && (wszystkie infix binarne); ! (jednokrotny prefiks)
  • zarządzanie pamięcią: new  new[]  delete  delete[]
  • niejawni operatorzy konwersji
  • zbieranina: =  []  ->  ->*  ,  (wszystkie infix binarne); *  & (wszystkie jednokrotne przedrostki) () (wywołanie funkcji, infiks n-ary)

Jednak fakt, że ty mogą przeciążenie wszystkich z nich nie oznacza ciebie powinien Zrób tak. Zobacz podstawowe zasady przeciążania operatorów.

W C ++ operatorzy są przeciążeni w postaci funkcje o specjalnych nazwach. Podobnie jak w przypadku innych funkcji, przeciążone operatory mogą być zwykle implementowane jako funkcja członkowska ich lewego typu operandu lub jak funkcje nie będące członkami. To, czy możesz swobodnie wybierać, czy korzystać z jednego, zależy od kilku kryteriów.2 Operator jednoargumentowy @3, zastosowane do obiektu x, jest wywoływane albo jako operator@(x) lub jak x.operator@(). Binarny operator infiksów @, zastosowane do obiektów x i y, nazywany jest albo operator@(x,y) lub jak x.operator@(y).4 

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

1  Termin "zdefiniowany przez użytkownika" może być nieco mylący. C ++ rozróżnia typy wbudowane i typy zdefiniowane przez użytkownika. Do tych pierwszych należą na przykład int, char i double; do tych drugich należą wszystkie typy struct, class, union i enum, w tym te z biblioteki standardowej, nawet jeśli nie są one zdefiniowane przez użytkowników.

2  Obejmuje to późniejsza część tego FAQ.

3  The @ nie jest prawidłowym operatorem w C ++, dlatego używam go jako symbolu zastępczego.

4  Jedyny potrójny operator w C ++ nie może być przeciążony, a jedyny n-ary operator musi zawsze zostać zaimplementowany jako funkcja składowa.


Kontynuuj Trzy podstawowe reguły przeciążania operatorów w C ++.


230
2017-12-12 12:46



%= nie jest operatorem "nieco manipulacji" - curiousguy
~ jest jednoargumentowym przedrostkiem, a nie infiksem binarnym. - mrkj
.* brakuje na liście operatorów nieprzeciążonych. - celticminstrel
@celticminstrel: Rzeczywiście, i nikt nie zauważył przez 4,5 roku ... Dzięki za wskazanie, włożyłem to. - sbi
@ H.R .: Czy przeczytałeś ten przewodnik, wiedziałbyś, co jest nie tak. Generalnie sugeruję, abyś przeczytał pierwsze trzy odpowiedzi powiązane z pytaniem. To nie powinno trwać dłużej niż pół godziny życia i daje podstawowe zrozumienie. Składnia specyficzna dla operatora, którą możesz później sprawdzić. Twój konkretny problem sugeruje, że próbujesz przeładować operator+() jako funkcja członkowska, ale nadał jej podpis funkcji swobodnej. Widzieć tutaj. - sbi


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

Operatory binarne = (zadanie), [] (subskrypcja macierzy), -> (dostęp do członka), jak również n-ary ()(wywołanie funkcji) operator musi zawsze być zaimplementowany jako funkcje członka, ponieważ wymaga tego składnia języka.

Inni operatorzy mogą być wdrażani jako członkowie lub jako członkowie niebędący członkami. Niektóre z nich jednak zwykle muszą być implementowane jako funkcje nie będące członkami, ponieważ ich lewy operand nie może być modyfikowany przez ciebie. Najważniejszym z nich są operatorzy wejścia i wyjścia << i >>, którego lewe operandy są klasami strumieniowymi ze standardowej biblioteki, których nie można zmienić.

Dla wszystkich operatorów, którzy muszą wybrać, aby wprowadzić je jako funkcję członkowską lub funkcję nie będącą członkiem, użyj następujących zasad zdecydować:

  1. Jeśli to jest jednoargumentowy operator, zaimplementuj go jako członek funkcjonować.
  2. Jeśli traktuje cię operator binarny oba operandy jednakowo (pozostawia je bez zmian), zaimplementuj tego operatora jako nie-członek funkcjonować.
  3. Jeśli działa operator binarny nie traktować oba operandy na równi (zazwyczaj zmienia lewy operand), może być użyteczne, aby to zrobić członek funkcja jego lewego operandu, jeśli musi uzyskać dostęp do prywatnych części operandu.

Oczywiście, jak w przypadku wszystkich reguł, są wyjątki. Jeśli masz typ

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

i chcesz przeciążyć operatory inkrementacji i dekrementacji, nie możesz tego zrobić jako funkcji składowej, ponieważ w C ++ typy enum nie mogą mieć funkcji składowych. Więc musisz przeciążać go jako darmową funkcję. I operator<() Szablon klasy zagnieżdżony w szablonie klasy jest o wiele łatwiejszy do napisania i odczytania, gdy zostanie wykonany jako funkcja składowa wbudowana w definicję klasy. Ale są to rzeczywiście rzadkie wyjątki.

(Jednak, gdyby robisz wyjątek, nie zapomnij o problemie const-ness dla operandu, który dla funkcji składowych staje się niejawny this argument. Jeśli operator jako funkcja nie będąca członkiem, zająłby jego najbardziej lewy argument jako const referencyjny, ten sam operator co funkcja członkowska musi mieć const na koniec zrobić *this za const odniesienie.)


Kontynuuj Typowi operatorzy przeciążają.


212
2017-12-12 12:49



Element Herb Sutter w Effective C ++ (czy jest to C ++ Coding Standards?) Mówi, że należy preferować nie-członkowskie funkcje nie-przyjacielskie do funkcji członkowskich, aby zwiększyć hermetyzację klasy. IMHO, powód enkapsulacji ma pierwszeństwo przed twoją regułą, ale nie obniża wartości jakości twojej reguły. - paercebal
@paercebal: Efektywny C ++ jest przez Meyersa, Standardy kodowania C ++ przez Sutter. Do którego się odnosisz? W każdym razie nie podoba mi się pomysł, powiedzmy, operator+=() nie będąc członkiem. Musi zmienić lewy operand, więc z definicji musi kopać głęboko w swoich wnętrznościach. Co zyskałbyś, nie czyniąc go członkiem? - sbi
@sbi: Item 44 in C ++ Standardy kodowania (Sutter) Wolisz pisać niepochodzące funkcje niezrzeszone, oczywiście, ma to zastosowanie tylko wtedy, gdy faktycznie możesz napisać tę funkcję przy użyciu tylko publicznego interfejsu klasy. Jeśli nie możesz (lub możesz, ale to źle wpłynęło na wydajność), musisz zrobić to albo członkiem albo przyjacielem. - Matthieu M.
@sbi: Oops, Effective, Exceptional ... Nic dziwnego, że wymieszałem nazwy. W każdym razie zysk polega na ograniczeniu w jak największym stopniu liczby funkcji, które mają dostęp do danych prywatnych / chronionych obiektu. W ten sposób zwiększasz hermetyzację swojej klasy, ułatwiając jej utrzymanie / testowanie / ewolucję. - paercebal
@sbi: Jeden przykład. Załóżmy, że kodujesz klasę String, z obydwoma operator += i append metody. The append metoda jest bardziej kompletna, ponieważ możesz dołączyć podciąg parametru z indeksu i do indeksu n -1: append(string, start, end) Wydaje się logiczne mieć += zadzwoń dołącz z start = 0 i end = string.size. W tym momencie append może być metodą członka, ale operator += nie musi być członkiem, a uczynienie z niego elementu niezwiązanego zmniejszyłoby ilość kodu odtwarzanego za pomocą instrantów smyczkowych, więc to dobrze ... ^ _ ^ ... - paercebal


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

W C ++ można tworzyć operatory konwersji, operatory, które pozwalają kompilatorowi konwertować typy i inne zdefiniowane typy. Istnieją dwa typy operatorów konwersji, domyślne i jawne.

Operatory konwersji niejawnej (C ++ 98 / C ++ 03 i C ++ 11)

Niejawny operator konwersji umożliwia niejawną konwersję kompilatora (np. Konwersję między int i long) wartość typu zdefiniowanego przez użytkownika do innego typu.

Poniżej przedstawiono prostą klasę z niejawnym operatorem konwersji:

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

Niejawni operatory konwersji, takie jak konstruktory jednoargumentowe, są konwersjami zdefiniowanymi przez użytkownika. Kompilatorzy przyznają jedną konwersję zdefiniowaną 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 nawet kopie, gdy nie oczekuje się. W poniższym kodzie void f(const char*)zostanie wywołany, ponieważ my_string() nie jest lwartość, więc pierwsze nie pasuje:

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

f(my_string());

Początkujący łatwo się mylą i nawet doświadczeni programiści C ++ są czasami zaskoczeni, ponieważ kompilator wybiera przeciążenie, którego nie podejrzewali. Problemy te mogą zostać złagodzone przez operatorów konwersji jawnych.

Jawne operatory konwersji (C ++ 11)

W przeciwieństwie do niejawnych operatorów konwersji, operatorzy konwersji jawnych nigdy nie wskoczą, gdy 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 spróbujesz wykonać nieoczekiwany kod z niejawnych operatorów konwersji, pojawi się błąd kompilatora:

prog.cpp: W funkcji 'int main ()':
prog.cpp: 15: 18: błąd: brak pasującej funkcji dla połączenia z 'f (my_string)'
prog.cpp: 15: 18: uwaga: kandydatami są:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: note: brak znanej konwersji dla argumentu 1 z 'my_string' na 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: uwaga: brak znanej konwersji dla argumentu 1 z 'my_string' na 'const char *'

Aby wywołać operatora rzutowania jawnego, musisz użyć static_cast, rzut w stylu C lub rzutowanie w stylu konstruktora (tj. T(value) ).

Istnieje jednak jeden wyjątek: kompilator może domyślnie dokonać konwersji bool. Ponadto kompilator nie może wykonać innej niejawnej konwersji po konwersji bool (Kompilator może wykonywać 2 niejawne konwersje jednocześnie, ale tylko 1 zdefiniowana przez użytkownika konwersja przy maks.).

Ponieważ kompilator nie rzuci "przeszłości" bool, operatorzy jawnych konwersji eliminują obecnie potrzebę Bezpieczny idiom. Na przykład inteligentne wskaźniki przed C ++ 11 używały idiomu Safe Bool, aby zapobiec konwersji do typów integralnych. W C ++ 11 inteligentne wskaźniki używają jawnego operatora, ponieważ kompilator nie może niejawnie przekonwertować na typ integralny po tym, jak jawnie przekonwertował typ na bool.

Kontynuuj Przeciążenie new i delete.


144
2018-05-17 18:32





Przeciążenie new i delete

Uwaga: Dotyczy to tylko składnia przeciążenia new i delete, nie z realizacja takich przeciążonych operatorów. Myślę, że semantyka przeciążania new i delete zasługują na własne FAQ, w temacie przeciążania operatorów nie mogę tego zrobić sprawiedliwie.

Podstawy

W C ++, kiedy piszesz nowe wyrażenie lubić new T(arg) Dwie rzeczy się zdarzą, gdy to wyrażenie zostanie ocenione: Najpierw operator new jest wywoływany w celu uzyskania surowej pamięci, a następnie odpowiedniego konstruktora T jest wywoływany, aby włączyć tę surową pamięć do poprawnego obiektu. Podobnie, gdy usuniesz obiekt, najpierw wywoływany jest jego destruktor, a następnie pamięć jest zwracana operator delete.
C ++ pozwala dostroić obie te operacje: zarządzanie pamięcią i konstrukcję / zniszczenie obiektu w przydzielonej pamięci. Ta ostatnia jest wykonywana przez pisanie konstruktorów i destruktorów dla klasy. Dostrajanie zarządzania pamięcią odbywa się poprzez napisanie własnego operator new i operator delete.

Pierwsza z podstawowych zasad przeciążania operatorów - nie rób tego - dotyczy szczególnie przeciążenia new i delete. Prawie jedynym powodem do przeciążenia tych operatorów są problemy z wydajnością i ograniczenia pamięci, aw wielu przypadkach inne działania, takie jak zmiany w algorytmach używane, zapewni dużo wyższy stosunek kosztów do zysku niż próba zmodyfikowania zarządzania pamięcią.

Biblioteka standardowa C ++ zawiera zestaw wstępnie zdefiniowanych new i delete operatoró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, dwa ostatnie dla tablicy obiektów. Jeśli podasz swoje własne wersje, będą nie przeciążaj, ale wymień te ze standardowej biblioteki.
Jeśli przeciążasz operator new, zawsze należy również przeciążać dopasowanie operator delete, nawet jeśli nigdy nie zamierzasz tego nazywać. Powodem jest to, że jeśli konstruktor rzuca podczas oceny nowego wyrażenia, system wykonawczy zwróci pamięć do operator delete pasujące do operator new który został wywołany w celu przydzielenia pamięci do utworzenia obiektu. Jeśli nie podasz dopasowania operator delete, wywoływana jest nazwa domyślna, co prawie zawsze jest błędne.
Jeśli przeciążasz new i delete, powinieneś rozważyć przeciążenie także wariantów macierzy.

Umieszczenie new

C ++ pozwala nowym operatorom kasować dodatkowe argumenty.
Tak zwany placement new umożliwia utworzenie obiektu 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 usuwania dla tego:

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 przykładowym kodzie umieszczenia nowego, podanego powyżej, operator delete nigdy nie jest wywoływana, chyba że konstruktor X zgłasza wyjątek.

Możesz także przeciążać new i delete z innymi argumentami. Podobnie jak w przypadku dodatkowego argumentu dotyczącego umieszczania new, argumenty te są również wymienione w nawiasie po słowie kluczowym new. Z powodów historycznych takie warianty są często nazywane nowymi miejscami, nawet jeśli ich argumenty nie dotyczą umieszczania obiektu pod konkretnym adresem.

Nowa klasa i usuń

Najczęściej będziesz chciał precyzyjnie dostroić zarządzanie pamięcią, ponieważ pomiar wykazał, że instancje danej klasy lub grupy powiązanych klas są często tworzone i niszczone, oraz że domyślne zarządzanie pamięcią systemu czasu, dostrojone dla ogólne wyniki, w tym konkretnym przypadku radzi sobie nieefektywnie. Aby to poprawić, możesz przeciążyć nowe i usunąć dla konkretnej 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);
    // ... 
}; 

Przeciążone w ten sposób nowe i usuwane zachowują się jak statyczne funkcje składowe. Dla obiektów o my_class, std::size_t Argument będzie zawsze sizeof(my_class). Jednak operatory te są również wywoływane dla obiektów przydzielanych dynamicznie klasy pochodne, w którym to przypadku może być większa.

Globalny nowy i usuń

Aby przeciążyć globalne nowe i usunąć, wystarczy zamienić wcześniej zdefiniowane operatory standardowej biblioteki na własne. Jednak rzadko to musi być zrobione.


131
2017-12-12 13:07



Nie zgadzam się również z tym, że zastąpienie operatora globalnego nowym i usunięcie jest zwykle związane z wydajnością: wręcz przeciwnie, zwykle służy do śledzenia błędów. - Yttrill
Należy również pamiętać, że jeśli używasz przeciążonego nowego operatora, musisz podać operator delete z pasującymi argumentami. Mówisz to w sekcji o globalnym nowym / usuń tam, gdzie nie jest to zbytnio interesujące. - Yttrill
@Teraz jesteś mylący. The znaczenie zostaje przeciążony. Co oznacza "przeciążenie operatora" oznacza, że ​​znaczenie jest przeciążone. Nie oznacza to, że funkcje dosłownie są przeciążone, i w szczególności operator new nie przeładuje wersji Standard. @sbi nie twierdzi czegoś przeciwnego. Powszechnie nazywa się to "przeciążaniem nowych", ponieważ często mówi się "przeciążanie operatora dodawania". - Johannes Schaub - litb
@sbi: Zobacz (lub lepiej, link do) gotw.ca/publications/mill15.htm . Jest to tylko dobra praktyka w stosunku do ludzi, którzy czasami używają nothrow Nowy. - Alexandre C.
"Jeśli nie podasz pasującego operatora, domyślny nazywa się" -> Właściwie, jeśli dodasz jakiekolwiek argumenty i nie utworzysz zgodnego usunięcia, żadne usuwanie operatora nie będzie w ogóle wywoływane i wystąpi przeciek pamięci. (15.2.2, pamięć zajmowana przez obiekt jest deallokowana tylko wtedy, gdy zostanie znaleziony odpowiedni operator delete) - dascandy


Dlaczego nie można operator<< funkcja do strumieniowania obiektów do std::cout lub do pliku może być funkcją członka?

Załóżmy, że masz:

struct Foo
{
   int a;
   double b;

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

Biorąc to pod uwagę, nie możesz użyć:

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

Od operator<< jest przeciążone jako funkcja składowa Foo, LHS operatora musi być Foo obiekt. Co oznacza, że ​​będziesz musiał użyć:

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

co jest bardzo nieintuicyjne.

Jeśli zdefiniujesz go 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żyć:

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

który jest bardzo intuicyjny.


29
2018-01-22 19:00