Opmerking: De antwoorden werden gegeven in een specifieke volgorde, maar aangezien veel gebruikers de antwoorden sorteren volgens de stemmen, eerder dan volgens het tijdstip waarop ze werden gegeven, is hier's een index van de antwoorden in de volgorde waarin ze het meest zinvol zijn:
_(Opmerking: Dit is bedoeld als een toevoeging aan Stack Overflow's C++ FAQ. Als je kritiek wilt leveren op het idee om een FAQ in deze vorm aan te bieden, dan zou de posting op meta waarmee dit alles begon de plaats zijn om dat te doen. Antwoorden op die vraag worden in de gaten gehouden in de C++ chatroom, waar het FAQ idee in de eerste plaats is ontstaan, dus het is zeer waarschijnlijk dat je antwoord gelezen wordt door degenen die met het idee kwamen.
Het meeste werk in het overloaden van operatoren is boiler-plate code. Dat is geen wonder, want operatoren zijn slechts syntactische suiker, hun eigenlijke werk kan gedaan worden door (en wordt vaak doorgestuurd naar) gewone functies. Maar het is belangrijk dat je deze boiler-plate code goed krijgt. Als je dat niet doet, zal ofwel de code van je operator niet compileren, ofwel de code van je gebruikers niet compileren, ofwel zal de code van je gebruikers zich verrassend gedragen.
Er is veel te zeggen over toewijzing. Het meeste is echter al gezegd in GMan's beroemde Copy-And-Swap FAQ, dus ik'zal het meeste hier overslaan, alleen de perfecte assignment operator ter referentie opsommen:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
De bitshift operatoren <<
en >>
, hoewel nog steeds gebruikt in hardware interfacing voor de bit-manipulatie functies die ze erven van C, zijn meer gangbaar geworden als overloaded stream input en output operatoren in de meeste toepassingen. Voor richtlijnen voor het overladen als bit-manipulatie operatoren, zie de paragraaf hieronder over Binaire rekenkundige operatoren. Voor het implementeren van uw eigen aangepaste formaat en parsing logica wanneer uw object wordt gebruikt met iostreams, ga verder.
De stream operatoren, een van de meest overbelaste operatoren, zijn binaire infix operatoren waarvoor de syntaxis geen beperking oplegt over of ze members of non-members moeten zijn.
Aangezien ze hun linker argument veranderen (ze veranderen de toestand van de stream), zouden ze, volgens de vuistregels, geïmplementeerd moeten worden als leden van het type van hun linker operand. Hun linker operanden zijn echter streams uit de standaardbibliotheek, en hoewel de meeste van de door de standaardbibliotheek gedefinieerde uitvoer- en invoeroperatoren voor streams inderdaad gedefinieerd zijn als leden van de streamklassen, kun je, wanneer je uitvoer- en invoeroperaties voor je eigen types implementeert, de streamtypes van de standaardbibliotheek niet wijzigen. Daarom moet je deze operatoren voor je eigen types implementeren als non-member functies.
De canonieke vormen van de twee zijn deze:
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;
}
Bij het implementeren van operator>>
is het handmatig instellen van de toestand van de stream alleen nodig als het lezen zelf is gelukt, maar het resultaat niet is wat zou worden verwacht.
De functie-aanroep operator, gebruikt om functie-objecten te maken, ook bekend als functors, moet worden gedefinieerd als een member functie, dus het heeft altijd het impliciete this
argument van member functies. Afgezien hiervan kan de functie worden overladen om een willekeurig aantal extra argumenten te nemen, inclusief nul.
Hier's een voorbeeld van de syntaxis:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Gebruik:
foo f;
int a = f("hello");
In de hele C++ standaard bibliotheek worden functie-objecten altijd gekopieerd. Je eigen functie-objecten moeten daarom goedkoop te kopiëren zijn. Als een functie-object absoluut gegevens moet gebruiken die duur zijn om te kopiëren, is het beter om die gegevens elders op te slaan en het functie-object ernaar te laten verwijzen.
De binaire infix-vergelijkingsoperatoren moeten, volgens de vuistregels, geïmplementeerd worden als niet-lidfuncties1. De unary prefix negatie !
moet (volgens dezelfde regels) geïmplementeerd worden als een member functie. (maar het is meestal geen goed idee om het te overloaden).
De standaard bibliotheek algoritmes (b.v. std::sort()
) en types (b.v. std::map
) zullen altijd verwachten dat operator<
alleen aanwezig is. Echter, de gebruikers van je type zullen verwachten dat alle andere operatoren ook aanwezig zijn, dus als je operator<
definieert, zorg er dan voor dat je de derde fundamentele regel van operator overloading volgt en ook alle andere booleaanse vergelijkingsoperatoren definieert. De canonieke manier om ze te implementeren is als volgt:
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);}
Het is belangrijk om op te merken dat slechts twee van deze operatoren daadwerkelijk iets doen, de anderen sturen hun argumenten gewoon door naar een van deze twee om het eigenlijke werk te doen.
De syntaxis voor het overloaden van de overige binaire booleaanse operatoren (||
, &&
) volgt de regels van de vergelijkingsoperatoren. Het is echter zeer onwaarschijnlijk dat je een redelijk use case zou vinden voor deze2.
1 Zoals met alle vuistregels, kunnen er soms redenen zijn om ook deze te breken. Als dat het geval is, vergeet dan niet dat de linker operand van de binaire vergelijkingsoperatoren, die voor member functies *this
zal zijn, ook const
moet zijn. Dus een vergelijkingsoperator die als memberfunctie is geïmplementeerd zou deze signatuur moeten hebben:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Let op de const
aan het eind.)
2 Opgemerkt moet worden dat de ingebouwde versie van ||
en &&
shortcut semantics gebruiken. Terwijl de door de gebruiker gedefinieerde (omdat ze syntactische suiker zijn voor methode-aanroepen) geen shortcut semantiek gebruiken. Gebruikers zullen verwachten dat deze operatoren een verkorte semantiek hebben, en hun code kan er van afhangen, daarom wordt het sterk aangeraden om ze NOOIT te definiëren.
De unary increment en decrement operatoren zijn er zowel in prefix als postfix smaak. Om het ene van het andere te onderscheiden, nemen de postfix varianten een extra dummy int argument. Als je increment of decrement overload, zorg er dan voor dat je altijd zowel de prefix als de postfix versie implementeert. Hier is de canonieke implementatie van increment, decrement volgt dezelfde regels:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Merk op dat de postfix variant geïmplementeerd is in termen van prefix. Merk ook op dat postfix een extra kopie doet.2
Het overladen van unary min en plus is niet erg gebruikelijk en wordt waarschijnlijk best vermeden. Indien nodig, moeten ze waarschijnlijk overloaded worden als lid functies.
2 Merk ook op dat de postfix variant meer werk doet en daarom minder efficiënt is om te gebruiken dan de prefix variant. Dit is een goede reden om in het algemeen prefix increment te verkiezen boven postfix increment. Terwijl compilers meestal het extra werk van postfix increment voor ingebouwde types kunnen wegoptimaliseren, kunnen ze dat misschien niet voor gebruikersgedefinieerde types (wat zoiets onschuldigs kan zijn als een lijst iterator). Als je eenmaal gewend bent om i++
te doen, wordt het erg moeilijk om te onthouden om in plaats daarvan ++i
te doen als i
niet van een ingebouwd type is (plus je'zou code moeten veranderen bij het veranderen van een type), dus is het beter om er een gewoonte van te maken om altijd prefix increment te gebruiken, tenzij postfix expliciet nodig is.
Vergeet bij de binaire rekenkundige operatoren niet te voldoen aan de derde basisregel operator overloading: Als je +
geeft, geef dan ook +=
, als je -
geeft, laat dan -=
niet weg, enz. Van Andrew Koenig wordt gezegd dat hij de eerste was die opmerkte dat de samengestelde toewijzingsoperatoren kunnen worden gebruikt als basis voor hun niet-samengestelde tegenhangers. Dat wil zeggen, operator +
is geïmplementeerd in termen van +=
, -
is geïmplementeerd in termen van -=
enz.
Volgens onze vuistregels zouden +
en zijn metgezellen niet-leden moeten zijn, terwijl hun samengestelde tegenhangers (+=
enz.), die hun linker argument veranderen, een lid zouden moeten zijn. Hier is de voorbeeldcode voor +=
en +
; de andere binaire rekenkundige operatoren moeten op dezelfde manier worden geïmplementeerd:
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+=
geeft zijn resultaat per referentie terug, terwijl operator+
een kopie van zijn resultaat teruggeeft. Natuurlijk is het teruggeven van een referentie meestal efficiënter dan het teruggeven van een kopie, maar in het geval van operator+
, is er geen manier om het kopiëren te omzeilen. Als je a + b
schrijft, verwacht je dat het resultaat een nieuwe waarde is, en daarom moet operator+
een nieuwe waarde teruggeven.3
Merk ook op dat operator+
zijn linker operand per copy neemt in plaats van per const reference. De reden hiervoor is dezelfde als die voor operator=
die zijn argument per kopie neemt.
De bitmanipulatie operatoren ~
&
|
^
<<
>>
moeten op dezelfde manier geïmplementeerd worden als de rekenkundige operatoren. Echter, (behalve voor het overloaden van <<
en >>
voor output en input) zijn er maar weinig redelijke use-cases voor het overloaden van deze.
3 Nogmaals, de les die je hieruit kunt trekken is dat a += b
in het algemeen efficiënter is dan a + b
en de voorkeur verdient indien mogelijk.
De array subscript operator is een binaire operator die geïmplementeerd moet worden als een klasse lid. Hij wordt gebruikt voor container-achtige types die toegang tot hun data-elementen toelaten via een sleutel. De canonieke vorm om deze te verstrekken is deze:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Tenzij u niet wilt dat gebruikers van uw klasse gegevenselementen kunnen wijzigen die door operator[]
worden geretourneerd (in dat geval kunt u de non-const variant weglaten), moet u altijd beide varianten van de operator opgeven.
Als van value_type bekend is dat het verwijst naar een ingebouwd type, kan de const variant van de operator beter een kopie teruggeven in plaats van een const verwijzing:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Om je eigen iterators of slimme pointers te definiëren, moet je de unary prefix dereference operator *
en de binary infix pointer member access operator ->
overloaden:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
operator geldt dat als value_type
van class
(of struct
of union
) type is, een andere operator->()
recursief wordt aangeroepen, totdat een operator->()
een waarde van niet-class type teruggeeft.
De unary address-of operator mag nooit overloaded worden.
Voor operator->*()
zie deze vraag. Het wordt zelden gebruikt en dus ook zelden overloaded. Sterker nog, zelfs iterators overloaden het niet.Ga verder naar Conversie Operatoren
Als het gaat om operator overloading in C++, zijn er drie basisregels die u moet volgen. Zoals met al zulke regels, zijn er inderdaad uitzonderingen. Soms zijn er mensen die er van afgeweken zijn en het resultaat was geen slechte code, maar zulke positieve afwijkingen zijn er maar weinig. Op zijn minst waren 99 van de 100 van dergelijke afwijkingen die ik heb gezien ongerechtvaardigd. Het hadden er echter net zo goed 999 van de 1000 kunnen zijn. Je kunt je dus maar beter aan de volgende regels houden.
Wanneer de betekenis van een operator niet duidelijk en onomstreden is, moet hij niet worden overloaded. In plaats daarvan een functie met een goed gekozen naam. In wezen zegt de eerste en belangrijkste regel voor het overloaden van operatoren: Doe het niet. Dat lijkt misschien vreemd, want er is veel te weten over operator overloading en er zijn dan ook veel artikelen, boekhoofdstukken en andere teksten die dit allemaal behandelen. Maar ondanks dit schijnbaar vanzelfsprekende bewijs, zijn er maar verrassend weinig gevallen waarin operator overloading gepast is. De reden hiervoor is dat het eigenlijk moeilijk is om de semantiek achter de toepassing van een operator te begrijpen, tenzij het gebruik van de operator in het toepassingsdomein goed gekend en onbetwist is. In tegenstelling tot wat vaak wordt gedacht, is dit bijna nooit het geval.
Houd u altijd aan de bekende semantiek van de operator.
C++ stelt geen beperkingen aan de semantiek van overloaded operatoren. Uw compiler zal zonder problemen code accepteren die de binaire +
operator implementeert om van zijn rechter operand af te trekken. De gebruikers van zo'n operator zullen echter nooit vermoeden dat de uitdrukking a + b
betekent a
aftrekken van b
. Natuurlijk, dit veronderstelt dat de semantiek van de operator in het toepassingsdomein onbetwist is.
Zie altijd alles uit een verzameling verwante operatoren.
Operators zijn gerelateerd aan elkaar en aan andere operaties. Als uw type a + b
ondersteunt, zullen gebruikers verwachten dat ze ook a += b
kunnen oproepen. Als het prefix increment ++a
ondersteunt, zullen ze verwachten dat a++
ook werkt. Als ze kunnen controleren of a < b
, zullen ze zeker verwachten dat ze ook kunnen controleren of a > b
. Als ze je type kunnen kopiëren, verwachten ze dat toewijzing ook werkt.
Ga verder naar De beslissing tussen lid en niet-lid.
Je kunt de betekenis van operatoren voor ingebouwde typen in C++ niet veranderen, operatoren kunnen alleen overloaded worden voor gebruikersgedefinieerde typen1. Dat wil zeggen, tenminste één van de operanden moet van een door de gebruiker gedefinieerd type zijn. Net als bij andere overloaded functies, kunnen operatoren maar één keer overloaded worden voor een bepaalde set parameters.
Niet alle operatoren kunnen worden overloaded in C++. Onder de operatoren die niet overloaded kunnen worden zijn: .
::
sizeof
typeid
.*
en de enige ternaire operator in C++, ?:
Tot de operatoren die in C++ overbelast kunnen worden, behoren de volgende:
+
-
*
/
%
en +=
-=
*=
/=
%=
(alle binaire infix); +
-
(unary prefix); ++
--
(unary prefix en postfix)&
|
^
<<
>>
en &=
|=
^=
<<=
>>=
(alle binaire infixen); ~
(unair voorvoegsel)==
!=
<
>=
<=
||
&&
(alle binaire infix); !
(unair voorvoegsel)new
new[]
delete`delete[]
=
[]
->`->*
,
(alle binaire tussenvoegsels); *
&
(alle unaire tussenvoegsels) ()
(functie-oproep, n-ary tussenvoegsels)Het feit dat je al deze operatoren kan overloaden betekent echter niet dat je moet doen. Zie de basisregels van operator overloading.
In C++ worden operatoren overloaded in de vorm van functies met speciale namen. Net als bij andere functies kunnen overloaded operatoren in het algemeen worden geïmplementeerd als een lidfunctie van het type van hun linker operand's of als niet-lidfuncties. Of je vrij bent om te kiezen of verplicht bent om een van beide te gebruiken hangt af van verschillende criteria.2 Een unary operator @
3, toegepast op een object x, wordt aangeroepen ofwel als operator@(x)
of als x.operator@()
. Een binaire infix operator @
, toegepast op de objecten x
en y
, wordt aangeroepen ofwel als operator@(x,y)
ofwel als x.operator@(y)
.4
Operatoren die zijn geïmplementeerd als niet-lid functies zijn soms vriend van het type van hun operand.
1 De term "user-defined" is misschien een beetje misleidend. C++ maakt onderscheid tussen ingebouwde typen en door de gebruiker gedefinieerde typen. Tot de eerste behoren bijvoorbeeld int, char, en double; tot de tweede behoren alle struct, class, union, en enum types, inclusief die uit de standaard bibliotheek, ook al zijn ze als zodanig niet door gebruikers gedefinieerd.
2 Dit wordt behandeld in een later deel van deze FAQ.
3 De @
is geen geldige operator in C++ en daarom gebruik ik hem als placeholder.
4 De enige ternaire operator in C++ kan niet overloaded worden en de enige n-naire operator moet altijd als memberfunctie geimplementeerd worden.
Ga verder naar De drie basisregels van operator-overloading in C++.