Operatori comuni da sovraccaricare
La maggior parte del lavoro negli operatori di sovraccarico è il codice della piastra della caldaia. Non c'è da meravigliarsi, dal momento che gli operatori sono semplicemente zucchero sintattico, il loro lavoro effettivo potrebbe essere svolto (e spesso viene trasmesso a) semplici funzioni. Ma è importante ottenere questo codice della piastra della caldaia giusto. Se fallisci, il codice del tuo operatore non verrà compilato o il codice dei tuoi utenti non verrà compilato o il codice dei tuoi utenti si comporterà in modo sorprendente.
Operatore di assegnazione
C'è molto da dire sull'incarico. Tuttavia, la maggior parte di ciò è già stato detto nelle famose FAQ su copia e scambio di GMan , quindi salterò la maggior parte qui, elencando solo l'operatore di assegnazione perfetto come riferimento:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Operatori Bitshift (usati per Stream I / O)
Gli operatori bitshift <<
e >>
, sebbene siano ancora utilizzati nell'interfaccia hardware per le funzioni di manipolazione dei bit che ereditano da C, sono diventati più diffusi come operatori di input e output di stream sovraccarichi nella maggior parte delle applicazioni. Per sovraccarico della guida come operatori di manipolazione dei bit, vedere la sezione seguente su Operatori aritmetici binari. Per implementare il proprio formato personalizzato e la logica di analisi quando l'oggetto viene utilizzato con iostreams, continuare.
Gli operatori di flusso, tra gli operatori di sovraccarico più comunemente, sono operatori di infissione binaria 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 (modificano lo stato del flusso), dovrebbero, secondo le regole empiriche, essere implementati come membri del tipo di operando di sinistra. Tuttavia, i loro operandi di sinistra sono flussi dalla 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 stream, quando si implementano operazioni di output e input per i propri tipi, impossibile modificare i tipi di flusso della libreria standard. Ecco perché è necessario implementare questi operatori per i propri tipi come funzioni non membro. Le forme canoniche delle 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;
}
Durante l'implementazione operator>>
, l'impostazione manuale dello stato del flusso è necessaria solo quando la lettura stessa ha avuto esito positivo, ma il risultato non è quello che ci si aspetterebbe.
Operatore di chiamata di funzione
L'operatore di chiamata funzione, utilizzato per creare oggetti funzione, noto anche come funzioni, deve essere definito come una funzione membro , quindi ha sempre l' this
argomento implicito delle funzioni membro. Oltre a questo, può essere sovraccarico per accettare qualsiasi numero di argomenti aggiuntivi, incluso 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 vengono sempre copiati. I tuoi oggetti funzione dovrebbero quindi essere economici da copiare. Se un oggetto funzione deve assolutamente utilizzare dati che sono costosi da copiare, è meglio archiviarli altrove e fare in modo che l'oggetto funzione si riferisca ad essi.
Operatori di confronto
Gli operatori di confronto binario di infissi dovrebbero, secondo le regole empiriche, essere implementati come funzioni non membro 1 . La negazione del prefisso unario !
dovrebbe (secondo le stesse regole) essere implementata come funzione membro. (ma di solito non è una buona idea sovraccaricarlo.)
Gli algoritmi della libreria standard (ad es. std::sort()
) E i tipi (ad es. std::map
) Si aspetteranno sempre operator<
di essere presenti. Tuttavia, gli utenti del tuo tipo si aspettano che siano presenti anche tutti gli altri operatori , quindi, se lo definisci operator<
, assicurati di seguire la terza regola fondamentale del sovraccarico degli operatori e di definire anche tutti gli altri operatori di confronto booleano. Il modo canonico per 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 qualsiasi cosa, gli altri stanno semplicemente trasmettendo i loro argomenti a uno di questi due per fare il lavoro vero e proprio.
La sintassi per il sovraccarico degli operatori booleani binari rimanenti ( ||
, &&
) segue le regole degli operatori di confronto. Tuttavia, è molto improbabile che tu possa trovare un caso d'uso ragionevole per questi 2 .
1 Come per tutte le regole pratiche, a volte potrebbero esserci delle ragioni per infrangere anche questa. Se è così, non dimenticate che l'operando a sinistra degli operatori di confronto binari, che per membro sarà funzioni *this
, deve essere const
, anche. Quindi un operatore di confronto implementato come funzione membro dovrebbe avere questa firma:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Nota const
alla fine.)
2 Va notato che la versione integrata di ||
e &&
usa la semantica del collegamento. Mentre quelli definiti dall'utente (perché sono zucchero sintattico per le chiamate di metodo) non usano la semantica di scelta rapida. L'utente si aspetta che questi operatori dispongano di una semantica di scelta rapida e il loro codice potrebbe dipendere da esso, pertanto si consiglia vivamente di definirli MAI.
Operatori aritmetici
Operatori aritmetici unari
Gli operatori di incremento e decremento unari sono disponibili sia in prefisso che postfisso. Per distinguere l'uno dall'altro, le varianti postfix accettano un ulteriore argomento int fittizio. Se sovraccarichi l'incremento o il decremento, assicurati di implementare sempre entrambe le versioni prefisso e postfisso. Ecco l'implementazione canonica dell'incremento, il decremento 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 prefisso. Si noti inoltre che postfix esegue una copia aggiuntiva. 2
Il sovraccarico meno unario e positivo non è molto comune e probabilmente è meglio evitarlo. Se necessario, dovrebbero probabilmente essere sovraccaricati come funzioni membro.
2 Si noti inoltre che la variante postfix funziona di più ed è quindi meno efficiente da utilizzare rispetto alla variante prefisso. Questo è un buon motivo per preferire generalmente l'incremento del prefisso rispetto all'incremento postfisso. Mentre i compilatori di solito possono ottimizzare il lavoro aggiuntivo dell'incremento postfix per i tipi predefiniti, potrebbero non essere in grado di fare lo stesso per i tipi definiti dall'utente (che potrebbero essere qualcosa di innocentemente simile a un iteratore di elenco). Una volta che ti sei abituato a fare i++
, diventa molto difficile ricordare di fare ++i
invece quando i
non è di un tipo incorporato (in più dovresti cambiare codice quando cambi un tipo), quindi è meglio prendere l'abitudine di sempre usando l'incremento prefisso, a meno che postfix non sia esplicitamente necessario.
Operatori aritmetici binari
Per gli operatori aritmetici binari, non dimenticare di obbedire al sovraccarico del terzo operatore di regole di base: se fornisci +
, fornisci anche +=
, se fornisci -
, non omettere -=
, ecc. Si dice che Andrew Koenig sia stato il primo a osservare che l'assegnazione composta gli operatori possono essere utilizzati 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, i +
suoi compagni dovrebbero essere non membri, mentre le loro controparti di assegnazione composta ( +=
ecc.), Cambiando il loro argomento di sinistra, dovrebbero essere un membro. Ecco il codice esemplare per +=
e +
; gli altri operatori aritmetici binari dovrebbero essere implementati allo 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;
}
operator+=
restituisce il risultato per riferimento, mentre operator+
restituisce una copia del risultato. Naturalmente, la restituzione di un riferimento è in genere più efficiente della restituzione di una copia, ma nel caso in cui operator+
non sia possibile aggirare la copia. Quando scrivi a + b
, ti aspetti che il risultato sia un nuovo valore, motivo operator+
per cui deve restituire un nuovo valore. 3
Si noti inoltre che operator+
prende l'operando di sinistra per copia anziché per riferimento const. Il motivo di ciò è lo stesso del motivo che dà per operator=
prendere il suo argomento per copia.
Gli operatori di manipolazione dei bit ~
&
|
^
<<
>>
dovrebbero essere implementati allo stesso modo degli operatori aritmetici. Tuttavia, (tranne per sovraccarico <<
e >>
di uscita e di ingresso) ci sono pochi casi di utilizzo ragionevoli per sovraccarico questi.
3 Ancora una volta, la lezione da trarre da ciò è che a += b
, in generale, è più efficiente di a + b
e dovrebbe essere preferito, se possibile.
Sottoscrizione di array
L'operatore di sottoscrizione dell'array è un operatore binario che deve essere implementato come membro della classe. Viene utilizzato per tipi simili a contenitori che consentono l'accesso ai loro elementi di dati tramite una chiave. La forma canonica di fornire questi è questa:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
A meno che non si desideri che gli utenti della propria classe siano in grado di modificare gli elementi di dati restituiti operator[]
(nel qual caso è possibile omettere la variante non const), è necessario fornire sempre entrambe le varianti dell'operatore.
Se è noto che value_type si riferisce a un tipo incorporato, la variante const dell'operatore dovrebbe restituire meglio una copia anziché un riferimento const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Operatori per tipi simili a puntatori
Per definire i propri iteratori o puntatori intelligenti, è necessario sovraccaricare l'operatore di dereference prefisso unario *
e l'operatore di accesso del membro puntatore infisso binario ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Nota che anche questi avranno quasi sempre bisogno sia di una versione const che di una non const. Per l' ->
operatore, se value_type
è di class
(o struct
o union
tipo), un altro operator->()
è chiamato ricorsivamente, fino ad ottenere un operator->()
restituisce un valore di tipo non-classe.
L'indirizzo unario dell'operatore non dovrebbe mai essere sovraccaricato.
Per operator->*()
vedere questa domanda . È usato raramente e quindi raramente mai sovraccaricato. In effetti, anche gli iteratori non lo sovraccaricano.
Continua con gli operatori di conversione