Quali sono le regole e gli idiomi di base per il sovraccarico dell'operatore?


2145

Nota: le risposte sono state fornite in un ordine specifico , ma poiché molti utenti ordinano le risposte in base ai voti, piuttosto che al momento in cui sono state fornite, ecco un indice delle risposte nell'ordine in cui hanno più senso:

(Nota: questo dovrebbe essere una voce alle FAQ C ++ di Stack Overflow . Se vuoi criticare l'idea di fornire una FAQ in questo modulo, allora la pubblicazione su meta che ha iniziato tutto questo sarebbe il posto dove farlo. tale domanda viene monitorata nella chatroom di C ++ , dove l'idea FAQ è iniziata in primo luogo, quindi è molto probabile che la tua risposta venga letta da coloro che hanno avuto l'idea.)


63
Se continueremo con il tag C ++ - FAQ, ecco come devono essere formattate le voci.
John Dibling,

Ho scritto una breve serie di articoli per la comunità tedesca del C ++ sul sovraccarico degli operatori: Parte 1: il sovraccarico degli operatori in C ++ riguarda la semantica, l'uso tipico e le specialità per tutti gli operatori. Ha alcune sovrapposizioni con le tue risposte qui, tuttavia ci sono alcune informazioni aggiuntive. Le parti 2 e 3 costituiscono un tutorial per l'utilizzo di Boost.Operators. Vorresti che le traducessi e le aggiungessi come risposte?
Arne Mertz,

Oh, ed è disponibile anche una traduzione in inglese: le basi e la pratica comune
Arne Mertz,

Risposte:


1044

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' thisargomento 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 constalla 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 ++iinvece quando inon è 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 + be 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 structo uniontipo), 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


89
operator->()è in realtà estremamente strano. Non è necessario restituire un value_type*- in effetti, può restituire un altro tipo di classe, a condizione che quel tipo di classe abbia unoperator->() , che verrà quindi chiamato successivamente. Questa chiamata ricorsiva di operator->()s procede fino a quando si value_type*verifica un tipo restituito. Follia! :)
j_random_hacker il

2
Non si tratta esattamente di efficacia. Si tratta di non poterlo fare nel modo tradizionale-idiomatico in (molto) pochi casi: quando la definizione di entrambi gli operandi deve rimanere invariata mentre calcoliamo il risultato. E come ho detto, ci sono due esempi classici: moltiplicazione delle matrici e moltiplicazione dei polinomi. Potremmo definire *in termini di *=ma sarebbe imbarazzante perché una delle prime operazioni di *=creare un nuovo oggetto, risultato del calcolo. Quindi, dopo il ciclo for-ijk, scambiamo questo oggetto temporaneo con *this. vale a dire. 1.copy, 2.operator *, 3.swap
Luc Hermitte,

6
Non sono d'accordo con le versioni const / non-const dei tuoi operatori simili a puntatori, ad esempio `const value_type & operator * () const;` - questo sarebbe come avere un T* constritorno const T&sul dereferencing, che non è il caso. O in altre parole: un puntatore const non implica un puntatore const. In effetti, non è banale imitare T const *- che è la ragione di tutto il const_iteratormateriale nella libreria standard. Conclusione: la firma dovrebbe esserereference_type operator*() const; pointer_type operator->() const
Arne Mertz,

6
Un commento: l'implementazione degli operatori aritmetici binari suggerita non è così efficiente come può essere. Se Boost operatori intestazioni simmetry note: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Se si utilizza una copia locale del primo parametro, si fa + = e si restituisce il valore + copia locale. Ciò consente l'ottimizzazione NRVO.
Manu343726,

3
Come ho detto nella chat, L <= Rpuò anche essere espresso come !(R < L)anziché !(L > R). Potrebbe salvare un ulteriore livello di allineamento in espressioni difficili da ottimizzare (ed è anche il modo in cui Boost.Operators lo implementa).
TemplateRex,

494

Le tre regole di base del sovraccarico dell'operatore in C ++

Quando si tratta di sovraccarico dell'operatore in C ++, ci sono tre regole di base che dovresti seguire . Come con tutte queste regole, ci sono davvero delle eccezioni. A volte le persone si sono discostate da loro e il risultato non è stato un cattivo codice, ma tali deviazioni positive sono poche e lontane tra loro. Per lo meno, 99 delle 100 deviazioni che ho visto erano ingiustificate. Tuttavia, potrebbe anche essere stato 999 su 1000. Quindi è meglio attenersi alle seguenti regole.

  1. Ogni volta che il significato di un operatore non è ovviamente chiaro e indiscusso, non dovrebbe essere sovraccarico. Fornisci invece una funzione con un nome ben scelto.
    Fondamentalmente, la prima e più importante regola per gli operatori di sovraccarico, nel suo cuore, dice: Non farlo . Ciò potrebbe sembrare strano, perché c'è molto da sapere sul sovraccarico dell'operatore e quindi molti articoli, capitoli di libri e altri testi trattano tutto questo. Ma nonostante questa evidenza apparentemente ovvia, ci sono solo pochi casi sorprendentemente in cui il sovraccarico dell'operatore è appropriato. Il motivo è che in realtà è difficile comprendere 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.

  2. Attenersi sempre alla semantica ben nota dell'operatore.
    Il C ++ non pone limiti alla semantica degli operatori sovraccarichi. Il compilatore accetterà felicemente il codice che implementa l'+operatorebinarioper sottrarre dal suo operando giusto. Tuttavia, gli utenti di tale operatore non avrebbe mai sospettato l'espressionea + bper sottrarreadab. Naturalmente, ciò suppone che la semantica dell'operatore nel dominio dell'applicazione sia indiscussa.

  3. Fornire sempre tutto da una serie di operazioni correlate.
    Gli operatori sono collegati tra loro e con altre operazioni. Se il tuo tipo supportaa + b, anche gli utenti si aspetteranno di poter chiamarea += b. Se supporta l'incremento del prefisso++a, si aspetteranno anchea++di funzionare. Se riescono a verificare sea < b, sicuramente si aspetteranno anche di poter verificare sea > b. Se riescono a costruire il tuo tipo di copia, si aspettano che anche il compito funzioni.


Continua con la decisione tra membri e non membri .


16
L'unica cosa di cui sono consapevole che viola uno di questi è boost::spiritlol.
Billy ONeal,

66
@Billy: Secondo alcuni, abusare +della concatenazione di stringhe è una violazione, ma ormai è diventata prassi consolidata, quindi sembra naturale. Anche se ricordo una classe di stringhe fatte in casa che ho visto negli anni '90 che utilizzavano il binario &per questo scopo (facendo riferimento a BASIC per prassi consolidate). Ma, sì, inserendolo nella libreria standard di base praticamente imposta questo in pietra. Lo stesso vale per l'abuso <<e >>per IO, BTW. Perché il cambio a sinistra sarebbe l'operazione di output ovvia? Perché lo abbiamo imparato tutti quando abbiamo visto il nostro primo "Ciao, mondo!" applicazione. E per nessun altro motivo.
sabato

5
@curiousguy: se devi spiegarlo, non è ovviamente chiaro e indiscusso. Allo stesso modo se è necessario discutere o difendere il sovraccarico.
sabato

5
@sbi: "peer review" è sempre una buona idea. Per me un operatore mal scelto non è diverso da un nome di funzione mal scelto (ne ho visti molti). Gli operatori sono solo funzioni. Ne più ne meno. Le regole sono le stesse. E per capire se un'idea è buona, il modo migliore è capire quanto tempo ci vuole per essere capito. (Quindi, la revisione tra pari è un must, ma i pari devono essere scelti tra persone libere da dogmi e pregiudizi.)
Emilio Garavaglia

5
@sbi Per me, l'unico fatto assolutamente ovvio e indiscutibile operator==è che dovrebbe essere una relazione di equivalenza (IOW, non dovresti usare NaN non segnalante). Esistono molte utili relazioni di equivalenza sui contenitori. Cosa significa uguaglianza? " auguale b" significa che ae bhanno lo stesso valore matematico. Il concetto di valore matematico di un (non NaN) floatè chiaro, ma il valore matematico di un contenitore può avere molte utili definizioni distinte (tipo ricorsivo). La più forte definizione di uguaglianza è "sono gli stessi oggetti" ed è inutile.
curioso

265

La sintassi generale del sovraccarico dell'operatore in C ++

Non è possibile modificare il significato degli operatori per i tipi predefiniti in C ++, gli operatori possono essere sovraccaricati solo per i tipi 1 definiti dall'utente . Cioè, almeno uno degli operandi deve essere di tipo definito dall'utente. Come con altre funzioni sovraccaricate, gli operatori possono essere sovraccaricati per una determinata serie 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:

  • operatori aritmetici: + - * / %e += -= *= /= %=(tutta l'infezione binaria); + -(prefisso unario); ++ --(prefisso e postfisso unari)
  • manipolazione dei bit: & | ^ << >>e &= |= ^= <<= >>=(tutta l'infezione binaria); ~(prefisso unario)
  • algebra booleana: == != < > <= >= || &&(tutta l'infezione binaria); !(prefisso unario)
  • gestione della memoria: new new[] delete delete[]
  • operatori di conversione implicita
  • miscellanea: = [] -> ->* , (tutta l'infezione binaria); * &(tutto prefisso unario) ()(chiamata di funzione, infisso n-ary)

Tuttavia, il fatto che è possibile sovraccaricare tutti questi non significa che si dovrebbe farlo. Vedere le regole di base del sovraccarico dell'operatore.

In C ++, gli operatori sono sovraccaricati sotto forma di funzioni con nomi speciali . Come con altre funzioni, gli operatori sovraccarichi possono generalmente essere implementati come una funzione membro del tipo del loro operando di sinistra o come funzioni non membro . Se sei libero di scegliere o sei obbligato a utilizzare uno dei due dipende da diversi criteri. 2 Un operatore unario @3 , applicato a un oggetto x, viene richiamato come operator@(x)o come x.operator@(). Un operatore di infissione binaria @, applicato agli oggetti xe y, viene chiamato come operator@(x,y)o come x.operator@(y). 4

Gli operatori implementati come funzioni non membro a volte sono amici del tipo di operando.

1 Il termine "definito dall'utente" potrebbe essere leggermente fuorviante. C ++ distingue tra tipi predefiniti e tipi definiti dall'utente. Al primo appartengono ad esempio int, char e double; a questi ultimi appartengono tutti i tipi di struttura, classe, unione ed enum, compresi quelli della libreria standard, anche se non sono definiti come tali dagli utenti.

2 Questo è trattato in una parte successiva di questa FAQ.

3 The @non è un operatore valido in C ++ ed è per questo che lo uso come segnaposto.

4 L'unico operatore ternario in C ++ non può essere sovraccaricato e l'unico operatore n-ary deve sempre essere implementato come funzione membro.


Continua con Le tre regole di base del sovraccarico dell'operatore in C ++ .


~è un prefisso unario, non un infisso binario.
mrkj,

1
.*manca nell'elenco degli operatori non sovraccaricabili.
celticminstrel,

1
@Mateen Volevo usare un segnaposto anziché un vero operatore per chiarire che non si tratta di un operatore speciale, ma si applica a tutti loro. E, se vuoi essere un programmatore C ++, dovresti imparare a prestare attenzione anche al smallprint. :)
sbi,

1
@HR: Se avessi letto questa guida, sapresti cosa c'è che non va. In genere suggerisco di leggere le prime tre risposte collegate alla domanda. Non dovrebbe essere più di mezz'ora della tua vita e ti dà una comprensione di base. La sintassi specifica dell'operatore che puoi consultare in seguito. Il tuo problema specifico suggerisce di provare a sovraccaricare operator+()come funzione membro, ma gli ha dato la firma di una funzione gratuita. Vedi qui .
sabato

1
@sbi: ho già letto i primi tre post e grazie per averli realizzati. :) Proverò a risolvere il problema, altrimenti penso che sia meglio porlo su una domanda separata. Grazie ancora per averci reso la vita così facile! : D
Hosein Rahnama,

251

La decisione tra membri e non membri

Gli operatori binari =(assegnazione), [](iscrizione array), ->(accesso membro), nonché l' ()operatore n-ary (chiamata di funzione), devono sempre essere implementati come funzioni membro , poiché la sintassi della lingua li richiede.

Altri operatori possono essere implementati come membri o come non membri. Alcuni di essi, tuttavia, in genere devono essere implementati come funzioni non membro, poiché il loro operando di sinistra non può essere modificato dall'utente. I più importanti di questi sono gli operatori di input e output <<e >>, i cui operandi di sinistra sono classi di stream dalla libreria standard che non è possibile modificare.

Per tutti gli operatori in cui è necessario scegliere di implementarli come funzione membro o come funzione non membro, utilizzare le seguenti regole pratiche per decidere:

  1. Se si tratta di un operatore unario , implementarlo come una funzione membro .
  2. Se un operatore binario tratta entrambi gli operandi allo stesso modo (li lascia invariati), implementare questo operatore come una funzione non membro .
  3. Se un operatore binario non tratta entrambi gli operandi allo stesso modo (di solito cambierà l'operando di sinistra), potrebbe essere utile renderlo una funzione membro del tipo di operando di sinistra, se deve accedere alle parti private dell'operando.

Naturalmente, come per tutte le regole pratiche, ci sono eccezioni. Se hai un tipo

enum Month {Jan, Feb, ..., Nov, Dec}

e vuoi sovraccaricare gli operatori di incremento e decremento per questo, non puoi farlo come funzioni membro, poiché in C ++, i tipi enum non possono avere funzioni membro. Quindi devi sovraccaricarlo come funzione gratuita. E operator<()per un modello di classe nidificato all'interno di un modello di classe è molto più semplice scrivere e leggere quando fatto come una funzione membro in linea nella definizione della classe. Ma queste sono davvero rare eccezioni.

(Tuttavia, se si fa un'eccezione, non dimenticare il problema di const-ness per l'operando che, per le funzioni membro, diventa l' thisargomento implicito . Se l'operatore come funzione non membro prenderebbe l'argomento più a sinistra come constriferimento , lo stesso operatore di una funzione membro deve avere un constalla fine per fare *thisun constriferimento.)


Passare a Operatori comuni per sovraccarico .


9
L'articolo di Herb Sutter in Effective C ++ (o è C ++ Coding Standards?) Dice che si dovrebbe preferire le funzioni non amiche non membro alle funzioni membro, per aumentare l'incapsulamento della classe. IMHO, il motivo dell'incapsulamento ha la precedenza sulla regola empirica, ma non diminuisce il valore di qualità della regola empirica.
Paercebal,

8
@paercebal: C ++ efficace è di Meyers, C ++ Standard di codifica di Sutter. a quale ti stai riferendo? Ad ogni modo, non mi piace l'idea, diciamo, di operator+=()non essere un membro. Deve cambiare il suo operando di sinistra, quindi per definizione deve scavare in profondità nelle sue viscere. Cosa guadagneresti non facendolo diventare membro?
sabato

9
@sbi: Item 44 in C ++ Coding Standards (Sutter) Preferisce scrivere funzioni non amichevoli non membro , ovviamente, si applica solo se puoi effettivamente scrivere questa funzione usando solo l'interfaccia pubblica della classe. Se non puoi (o puoi ma ciò ostacolerebbe gravemente le prestazioni), allora devi renderlo membro o amico.
Matthieu M.,

3
@sbi: Oops, efficace, eccezionale ... Non c'è da stupirsi che mescoli i nomi. Ad ogni modo il guadagno è limitare il più possibile il numero di funzioni che hanno accesso a un oggetto dati privati ​​/ protetti. In questo modo, aumenti l'incapsulamento della tua classe, semplificandone la manutenzione / i test / l'evoluzione.
Paercebal,

12
@sbi: un esempio. Diciamo che stai codificando una classe String, con entrambi operator +=i appendmetodi. Il appendmetodo è più completo, poiché è possibile aggiungere una sottostringa del parametro dall'indice i all'indice n -1: append(string, start, end)sembra logico avere +=un'appendice di chiamata con start = 0e end = string.size. In quel momento, append potrebbe essere un metodo membro, ma operator +=non ha bisogno di essere un membro, e renderlo un non membro ridurrebbe la quantità di codice che gioca con le stringhe interne, quindi è una buona cosa ... ^ _ ^ ...
paercebal,

165

Operatori di conversione (noti anche come conversioni definite dall'utente)

In C ++ puoi creare operatori di conversione, operatori che consentono al compilatore di convertire tra i tuoi tipi e altri tipi definiti. Esistono due tipi di operatori di conversione, impliciti ed espliciti.

Operatori di conversione implicita (C ++ 98 / C ++ 03 e C ++ 11)

Un operatore di conversione implicita consente al compilatore di convertire implicitamente (come la conversione tra inte long) il valore di un tipo definito dall'utente in un altro tipo.

Di seguito è una classe semplice con un operatore di conversione implicito:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Gli operatori di conversione implicita, come i costruttori a argomento singolo, sono conversioni definite dall'utente. I compilatori garantiranno una conversione definita dall'utente quando tentano di abbinare una chiamata a una funzione sovraccarica.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

All'inizio sembra molto utile, ma il problema è che la conversione implicita si avvia anche quando non è prevista. Nel seguente codice, void f(const char*)verrà chiamato perché my_string()non è un lvalue , quindi il primo non corrisponde:

void f(my_string&);
void f(const char*);

f(my_string());

I principianti possono facilmente sbagliare e persino i programmatori C ++ esperti sono talvolta sorpresi perché il compilatore rileva un sovraccarico che non sospettavano. Questi problemi possono essere mitigati da operatori di conversione espliciti.

Operatori di conversione esplicita (C ++ 11)

A differenza degli operatori di conversione impliciti, gli operatori di conversione esplicita non si attivano mai quando non ci si aspetta che lo facciano. Quella che segue è una classe semplice con un operatore di conversione esplicita:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Notare il explicit. Ora, quando si tenta di eseguire il codice imprevisto dagli operatori di conversione implicita, viene visualizzato un errore del compilatore:

prog.cpp: nella funzione 'int main ()':
prog.cpp: 15: 18: errore: nessuna funzione corrispondente per la chiamata a 'f (my_string)'
prog.cpp: 15: 18: nota: i candidati sono:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: nota: nessuna conversione nota per l'argomento 1 da 'my_string' a 'my_string &'
prog.cpp: 12: 10: note: void f (const char *)
prog.cpp: 12: 10: nota: nessuna conversione nota per l'argomento 1 da 'my_string' a 'const char *'

Per invocare l'operatore di cast esplicito, devi usare static_castun cast in stile C o un cast in stile costruttore (es T(value).).

Tuttavia, esiste un'eccezione a ciò: al compilatore è consentito convertire in modo implicito bool. Inoltre, al compilatore non è consentito eseguire un'altra conversione implicita dopo la conversione in bool(a un compilatore è consentito eseguire 2 conversioni implicite alla volta, ma solo 1 conversione definita dall'utente al massimo).

Poiché il compilatore non eseguirà il cast "passato" bool, gli operatori di conversione esplicita ora rimuovono la necessità del linguaggio sicuro Bool . Ad esempio, i puntatori intelligenti prima di C ++ 11 utilizzavano il linguaggio Safe Bool per impedire le conversioni ai tipi integrali. In C ++ 11, i puntatori intelligenti utilizzano invece un operatore esplicito perché al compilatore non è consentito convertire implicitamente in un tipo integrale dopo aver convertito esplicitamente un tipo in bool.

Continua con sovraccarico newedelete .


148

Sovraccarico newedelete

Nota: si tratta solo della sintassi del sovraccariconewedeletenon dell'implementazione di tali operatori sovraccarichi. Penso che la semantica del sovraccariconew e deletemeriti le proprie FAQ , all'interno del tema del sovraccarico dell'operatore non posso mai rendergli giustizia.

Nozioni di base

In C ++, quando si scrive un nuova espressione come new T(arg)due cose accadere quando questa espressione viene valutata: In primo luogo operator newè invocato per ottenere la memoria cruda, e poi il costruttore appropriato di Tè invocato per trasformare questa memoria grezzo in un oggetto valido. Allo stesso modo, quando si elimina un oggetto, viene prima chiamato il suo distruttore, quindi viene restituita la memoria operator delete.
C ++ consente di ottimizzare entrambe queste operazioni: gestione della memoria e costruzione / distruzione dell'oggetto nella memoria allocata. Quest'ultimo è fatto scrivendo costruttori e distruttori per una classe. La messa a punto della gestione della memoria viene eseguita scrivendo la propria operator newe operator delete.

La prima delle regole di base del sovraccarico dell'operatore - non farlo - si applica soprattutto al sovraccarico newe delete. Quasi le uniche ragioni per sovraccaricare questi operatori sono problemi di prestazioni e vincoli di memoria e , in molti casi, altre azioni, come le modifiche agli algoritmi utilizzati, forniranno un rapporto costi / guadagni molto più elevato rispetto al tentativo di modificare la gestione della memoria.

La libreria standard C ++ include un set di operatori newe predefiniti delete. I più importanti sono questi:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

I primi due allocano / deallocano la memoria per un oggetto, gli ultimi due per un array di oggetti. Se fornisci le tue versioni di questi, non sovraccaricheranno, ma sostituiranno quelle della libreria standard.
Se sovraccarichi operator new, dovresti sempre sovraccaricare anche la corrispondenza operator delete, anche se non hai mai intenzione di chiamarla. Il motivo è che, se un costruttore lancia durante la valutazione di una nuova espressione, il sistema di runtime restituirà la memoria alla operator deletecorrispondenza operator newche era stata chiamata per allocare la memoria per creare l'oggetto. Se non si fornisce una corrispondenza operator delete, viene chiamato quello predefinito, che è quasi sempre sbagliato.
Se sovraccarichi newe delete, dovresti considerare di sovraccaricare anche le varianti di array.

Posizionamento new

C ++ consente agli operatori nuovi ed eliminati di accettare argomenti aggiuntivi.
Il cosiddetto posizionamento new consente di creare un oggetto a un determinato indirizzo che viene passato a:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

La libreria standard viene fornita con i sovraccarichi appropriati dei nuovi operatori e cancella gli operatori per questo:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Si noti che, nel codice di esempio per il posizionamento di cui sopra, operator deletenon viene mai chiamato, a meno che il costruttore di X non generi un'eccezione.

Puoi anche sovraccaricare newe deletecon altri argomenti. Come per l'argomento aggiuntivo per il posizionamento nuovo, questi argomenti sono elencati anche tra parentesi dopo la parola chiave new. Solo per motivi storici, tali varianti sono spesso chiamate anche posizionamento nuovo, anche se i loro argomenti non sono per posizionare un oggetto a un indirizzo specifico.

Novità ed eliminazione specifiche della classe

Più comunemente vorrai ottimizzare la gestione della memoria perché la misurazione ha dimostrato che le istanze di una classe specifica, o di un gruppo di classi correlate, vengono create e distrutte spesso e che la gestione della memoria predefinita del sistema di runtime, ottimizzata per prestazioni generali, si occupa in modo inefficiente in questo caso specifico. Per migliorare ciò, puoi sovraccaricare il nuovo ed eliminare per una classe specifica:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Sovraccaricato in tal modo, nuovo ed elimina si comportano come le funzioni di membro statico. Per gli oggetti di my_class, l' std::size_targomento sarà sempre sizeof(my_class). Tuttavia, questi operatori sono anche chiamati per oggetti allocati dinamicamente di classi derivate , nel qual caso potrebbe essere maggiore di quello.

Nuovo globale ed elimina

Per sovraccaricare il nuovo globale ed eliminare, è sufficiente sostituire gli operatori predefiniti della libreria standard con i nostri. Tuttavia, questo raramente deve essere fatto.


11
Inoltre, non sono d'accordo sul fatto che la sostituzione dell'operatore globale con new ed delete è generalmente per prestazioni: al contrario, di solito è per la ricerca di bug.
Yttrill,

1
Si noti inoltre che se si utilizza un nuovo operatore sovraccarico, è necessario fornire anche un operatore di eliminazione con argomenti corrispondenti. Lo dici nella sezione globale new / delete dove non è di grande interesse.
Yttrill,

13
@Yttrill stai confondendo le cose. Il significato viene sovraccaricato. Ciò che significa "sovraccarico dell'operatore" è che il significato è sovraccarico. Ciò non significa che le funzioni letteralmente siano sovraccaricate, e in particolare l' operatore nuovo non sovraccaricherà la versione dello Standard. @sbi non afferma il contrario. È comune chiamarlo "sovraccarico nuovo" così come è comune dire "sovraccarico operatore addizione".
Johannes Schaub - litb

1
@sbi: vedi (o meglio, link a) gotw.ca/publications/mill15.htm . È solo una buona pratica nei confronti delle persone che a volte ne usano di nothrownuove.
Alexandre C.,

1
"Se non si fornisce una cancellazione dell'operatore corrispondente, viene chiamata quella predefinita" -> In realtà, se si aggiungono argomenti e non si crea una cancellazione corrispondente, nessuna cancellazione dell'operatore viene chiamata e si verifica una perdita di memoria. (15.2.2, la memoria occupata dall'oggetto viene deallocata solo se viene trovata un'appropriata ... eliminazione dell'operatore)
dascandy

46

Perché la operator<<funzione per lo streaming di oggetti su std::couto su un file non può essere una funzione membro?

Diciamo che hai:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Detto questo, non è possibile utilizzare:

Foo f = {10, 20.0};
std::cout << f;

Poiché operator<<viene sovraccaricato come una funzione membro di Foo, l'LHS dell'operatore deve essere un Foooggetto. Ciò significa che ti verrà richiesto di utilizzare:

Foo f = {10, 20.0};
f << std::cout

che è molto intuitivo.

Se lo definisci come una funzione non membro,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Sarai in grado di usare:

Foo f = {10, 20.0};
std::cout << f;

che è molto intuitivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.