Nota: Le risposte sono state date in un ordine specifico, ma poiché molti utenti ordinano le risposte in base ai voti, piuttosto che al tempo in cui sono state date, ecco un indice delle risposte nell'ordine in cui hanno più senso:
sub> (Nota: Questa vuole essere una voce per Stack Overflow's C++ FAQ. Se vuoi criticare l'idea di fornire una FAQ in questa forma, allora il post su meta che ha iniziato tutto questo sarebbe il posto per farlo. Le risposte a quella domanda sono monitorate nella chatroom C++, dove l'idea delle FAQ è partita in primo luogo, quindi è molto probabile che la tua risposta venga letta da coloro che hanno avuto l'idea.) {\a6}(*)
La maggior parte del lavoro nel sovraccarico degli operatori è codice boiler-plate. Non c'è da meravigliarsi, dato che gli operatori sono solo zucchero sintattico, il loro lavoro effettivo potrebbe essere fatto da (e spesso viene inoltrato a) semplici funzioni. Ma è importante che il codice boiler-plate sia corretto. Se fallite, o il codice del vostro operatore non verrà compilato o il codice dei vostri utenti non verrà compilato o il codice dei vostri utenti si comporterà in modo sorprendente.
C'è molto da dire sull'assegnazione. Tuttavia, la maggior parte è già stata detta in GMan's famoso Copy-And-Swap FAQ, quindi ne salterò la maggior parte qui, elencando solo il perfetto operatore di assegnazione per riferimento:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Gli operatori bitshift <<
e >>
, sebbene siano ancora usati nell'interfacciamento hardware per le funzioni di manipolazione dei bit che ereditano dal C, sono diventati più prevalenti come operatori di input e output di stream sovraccaricati nella maggior parte delle applicazioni. Per l'overloading della guida come operatori di manipolazione dei bit, vedi la sezione sottostante sugli operatori aritmetici binari. Per implementare il proprio formato personalizzato e la logica di parsing quando l'oggetto viene usato con iostreams, continua.
Gli operatori di flusso, tra gli operatori più comunemente sovraccaricati, sono operatori binari infix per i quali la sintassi non specifica alcuna restrizione sul fatto che debbano essere membri o non membri.
Poiché cambiano il loro argomento di sinistra (alterano lo stato del flusso), dovrebbero, secondo le regole empiriche, essere implementati come membri del tipo del loro operando di sinistra. Tuttavia, i loro operandi di sinistra sono flussi della libreria standard, e mentre la maggior parte degli operatori di output e input di flusso definiti dalla libreria standard sono effettivamente definiti come membri delle classi di flusso, quando si implementano operazioni di output e input per i propri tipi, non si possono cambiare i tipi di flusso della libreria standard. Ecco perché dovete implementare questi operatori per i vostri tipi come funzioni non membri.
Le forme canoniche dei due sono queste:
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;
}
Quando si implementa un operatore>>
, impostare manualmente lo stato del flusso è necessario solo quando la lettura stessa ha avuto successo, ma il risultato non è quello che ci si aspettava.
L'operatore di chiamata di funzione, usato per creare oggetti funzione, noti anche come funtori, deve essere definito come una funzione member, quindi ha sempre l'argomento implicito this
delle funzioni membro. A parte questo, può essere sovraccaricata per prendere qualsiasi numero di argomenti aggiuntivi, compreso lo zero.
Ecco un esempio della sintassi:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Uso:
foo f;
int a = f("hello");
In tutta la libreria standard C++, gli oggetti funzione sono sempre copiati. I vostri oggetti funzione dovrebbero quindi essere economici da copiare. Se un oggetto funzione ha assolutamente bisogno di usare dati che sono costosi da copiare, è meglio memorizzare quei dati altrove e fare in modo che l'oggetto funzione vi faccia riferimento.
Gli operatori di confronto infix binari dovrebbero, secondo le regole empiriche, essere implementati come funzioni non membri1. La negazione del prefisso unario !
dovrebbe (secondo le stesse regole) essere implementata come funzione membro. (ma di solito non è una buona idea sovraccaricarla).
Gli algoritmi della libreria standard (ad esempio, std::sort()
) e i tipi (ad esempio, std::map
) si aspettano sempre che sia presente solo operator<
. Tuttavia, gli utenti del vostro tipo si aspettano che anche tutti gli altri operatori siano presenti, quindi se definite operator<
, assicuratevi di seguire la terza regola fondamentale dell'overloading degli operatori e di definire anche tutti gli altri operatori di confronto booleano. Il modo canonico di implementarli è questo:
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);}
La cosa importante da notare qui è che solo due di questi operatori fanno effettivamente qualcosa, gli altri stanno solo inoltrando i loro argomenti a uno di questi due per fare il lavoro effettivo.
La sintassi per sovraccaricare i rimanenti operatori booleani binari (||
, &&
) segue le regole degli operatori di confronto. Tuttavia, è molto improbabile che tu possa trovare un caso d'uso ragionevole per questi `sup>2</sup>. Come per tutte le regole empiriche, a volte ci potrebbero essere ragioni per infrangere anche questa. Se è così, non dimenticate che anche l'operando di sinistra degli operatori di confronto binario, che per le funzioni membro sarà
*this, deve essere
const`. Quindi un operatore di confronto implementato come funzione membro dovrebbe avere questa firma:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Notate il const
alla fine);
22 Va notato che la versione built-in di ||
e &&
usano la semantica di scorciatoia. Mentre quelli definiti dall'utente (perché sono zucchero sintattico per le chiamate ai metodi) non usano la semantica di scorciatoia. L'utente si aspetterà che questi operatori abbiano una semantica di scorciatoia, e il loro codice potrebbe dipendere da questo, perciò è altamente consigliato di non definirli MAI.
Gli operatori unari di incremento e decremento sono disponibili sia in versione prefisso che postfisso. Per distinguerli l'uno dall'altro, le varianti postfisso prendono un argomento int aggiuntivo fittizio. Se si sovraccarica l'incremento o il decremento, assicurarsi di implementare sempre entrambe le versioni prefisso e postfisso. Ecco l'implementazione canonica di increment, decrement segue le stesse regole:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Si noti che la variante postfix è implementata in termini di prefix. Notate anche che postfix fa una copia extra;
Sovraccaricare gli unari minus e plus non è molto comune e probabilmente è meglio evitare. Se necessario, dovrebbero probabilmente essere sovraccaricate come funzioni membro.
Notate anche che la variante postfix fa più lavoro ed è quindi meno efficiente da usare della variante prefix. Questa è una buona ragione per preferire generalmente l'incremento del prefisso all'incremento del postfisso. Mentre i compilatori possono di solito ottimizzare il lavoro aggiuntivo dell'incremento postfix per i tipi built-in, potrebbero non essere in grado di fare lo stesso per i tipi definiti dall'utente (che potrebbero essere qualcosa dall'aspetto innocente come un iteratore di lista). Una volta che ci si è abituati a fare i++
, diventa molto difficile ricordarsi di fare invece ++i
quando i
non è di un tipo built-in (in più si dovrebbe cambiare codice quando si cambia un tipo), quindi è meglio prendere l'abitudine di usare sempre l'incremento del prefisso, a meno che il postfix sia esplicitamente necessario.
Per gli operatori aritmetici binari, non dimenticate di obbedire alla terza regola base dell'overloading degli operatori: Se fornisci +
, fornisci anche +=
, se fornisci -
, non omettere -=
, ecc. Si dice che Andrew Koenig sia stato il primo ad osservare che gli operatori di assegnazione composti possono essere usati come base per le loro controparti non composte. Cioè, l'operatore +
è implementato in termini di +=
, -
è implementato in termini di -=
ecc.
Secondo le nostre regole empiriche, +
e i suoi compagni dovrebbero essere non-membri, mentre la loro controparte di assegnazione composta (+=
ecc.), cambiando il loro argomento di sinistra, dovrebbe essere un membro. Ecco il codice esemplare per +=
e +
; gli altri operatori aritmetici binari dovrebbero essere implementati nello stesso modo:
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;
}
operatore+=
restituisce il suo risultato per riferimento, mentre operatore+
restituisce una copia del suo risultato. Naturalmente, restituire un riferimento è di solito più efficiente che restituire una copia, ma nel caso di operatore+
, non c'è modo di aggirare la copia. Quando scrivete a + b
, vi aspettate che il risultato sia un nuovo valore, ecco perché operator+
deve restituire un nuovo valore;
Notate anche che operatore+
prende il suo operando sinistro per copia piuttosto che per riferimento const. La ragione di ciò è la stessa di quella che spiega perché operatore=
prende il suo argomento per copia.
Gli operatori di manipolazione dei bit ~
&
|
^
<<
>>
dovrebbero essere implementati nello stesso modo degli operatori aritmetici. Tuttavia, (eccetto per l'overloading di <<
e >>
per output e input) ci sono pochissimi casi d'uso ragionevoli per l'overloading di questi.
33 Di nuovo, la lezione da trarre da questo è che a += b
è, in generale, più efficiente di a + b
e dovrebbe essere preferito se possibile.
L'operatore di pedice dell'array è un operatore binario che deve essere implementato come membro della classe. È usato per tipi simili a contenitori che permettono l'accesso ai loro elementi di dati tramite una chiave. La forma canonica per fornirli è questa:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
A meno che non vogliate che gli utenti della vostra classe siano in grado di cambiare gli elementi di dati restituiti da operatore[]
(nel qual caso potete omettere la variante non-const), dovreste sempre fornire entrambe le varianti dell'operatore.
Se value_type è noto per riferirsi ad un tipo built-in, la variante const dell'operatore dovrebbe meglio restituire una copia invece di un riferimento const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Per definire i vostri iteratori o puntatori intelligenti, dovete sovraccaricare l'operatore di dereferenza con prefisso unario *
e l'operatore di accesso ai membri del puntatore binario infix ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
->
, se value_type
è di tipo class
(o struct
o union
), un altro operatore->()
viene chiamato ricorsivamente, finché un operatore->()
restituisce un valore di tipo non-class.
L'operatore unario address-of non dovrebbe mai essere sovraccaricato.
Per operatore->*()
vedi questa domanda. Viene usato raramente e quindi raramente viene sovraccaricato. Infatti, anche gli iteratori non lo sovraccaricano.Continua a Operatori di conversione
Quando si tratta di sovraccarico dell'operatore in C++, ci sono tre regole di base da seguire. Come per tutte queste regole, ci sono effettivamente delle eccezioni. A volte le persone hanno deviato da esse e il risultato non è stato un cattivo codice, ma tali deviazioni positive sono poche e lontane tra loro. Come minimo, 99 deviazioni su 100 che ho visto erano ingiustificate. Tuttavia, potrebbero anche essere state 999 su 1000. Quindi è meglio attenersi alle seguenti regole.
Quando il significato di un operatore non è ovviamente chiaro e indiscusso, non dovrebbe essere sovraccaricato. Invece, fornite una funzione con un nome ben scelto. Fondamentalmente, la prima e più importante regola per l'overloading degli operatori, alla base, dice: Non fatelo. Questo potrebbe sembrare strano, perché c'è molto da sapere sull'overloading degli operatori e quindi un sacco di articoli, capitoli di libri e altri testi trattano tutto questo. Ma nonostante questa evidenza apparentemente ovvia, ci sono solo sorprendentemente pochi casi in cui il sovraccarico dell'operatore è appropriato. La ragione è che in realtà è difficile capire la semantica dietro l'applicazione di un operatore a meno che l'uso dell'operatore nel dominio dell'applicazione sia ben noto e indiscusso. Contrariamente alla credenza popolare, questo non è quasi mai il caso.
Sempre attenersi alla semantica nota dell'operatore.
Il C++ non pone limitazioni sulla semantica degli operatori sovraccaricati. Il vostro compilatore accetterà volentieri il codice che implementa l'operatore binario +
per sottrarre dal suo operando di destra. Tuttavia, gli utenti di un tale operatore non sospetterebbero mai l'espressione a + b
per sottrarre a
da b
. Naturalmente, questo suppone che la semantica dell'operatore nel dominio dell'applicazione sia indiscussa.
Fornire sempre tutto di un insieme di operazioni correlate.
Gli operatori sono correlati tra loro e con altre operazioni. Se il vostro tipo supporta a + b
, gli utenti si aspettano di poter chiamare anche a += b
. Se supporta l'incremento del prefisso ++a
, si aspetteranno che anche a++
funzioni. Se possono controllare se a < b
, si aspetteranno sicuramente di poter controllare anche se a > b
. Se possono copiare-costruire il tuo tipo, si aspettano che anche l'assegnazione funzioni.
Continua a La decisione tra membro e non membro.
Non potete cambiare il significato degli operatori per i tipi built-in in C++, gli operatori possono essere sovraccaricati solo per i tipi definiti dall'utente1. Cioè, almeno uno degli operandi deve essere di un tipo definito dall'utente. Come per altre funzioni sovraccaricate, gli operatori possono essere sovraccaricati per un certo insieme di parametri solo una volta.
Non tutti gli operatori possono essere sovraccaricati in C++. Tra gli operatori che non possono essere sovraccaricati ci sono: .
::
sizeof`typeid
.*
e l'unico operatore ternario in C++, ?:
Tra gli operatori che possono essere sovraccaricati in C++ ci sono questi:
+
-
*
/
%
e +=
-=
*=
/=
%=
(tutti infissi binari); +
-
(prefisso unario); ++
--
(prefisso e postfisso unario)&
|`^
<<
>>
e &=
|=
^=
<<=
>>=
(tutti infissi binari); ~
(prefisso unario)==
!=
<
>
<=
>=
||
&&
(tutti infissi binari); !
(prefisso unario)new
new[]
delete
delete[]
=
[]
->
->*
,
(tutti gli infissi binari); *
&
(tutti i prefissi unari) ()
(chiamata di funzione, infisso n-ario)Tuttavia, il fatto che si può sovraccaricare tutti questi operatori non significa che si deve farlo. Vedi le regole di base dell'overloading degli operatori.
In C++, gli operatori sono sovraccaricati sotto forma di funzioni con nomi speciali. Come per le altre funzioni, gli operatori sovraccaricati possono generalmente essere implementati o come una funzione membro del loro operando sinistro o come funzioni non membro. Se siete liberi di scegliere o vincolati ad usare l'uno o l'altro dipende da diversi criteri.2 Un operatore unario @
3, applicato ad un oggetto x, è invocato o come operatore@(x)
o come x.operator@()
. Un operatore infisso binario @
, applicato agli oggetti x
e y
, viene invocato o come operator@(x,y)
o come x.operator@(y)
;
Gli operatori che sono implementati come funzioni non membri sono talvolta amici del tipo del loro operando.
Il termine "definito dall'utente" potrebbe essere leggermente fuorviante. Il C++ fa la distinzione tra tipi built-in e tipi definiti dall'utente. Ai primi appartengono per esempio int, char e double; ai secondi appartengono tutti i tipi struct, class, union ed enum, compresi quelli della libreria standard, anche se non sono, in quanto tali, definiti dagli utenti;
22 Questo è trattato in una parte successiva di questa FAQ.
33 La @
non è un operatore valido in C++ ed è per questo che la uso come segnaposto.
44 L'unico operatore ternario in C++ non può essere sovraccaricato e l'unico operatore n-ario deve sempre essere implementato come funzione membro.
Continua a Le tre regole fondamentali del sovraccarico degli operatori in C++.