Bemærk: Svarene blev givet i en bestemt rækkefølge, men da mange brugere sorterer svarene efter stemmer, snarere end efter tidspunktet, hvor de blev givet, er her et indeks over svarene i den rækkefølge, hvor de giver mest mening:
(Bemærk: Dette er ment som et indlæg til Stack Overflow's C++ FAQ. Hvis du ønsker at kritisere ideen om at stille en FAQ til rådighed i denne form, så er det indlæg på meta, der startede alt dette det rette sted at gøre det. Svarene på dette spørgsmål overvåges i C++-chatroom, hvor FAQ-idéen oprindeligt startede, så dit svar vil med stor sandsynlighed blive læst af dem, der kom med idéen.) )
Det meste af arbejdet med at overloade operatorer er kodekode. Det er ikke så mærkeligt, da operatorer blot er syntaktisk sukker, og deres egentlige arbejde kunne udføres af (og bliver ofte videresendt til) almindelige funktioner. Men det er vigtigt, at du får denne boiler-plate-kode korrekt. Hvis du fejler, vil enten din operatørkode ikke kunne kompileres, eller dine brugeres kode vil ikke kunne kompileres, eller dine brugeres kode vil opføre sig overraskende.
Der er meget at sige om assignment. Det meste af det er dog allerede blevet sagt i GMan's berømte Copy-And-Swap FAQ, så jeg springer det meste over her, og nævner kun den perfekte assignment-operator som reference:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Bitshift-operatorerne <<
og >>
, der ganske vist stadig bruges i hardware-interfacing for de bit-manipulationsfunktioner, de arver fra C, er blevet mere udbredt som overbelastede stream input- og output-operatører i de fleste applikationer. For vejledning om overbelastning som bit-manipulationsoperatører, se afsnittet nedenfor om binære aritmetiske operatører. Fortsæt med at implementere din egen brugerdefinerede format- og parsinglogik, når dit objekt bruges med iostreams.
Stream-operatorerne, der er blandt de mest overloadede operatører, er binære infix-operatører, for hvilke syntaksen ikke angiver nogen begrænsning for, om de skal være medlemmer eller ikke-medlemmer.
Da de ændrer deres venstre argument (de ændrer streamens tilstand), bør de ifølge tommelfingerreglerne implementeres som medlemmer af deres venstre operands type. Deres venstre operander er imidlertid streams fra standardbiblioteket, og selv om de fleste af de output- og input-operatorer for streams, der er defineret af standardbiblioteket, faktisk er defineret som medlemmer af stream-klasserne, kan du ikke ændre standardbibliotekets stream-typer, når du implementerer output- og input-operationer for dine egne typer. Derfor skal du implementere disse operatorer for dine egne typer som ikke-medlemsfunktioner.
De kanoniske former 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 kun nødvendigt at indstille streamens tilstand manuelt, når selve læsningen lykkedes, men resultatet ikke er som forventet.
Funktionskaldsoperatoren, der bruges til at oprette funktionsobjekter, også kendt som funktorer, skal defineres som en member-funktion, så den har altid det implicitte this
-argument for medlemsfunktioner. Udover dette kan den overbelastes til at tage et vilkårligt antal yderligere argumenter, herunder nul.
Her er et eksempel på syntaksen:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Anvendelse:
foo f;
int a = f("hello");
I hele C++-standardbiblioteket kopieres funktionsobjekter altid. Dine egne funktionsobjekter skal derfor være billige at kopiere. Hvis et funktionsobjekt absolut skal bruge data, som er dyre at kopiere, er det bedre at gemme disse data et andet sted og lade funktionsobjektet henvise til dem.
De binære infix-sammenligningsoperatorer bør ifølge tommelfingerreglerne implementeres som ikke-medlemsfunktioner1. Den unære præfiks-negation !
bør (i henhold til de samme regler) implementeres som en medlemsfunktion. (men det er normalt ikke en god idé at overloade den).
Standardbibliotekets algoritmer (f.eks. std::sort()
) og typer (f.eks. std::map
) vil altid kun forvente at operator<
er til stede. Men brugerne af din type vil også forvente, at alle de andre operatører er til stede, så hvis du definerer operator<
, skal du sørge for at følge den tredje grundlæggende regel for operatoroverloadning og også definere alle de andre boolske sammenligningsoperatører. Den kanoniske måde at 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 vigtige at bemærke her er, at kun to af disse operatører rent faktisk gør noget, de andre videresender blot deres argumenter til en af disse to for at udføre det egentlige arbejde.
Syntaksen for overloading af de resterende binære boolske operatorer (|||
, &&
) følger reglerne for sammenligningsoperatorer. Det er dog meget usandsynligt, at du vil finde et fornuftigt anvendelsestilfælde for disse2.
1 Som med alle tommelfingerregler kan der nogle gange også være grunde til at bryde denne regel. Hvis det er tilfældet, skal du ikke glemme, at den venstre operand i de binære sammenligningsoperatorer, som for medlemsfunktioner vil være *this
, også skal være const
. Så en sammenligningsoperator implementeret som en medlemsfunktion ville skulle have denne signatur:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Bemærk const
til sidst.)
2 Det skal bemærkes, at den indbyggede version af |||
og &&
bruger genvejssemantik. Mens de brugerdefinerede (fordi de er syntaktisk sukker for metodekald) ikke bruger genvejssemantik. Brugeren vil forvente, at disse operatorer har genvejssemantik, og deres kode kan afhænge af det, derfor anbefales det stærkt, at de ALDRIG defineres.
De unære inkrement- og decrement-operatører findes både i præfiks og postfiks-stil. For at skelne den ene fra den anden, tager postfix-varianterne et ekstra dummy int-argument. Hvis du overloader increment eller decrement, skal du altid implementere både præfiks og postfiks versioner. Her er den kanoniske implementering af increment, decrement følger de samme regler:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Bemærk, at postfix-varianten er implementeret i form af præfiks. Bemærk også, at postfix laver en ekstra kopi. 2
Overbelastning af unary minus og plus er ikke særlig almindeligt og bør nok bedst undgås. Hvis det er nødvendigt, bør de sandsynligvis overbelastes som medlemsfunktioner.
2 Bemærk også, at postfix-varianten gør mere arbejde og derfor er mindre effektiv at bruge end præfiksvarianten. Dette er en god grund til generelt at foretrække præfiksinkrement frem for postfiksinkrement. Mens compilere normalt kan optimere det ekstra arbejde, der er forbundet med postfixinkrement, væk for indbyggede typer, kan de måske ikke gøre det samme for brugerdefinerede typer (som kan være noget så uskyldigt udseende som en liste-iterator). Når man først har vænnet sig til at gøre i++
, bliver det meget svært at huske at gøre ++i
i stedet, når i
ikke er af en indbygget type (plus at man skal ændre kode, når man ændrer en type), så det er bedre at gøre det til en vane altid at bruge præfiksinkrement, medmindre postfiks udtrykkeligt er nødvendigt.
For de binære aritmetiske operatorer må man ikke glemme at overholde den tredje grundregel operatoroverload: Hvis du tilbyder +
, skal du også tilbyde +=
, hvis du tilbyder -
, må du ikke udelade -=
osv. Andrew Koenig siges at have været den første til at bemærke, at de sammensatte tildelingsoperatorer kan bruges som grundlag for deres ikke-sammensatte modstykker. Det vil sige, at operatoren +
er implementeret i form af +=
, -
er implementeret i form af -=
osv.
Ifølge vores tommelfingerregler bør +
og dets ledsagere være ikke-medlemmer, mens deres sammensatte tildelingsmodstykker (+=
osv.), der ændrer deres venstre argument, bør være et medlem. Her er den eksemplariske kode for +=
og +
; de andre binære aritmetiske operatorer bør implementeres på samme måde:
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 sit resultat pr. reference, mens operator+
returnerer en kopi af sit resultat. Selvfølgelig er det normalt mere effektivt at returnere en reference end at returnere en kopi, men i tilfældet operator+
er der ingen vej uden om kopieringen. Når du skriver a + b
, forventer du, at resultatet er en ny værdi, og derfor skal operator+
returnere en ny værdi.3
Bemærk også, at operator+
tager sin venstre operand ved copy i stedet for ved const reference. Grunden til dette er den samme som den, der er givet for at operator=
tager sit argument pr. kopi.
Bitmanipulationsoperatorerne ~
&
`|
^
<<
`>>
bør implementeres på samme måde som de aritmetiske operatorer. Der er imidlertid (bortset fra overbelastning af <<
og >>
for output og input) meget få rimelige anvendelsesmuligheder for overbelastning af disse.
3 Igen er læren af dette, at a += b
generelt er mere effektiv end a + b
og bør foretrækkes, hvis det er muligt.
Array subscript-operatoren er en binær operator, som skal implementeres som et klassemedlem. Den anvendes til containerlignende typer, der giver adgang til deres dataelementer ved hjælp af en nøgle. Den kanoniske form for at levere disse er denne:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Medmindre du ikke ønsker, at brugere af din klasse skal kunne ændre dataelementer, der returneres af operator[]
(i så fald kan du udelade non-const-varianten), bør du altid levere begge varianter af operatoren.
Hvis det er kendt, at value_type henviser til en indbygget type, bør const-varianten af operatoren hellere returnere en kopi i stedet for en const-reference:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
For at definere dine egne iteratorer eller smarte pointere skal du overloade den unære præfiks dereference-operator *
og den binære infix pointer-medlemsadgangsoperator ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
-operatoren gælder det, at hvis value_type
er af typen class
(eller struct
eller union
), kaldes en anden operator->()
rekursivt, indtil en operator->()
returnerer en værdi af en anden type end en klasse.
Den unære address-of-operator bør aldrig overbelastes.
For operator->*()
se [dette spørgsmål] (https://stackoverflow.com/q/8777845/140719). Den bruges sjældent og er derfor sjældent overbelastet. Faktisk overbelastes den ikke engang af iteratorer.Fortsæt til Konverteringsoperatorer
Når det drejer sig om operatoroverloadning i C++, er der tre grundlæggende regler, du bør følge. Som med alle sådanne regler er der dog undtagelser. Nogle gange har folk afveget fra dem, og resultatet var ikke dårlig kode, men sådanne positive afvigelser er få og sjældne. I det mindste var 99 ud af 100 afvigelser, som jeg har set, ubegrundede. Det kunne dog lige så godt have været 999 ud af 1000. Så du må hellere holde dig til følgende regler.
Når betydningen af en operatør ikke er åbenlyst klar og uomtvistelig, bør den ikke overbelastes. _ Sørg i stedet for en funktion med et velvalgt navn. Grundlæggende siger den første og vigtigste regel for overloading af operatører i sin kerne: Don't do it. Det kan virke mærkeligt, for der er meget at vide om operatoroverloading, og derfor er der en masse artikler, bogkapitler og andre tekster, der beskæftiger sig med alt dette. Men på trods af denne tilsyneladende indlysende evidens er der kun overraskende få tilfælde hvor operatoroverloading er hensigtsmæssigt. Årsagen er, at det faktisk er svært at forstå semantikken bag anvendelsen af en operator, medmindre brugen af operatoren i anvendelsesområdet er velkendt og ubestridt. I modsætning til hvad mange tror, er dette næsten aldrig tilfældet.
Hold dig altid til operatørens velkendte semantik.
C++ stiller ingen begrænsninger på semantikken for overloadede operatorer. Din compiler vil med glæde acceptere kode, der implementerer den binære +
-operator til at subtrahere fra sin højre operand. Brugerne af en sådan operatør ville dog aldrig mistænke udtrykket a + b
for at subtrahere a
fra b
. Dette forudsætter naturligvis, at operatørens semantik i anvendelsesområdet er uomtvistet.
Leverer altid alle ud af et sæt relaterede operationer.
Operatorer er relateret til hinanden og til andre operationer. Hvis din type understøtter a + b
, vil brugerne forvente også at kunne kalde a += b
. Hvis den understøtter præfiksinkrement ++a
, vil de også forvente, at a++
også virker. Hvis de kan kontrollere, om a < b
, vil de helt sikkert forvente, at de også kan kontrollere, om a > b
. Hvis de kan copy-construere din type, forventer de også, at assignment også virker.
Fortsæt til [Afgørelsen mellem medlem og ikke-medlem] (https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421729#4421729).
Du kan ikke ændre betydningen af operatorer for indbyggede typer i C++, operatorer kan kun overbelastes for brugerdefinerede typer1. Det vil sige, at mindst en af operanderne skal være af en brugerdefineret type. Som med andre overloadede funktioner kan operatorer kun overloades for et bestemt sæt parametre én gang.
Ikke alle operatorer kan overbelastes i C++. Blandt de operatorer, der ikke kan overbelastes, er bl.a: .
`::
sizeof
typeid
.*
og den eneste ternære operatør i C++, ?:
.
Blandt de operatorer, der kan overbelastes i C++, er disse:
+``
-`*
/
%
og +=
-=
*=
/=
%=
(alle binære infix); +``
-(unært præfiks);
++`--
(unært præfiks og postfiks)&
|
^
<<
>>
og &=
|=
^=
<<=
>>=
>>=
(alle binære infix); ~
(unært præfiks)==
!=
<
<
>
<=
>=
|||
&&
(alle binære infix); !
(unært præfiks)new
new[]
delete
delete[]
=``
[]`->
->*
,
(alle binære infix); *
&
(alle unære præfikser) ()
(funktionsopkald, n-ary infix)At du kan overloade alle disse betyder dog ikke, at du skal gøre det. Se de grundlæggende regler for overbelastning af operatører.
I C++ overbelastes operatorer i form af funktioner med særlige navne. Som med andre funktioner kan overloadede operatører generelt implementeres enten som en medlemsfunktion af deres venstre operand's type eller som ikke-medlemsfunktioner. Om man er fri til at vælge eller forpligtet til at bruge en af dem afhænger af flere kriterier.2 En unær operatør @
3, der anvendes på et objekt x, påkaldes enten som operator@@(x)
eller som x.operator@()
. En binær infix-operator @
, der anvendes på objekterne x
og y
, kaldes enten som operator@(x,y)
eller som x.operator@(y)
.4
Operatorer, der er implementeret som ikke-medlemsfunktioner, er undertiden venner af deres operandens type.
1 Udtrykket "brugerdefineret" kan være en smule misvisende. C++ skelner mellem indbyggede typer og brugerdefinerede typer. Til førstnævnte hører f.eks. int, char og double; til sidstnævnte hører alle struct-, class-, union- og enum-typer, herunder dem fra standardbiblioteket, selv om de ikke som sådan er defineret af brugerne.
2 Dette er behandlet i en senere del af denne FAQ.
3 @
er ikke en gyldig operatør i C++, hvorfor jeg bruger den som en stedholder.
4 Den eneste ternære operatør i C++ kan ikke overbelastes, og den eneste n-ære operatør skal altid implementeres som en medlemsfunktion.
Fortsæt til [De tre grundlæggende regler for overbelastning af operatører i C++] (https://stackoverflow.com/questions/4421706/operator-overloading-in-c/4421708#4421708).