Merk: Svarene ble gitt i en bestemt rekkefølge, men siden mange brukere sorterer svarene i henhold til stemmer, snarere enn tidspunktet de ble gitt, er det her en indeks over svarene i den rekkefølgen de gir mest mening:
_(Merk: Dette er ment å være en oppføring til Stack Overflow&# 39s C++ FAQ. Hvis du ønsker å kritisere ideen om å gi en FAQ i denne formen, så innlegget på meta som startet alt dette ville være stedet å gjøre det. Svarene på dette spørsmålet overvåkes i C++-chatrommet, der FAQ-ideen startet i utgangspunktet, så det er svært sannsynlig at svaret ditt blir lest av dem som kom opp med ideen.).
Det meste av arbeidet med å overbelaste operatorer er kjeleplatekode. Det er ikke så rart, siden operatorer bare er syntaktisk sukker, kan deres faktiske arbeid gjøres av (og blir ofte videresendt til) vanlige funksjoner. Men det er viktig at du får denne koden riktig. Hvis du mislykkes, vil enten operatørkoden din ikke kompilere, eller brukerkoden din vil ikke kompilere, eller brukerkoden din vil oppføre seg overraskende.
Det er mye å si om tildeling. Imidlertid har det meste allerede blitt sagt i GMan&# 39s berømte Copy-And-Swap FAQ, så jeg vil hoppe over det meste av det her, og bare oppgi den perfekte tilordningsoperatoren som referanse:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Bitshift-operatorene <<
og >>
, selv om de fortsatt brukes i maskinvaregrensesnitt for bit-manipulasjonsfunksjonene de arver fra C, har blitt mer utbredt som overbelastede strøminngangs- og utgangsoperatorer i de fleste applikasjoner. For veiledning om overbelastning som bitmanipulasjonsoperatorer, se avsnittet nedenfor om binære aritmetiske operatorer. For å implementere ditt eget egendefinerte format og analyseringslogikk når objektet ditt brukes med iostreams, fortsett.
Strømoperatorene, som er blant de mest overbelastede operatørene, er binære infiksoperatorer der syntaksen ikke spesifiserer noen begrensning på om de skal være medlemmer eller ikke-medlemmer.
Siden de endrer sitt venstre argument (de endrer strømmens tilstand), bør de i henhold til tommelfingerreglene implementeres som medlemmer av sin venstre operands type. Deres venstre operander er imidlertid strømmer fra standardbiblioteket, og selv om de fleste av strømutgangs- og inngangsoperatørene som er definert av standardbiblioteket faktisk er definert som medlemmer av strømklassene, kan du ikke endre standardbibliotekets strømtyper når du implementerer utgangs- og inngangsoperasjoner for dine egne typer. Derfor må du implementere disse operatørene for dine egne typer som ikke-medlemsfunksjoner.
De kanoniske formene for de to er disse:
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;
}
Når du implementerer operator>>
, er det bare nødvendig å angi strømmens tilstand manuelt når selve avlesningen lyktes, men resultatet er ikke det som forventes.
Funksjonskalloperatoren, som brukes til å opprette funksjonsobjekter, også kjent som funktorer, må defineres som en member-funksjon, slik at den alltid har det implisitte this
-argumentet til medlemsfunksjoner. Bortsett fra dette kan den overbelastes for å ta et hvilket som helst antall ekstra argumenter, inkludert null.
Her er et eksempel på syntaksen:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Bruk:
foo f;
int a = f("hello");
Gjennom hele C++ standardbiblioteket kopieres alltid funksjonsobjekter. Dine egne funksjonsobjekter bør derfor være billige å kopiere. Hvis et funksjonsobjekt absolutt må bruke data som er dyre å kopiere, er det bedre å lagre disse dataene et annet sted og la funksjonsobjektet referere til dem.
Sammenligningsoperatorer med binært prefiks bør i henhold til tommelfingerreglene implementeres som ikke-medlemsfunksjoner1. Den unære prefiksnegasjonen !
bør (i henhold til de samme reglene) implementeres som en medlemsfunksjon. (men det er vanligvis ikke en god idé å overbelaste den).
Standardbibliotekets algoritmer (f.eks. std::sort()
) og typer (f.eks. std::map
) vil alltid bare forvente at operator<
er til stede. Brukerne av typen din vil imidlertid forvente at alle de andre operatorene også er til stede, så hvis du definerer operator<
, må du følge den tredje grunnleggende regelen for overbelastning av operatorer og også definere alle de andre boolske sammenligningsoperatorene. Den kanoniske måten å implementere dem på er denne:
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);}
Det viktige å merke seg her er at bare to av disse operatørene faktisk gjør noe, de andre videresender bare argumentene sine til en av disse to for å gjøre det faktiske arbeidet.
Syntaksen for overbelastning av de resterende binære boolske operatorene (||
, &&
) følger reglene for sammenligningsoperatorene. Det er imidlertid svært usannsynlig at du vil finne et fornuftig brukstilfelle for disse2.
1 Som med alle tommelfingerregler kan det noen ganger være grunner til å bryte denne også. I så fall må du ikke glemme at den venstre operanden til de binære sammenligningsoperatorene, som for medlemsfunksjoner vil være *this
, også må være const
. Så en sammenligningsoperator implementert som en medlemsfunksjon må ha denne signaturen:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Legg merke til const
på slutten.)
2 Det bør bemerkes at den innebygde versjonen av ||
og &&
bruker snarveissemantikk. Mens de brukerdefinerte (fordi de er syntaktisk sukker for metodeanrop) ikke bruker snarveissemantikk. Brukeren vil forvente at disse operatørene har snarveissemantikk, og koden deres kan avhenge av den, derfor anbefales det på det sterkeste å ALDRI definere dem.
De unære inkrement- og dekrementoperatorene kommer i både prefiks og postfiks smak. For å skille den ene fra den andre, tar postfix-variantene et ekstra dummy int-argument. Hvis du overbelaster inkrement eller dekrement, må du alltid implementere både prefiks- og postfiksversjoner. Her er den kanoniske implementeringen av inkrement, decrement følger de samme reglene:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Merk at postfix-varianten er implementert i form av prefiks. Legg også merke til at postfix gjør en ekstra kopi.2
Overbelastning av unary minus og pluss er ikke veldig vanlig og bør sannsynligvis unngås. Hvis det er nødvendig, bør de sannsynligvis overbelastes som medlemsfunksjoner.
2 Legg også merke til at postfix-varianten gjør mer arbeid og derfor er mindre effektiv å bruke enn prefix-varianten. Dette er en god grunn til generelt å foretrekke prefiksøkning fremfor postfiksøkning. Mens kompilatorer vanligvis kan optimalisere bort det ekstra arbeidet med postfix inkrement for innebygde typer, kan de kanskje ikke gjøre det samme for brukerdefinerte typer (som kan være noe så uskyldig som en liste-iterator). Når du har blitt vant til å gjøre i++
, blir det veldig vanskelig å huske å gjøre ++i
i stedet når i
ikke er av en innebygd type (pluss at du'd må endre kode når du endrer en type), så det er bedre å gjøre det til en vane å alltid bruke prefix inkrement, med mindre postfix er eksplisitt nødvendig.
For de binære aritmetiske operatørene, ikke glem å adlyde den tredje grunnleggende regelen operatør overbelastning: Hvis du oppgir +
, må du også oppgi +=
, hvis du oppgir -
, må du ikke utelate -=
osv. Andrew Koenig sies å ha vært den første til å observere at de sammensatte tilordningsoperatorene kan brukes som base for sine ikke-sammensatte motstykker. Det vil si at operatoren +
implementeres i form av +=
, -
implementeres i form av -=
osv.
I henhold til våre tommelfingerregler skal +
og dens ledsagere være ikke-medlemmer, mens deres sammensatte motstykker (+=
osv.), som endrer deres venstre argument, skal være et medlem. Her er eksempelkoden for +=
og +
; de andre binære aritmetiske operatorene bør implementeres på samme måte:
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+=
returnerer resultatet per referanse, mens operator+
returnerer en kopi av resultatet. Selvfølgelig er det vanligvis mer effektivt å returnere en referanse enn å returnere en kopi, men i tilfellet operator+
er det ingen vei utenom kopieringen. Når du skriver a + b
, forventer du at resultatet skal være en ny verdi, og derfor må operator+
returnere en ny verdi.3
Legg også merke til at operator+
tar sin venstre operand ved copy i stedet for ved const-referanse. Årsaken til dette er den samme som for operator=
som tar sitt argument per kopi.
Bitmanipuleringsoperatorene ~
&
|
^
<<
>>
bør implementeres på samme måte som de aritmetiske operatorene. Imidlertid er det (bortsett fra overbelastning av <<
og >>
for utdata og inndata) svært få rimelige brukstilfeller for overbelastning av disse.
3 Igjen er lærdommen fra dette at a += b
generelt er mer effektivt enn a + b
og bør foretrekkes hvis mulig.
Operatoren array subscript er en binær operator som må implementeres som et klassemedlem. Den brukes for containerlignende typer som gir tilgang til dataelementene ved hjelp av en nøkkel. Den kanoniske formen for å angi disse er denne:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Med mindre du ikke vil at brukere av klassen din skal kunne endre dataelementer som returneres av operator[]
(i så fall kan du utelate non-const-varianten), bør du alltid angi begge variantene av operatoren.
Hvis value_type er kjent for å referere til en innebygd type, bør const-varianten av operatoren heller returnere en kopi i stedet for en const-referanse:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
For å definere dine egne iteratorer eller smartpekere, må du overbelaste den unære prefiks dereference-operatoren *
og den binære infix pointer member access-operatoren ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
-operatoren, hvis value_type
er av typen class
(eller struct
eller union
), kalles en annen operator->()
rekursivt, inntil en operator->()
returnerer en verdi av ikke-klassetype.
Den unære address-of-operatoren skal aldri overbelastes.
For operatør->*()
se dette spørsmålet. Den brukes sjelden og blir derfor sjelden overbelastet. Selv iteratorer overbelaster den faktisk ikke.Fortsett til Konverteringsoperatorer
Når det gjelder overbelastning av operatorer i C++, er det tre grunnleggende regler du bør følge. Som med alle slike regler er det faktisk unntak. Noen ganger har folk avveket fra dem, og resultatet var ikke dårlig kode, men slike positive avvik er få og langt mellom. I det minste var 99 av 100 slike avvik jeg har sett, uberettiget. Det kunne imidlertid like gjerne ha vært 999 av 1000. Så du bør holde deg til følgende regler.
Når betydningen av en operatør ikke er åpenbart klar og ubestridt, bør den ikke overbelastes. _Gi i stedet en funksjon med et godt valgt navn. I utgangspunktet sier den første og viktigste regelen for overbelastning av operatorer, i sin kjerne: Ikke gjør det. Det kan virke rart, fordi det er mye å vite om overbelastning av operatører, og mange artikler, bokkapitler og andre tekster omhandler alt dette. Men til tross for disse tilsynelatende åpenbare bevisene, er det bare overraskende få tilfeller der overbelastning av operatører er hensiktsmessig. Årsaken er at det faktisk er vanskelig å forstå semantikken bak bruken av en operator med mindre bruken av operatoren i applikasjonsdomenet er velkjent og ubestridt. I motsetning til hva mange tror, er dette nesten aldri tilfelle.
Hold deg alltid til operatørens velkjente semantikk.___.
C++ har ingen begrensninger på semantikken til overbelastede operatorer. Kompilatoren din vil gjerne godta kode som implementerer den binære +
-operatoren for å trekke fra den høyre operanden. Brukerne av en slik operator vil imidlertid aldri mistenke uttrykket a + b
for å trekke a
fra b
. Dette forutsetter selvfølgelig at semantikken til operatøren i applikasjonsdomenet er ubestridt.
Altid gi alle ut av et sett med relaterte operasjoner..
Operatorer er relatert til hverandre og til andre operasjoner. Hvis din type støtter a + b
, vil brukerne forvente å kunne kalle a += b
også. Hvis den støtter prefiksøkningen ++a
, vil de forvente at a++
også fungerer. Hvis de kan sjekke om a < b
, vil de helt sikkert også forvente å kunne sjekke om a > b
. Hvis de kan kopi-konstruere typen din, forventer de at oppgaven også fungerer.
Fortsett til Avgjørelsen mellom medlem og ikke-medlem.
Den generelle syntaksen for overbelastning av operatorer i C++ ##.
Du kan ikke endre betydningen av operatorer for innebygde typer i C++, operatorer kan bare overbelastes for brukerdefinerte typer1. Det vil si at minst en av operandene må være av en brukerdefinert type. Som med andre overbelastede funksjoner, kan operatorer bare overbelastes for et bestemt sett med parametere én gang.
Ikke alle operatorer kan overbelastes i C++. Blant operatørene som ikke kan overbelastes er: .
::
sizeof
typeid
.*
og den eneste ternære operatoren i C++, ?:
.
Blant operatørene som kan overbelastes i C++ er disse:
+
-
*
/
%
og +=
-=
*=
/=
%=
(alle binære infix); +
-
(unary prefix); ++
--
(unary prefix og postfix)&
|
^
<<
>>
og &=
|=
^=
<<=
>>=
(alt binært infiks); ~
(unært prefiks)==
!=
<
>
<=
>=
||
&&
(alt binært infiks); !
(unært prefiks)new
new[]
delete
delete[]
delete[]
=
[]
->
->*
,
(alt binært prefiks); *
&
(alt unært prefiks) ()
(funksjonskall, n-ært prefiks)Det faktum at du kan overbelaste alle disse betyr imidlertid ikke at du bør gjøre det. Se de grunnleggende reglene for overbelastning av operatorer.
I C++ overbelastes operatorer i form av funksjoner med spesielle navn. Som med andre funksjoner kan overbelastede operatorer generelt implementeres enten som en medlemsfunksjon av deres venstre operands type eller som ikke-medlemsfunksjoner. Hvorvidt du er fri til å velge eller bundet til å bruke en av dem, avhenger av flere kriterier.2 En unær operator @
3, anvendt på et objekt x, påkalles enten som operator@(x)
eller som x.operator@()
. En binær infiksoperator @
, anvendt på objektene x
og y
, kalles enten som operator@(x,y)
eller som x.operator@(y)
.4
Operatorer som er implementert som ikke-medlemsfunksjoner er noen ganger venn av sin operands type.
1 Begrepet "brukerdefinert" kan være litt misvisende. C++ skiller mellom innebygde typer og brukerdefinerte typer. Til førstnevnte hører for eksempel int, char og double; til sistnevnte hører alle struct-, class-, union- og enum-typer, inkludert de fra standardbiblioteket, selv om de ikke som sådan er definert av brukere.
2 Dette er dekket i en senere del av denne FAQ.
3 @` er ikke en gyldig operator i C++, og derfor bruker jeg den som en plassholder.
4 Den eneste ternære operatøren i C++ kan ikke overbelastes, og den eneste n-ære operatøren må alltid implementeres som en medlemsfunksjon.
Fortsett til The Three Basic Rules of Operator Overloading in C++.