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


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

(Nota: Questo è pensato per essere una voce per Domande frequenti su C ++ di Overflow dello stack. Se vuoi criticare l'idea di fornire una FAQ in questo modulo, allora il post su meta che ha dato inizio a tutto questo sarebbe il posto giusto per farlo. Le risposte a questa domanda sono monitorate nel Chatroom C ++, in cui l'idea delle FAQ è iniziata in primo luogo, quindi è molto probabile che la tua risposta venga letta da coloro che hanno avuto l'idea.)  


1841
2017-12-12 12:44


origine


risposte:


Operatori comuni da sovraccaricare

La maggior parte del lavoro negli operatori con 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 fatto da (e spesso viene inoltrato 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 degli utenti non verrà compilato o il codice degli utenti si comporterà in modo sorprendente.

Operatore di assegnazione

C'è molto da dire sull'assegnazione. Tuttavia, la maggior parte di questo è già stato detto in Le famose FAQ di copia e sostituzione di GMan, quindi salterò la maggior parte qui, elencando solo l'operatore di assegnazione perfetto per 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 overload sovraccarico nella maggior parte delle applicazioni. Per l'overloading della guida come operatori di manipolazione di bit, vedere la sezione seguente sugli operatori aritmetici binari. Per implementare il tuo formato personalizzato e la logica di analisi quando il tuo oggetto viene utilizzato con iostream, continua.

Gli operatori di flusso, tra gli operatori più comunemente sovraccaricati, sono operatori in formato binario per i quali la sintassi non specifica alcuna restrizione sulla necessità di membri o non membri. Dal momento che cambiano il loro argomento di sinistra (alterano lo stato del flusso), dovrebbero, secondo le regole del pollice, essere implementati come membri del tipo del loro 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 del flusso definiti dalla libreria standard sono effettivamente definiti come membri delle classi stream, quando si implementano le operazioni di output e input per i propri tipi, si non è possibile modificare i tipi di flusso della libreria standard. Ecco perché è necessario implementare questi operatori per i propri tipi come funzioni non membri. 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;
}

Quando si implementa operator>>, l'impostazione manuale dello stato del flusso è necessaria solo quando la lettura stessa è riuscita, ma il risultato non è quello che ci si aspetterebbe.

Operatore di chiamata di funzione

L'operatore di chiamata di funzione, utilizzato per creare oggetti funzione, noti anche come funtori, deve essere definito come a membro funzione, quindi ha sempre l'implicito this argomento delle funzioni membro. Oltre a questo può essere sovraccaricato per prendere qualsiasi numero di argomenti aggiuntivi, compreso 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");

Attraverso la libreria standard C ++, gli oggetti funzione vengono sempre copiati. I tuoi 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 si riferisca ad esso.

Operatori di confronto

Gli operatori di confronto binario infisso dovrebbero, secondo le regole del pollice, essere implementati come funzioni non membri1. La negazione del prefisso unario ! dovrebbe (secondo le stesse regole) essere implementato come funzione membro. (ma di solito non è una buona idea sovraccaricarlo).

Gli algoritmi della libreria standard (ad es. std::sort()) e tipi (ad es. std::map) si aspetta sempre operator< essere presente. comunque, il gli utenti del tuo tipo si aspettano che tutti gli altri operatori siano presentianche, quindi se lo definisci operator<, assicurarsi di seguire la terza regola fondamentale dell'overloading dell'operatore e definire anche tutti gli altri operatori di confronto booleani. 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 qualcosa, gli altri stanno semplicemente inoltrando i loro argomenti a uno di questi due per fare il lavoro vero e proprio.

La sintassi per sovraccaricare i restanti operatori binari booleani (||, &&) segue le regole degli operatori di confronto. Tuttavia, lo è molto improbabile che tu possa trovare un caso d'uso ragionevole per questi2.

1  Come con tutte le regole empiriche, a volte potrebbero esserci motivi per rompere anche questo. Se è così, non dimenticare che l'operando di sinistra degli operatori di confronto binario, che per le funzioni membro sarà *this, deve essere const, pure. Quindi un operatore di confronto implementato come funzione membro dovrebbe avere questa firma:

bool operator<(const X& rhs) const { /* do actual comparison with *this */ }

(Notare la const alla fine.)

2  Va notato che la versione integrata di || e && usa la semantica della scorciatoia. Mentre quelli definiti dall'utente (perché sono zucchero sintattico per le chiamate di metodo) non usano la semantica di collegamento. L'utente si aspetta che questi operatori abbiano una semantica di collegamenti e il loro codice potrebbe dipendere da esso, pertanto è altamente consigliato NON definirli.

Operatori aritmetici

Operatori aritmetici unari

Gli operatori incrementali e decrescenti unari hanno sia il gusto prefisso che postfix. Per distinguere l'uno dall'altro, le varianti postfix richiedono un argomento fittizio int aggiuntivo. Se sovraccarichi l'incremento o il decremento, assicurati di implementare sempre entrambe le versioni di prefisso e postfix. Ecco l'implementazione canonica di incremento, 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 fa una copia extra.2

Il sovraccarico di unario meno e più non è molto comune e probabilmente è meglio evitare. Se necessario, dovrebbero probabilmente essere sovraccaricati come funzioni membro.

2  Si noti inoltre che la variante postfix ha più lavoro ed è quindi meno efficiente da usare rispetto alla variante prefisso. Questa è una buona ragione per preferire generalmente l'incremento del prefisso rispetto all'incremento postfisso. Mentre i compilatori di solito possono ottimizzare il lavoro aggiuntivo di incremento postfisso per i tipi predefiniti, potrebbero non essere in grado di fare lo stesso per i tipi definiti dall'utente (che potrebbe essere qualcosa che sembra innocentemente come un iteratore di elenchi). Una volta ti sei abituato a farlo i++, diventa molto difficile ricordarsi di fare ++i invece quando i non è di tipo built-in (in più dovresti cambiare il codice quando cambi un tipo), quindi è meglio prendere l'abitudine di usare sempre l'incremento del prefisso, a meno che non sia esplicitamente necessario postfix.

Operatori aritmetici binari

Per gli operatori di aritmetica binaria, non dimenticare di obbedire al terzo sovraccarico dell'operatore di regole di base: Se fornisci +, anche fornire +=, se fornisci -, non omettere -=, ecc. Andrew Koenig è stato il primo ad osservare che gli operatori di assegnazione compositi possono essere usati come base per le loro controparti non composte. Cioè, operatore + è implementato in termini di +=, - è implementato in termini di -= eccetera.

Secondo le nostre regole pratiche, + e i suoi compagni dovrebbero essere non membri, mentre i loro corrispettivi di assegnazione composti (+= ecc.), cambiando il loro argomento a 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;
}

operator+= restituisce il suo risultato per riferimento, mentre operator+ restituisce una copia del suo risultato. Ovviamente, restituire un riferimento è in genere più efficiente del restituire una copia, ma nel caso di operator+, non c'è modo di aggirare la copia. Quando scrivi a + b, ti aspetti che il risultato sia un nuovo valore, ecco perché operator+ deve restituire un nuovo valore.3 Si noti inoltre che operator+ prende il suo operando di sinistra per copia piuttosto che con riferimento const. La ragione di questo è la stessa ragione per la quale si dà ragione operator= prendendo la sua argomentazione per copia.

Gli operatori di manipolazione bit ~  &  |  ^  <<  >> dovrebbe essere implementato allo stesso modo degli operatori aritmetici. Tuttavia, (eccetto per il sovraccarico << e >> per output e input) ci sono pochissimi casi d'uso ragionevoli per sovraccaricarli.

3  Ancora una volta, la lezione da trarre da questo è quella a += b è, in generale, più efficiente di a + b e dovrebbe essere preferito, se possibile.

Sottoscrizione di array

L'operatore dell'abbonato 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 relativi elementi di dati tramite una chiave. La forma canonica di fornire questi è questo:

class X {
        value_type& operator[](index_type idx);
  const value_type& operator[](index_type idx) const;
  // ...
};

A meno che tu non voglia che gli utenti della tua classe siano in grado di modificare gli elementi di dati restituiti da operator[] (nel qual caso è possibile omettere la variante non const), è necessario fornire sempre entrambe le varianti dell'operatore.

Se value_type è noto per fare riferimento a un tipo built-in, la variante const dell'operatore dovrebbe restituire una copia anziché un riferimento const.

Operatori per tipi simili a puntatori

Per definire i propri iteratori o puntatori intelligenti, è necessario sovraccaricare l'operatore di dereferenza del prefisso unario * e l'operatore di accesso ai membri del 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 hanno quasi sempre bisogno sia di una versione const sia di una versione non const. Per il -> operatore, se value_type è di class (o struct o union) tipo, un altro operator->() si chiama ricorsivamente, fino a un operator->() restituisce un valore di tipo non di classe.

L'operatore unario di indirizzo non dovrebbe mai essere sovraccaricato.

Per operator->*() vedere questa domanda. È usato raramente e quindi raramente sovraccarico. In effetti, anche gli iteratori non lo sovraccaricano.


Continuare a Operatori di conversione


896
2017-12-12 12:47



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

Quando si tratta di sovraccaricare gli operatori 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 hanno deviato da loro e il risultato non era un codice cattivo, ma tali deviazioni positive sono poche e lontane tra loro. Per lo meno, 99 su 100 di tali 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 è chiaramente chiaro e indiscusso, non dovrebbe essere sovraccaricato.  Invece, fornire una funzione con un nome ben scelto.
    Fondamentalmente, la prima e più importante regola per gli operatori che sovraccaricano, nel suo stesso cuore, dice: Non farlo. Potrebbe sembrare strano, perché c'è molto da sapere sull'overloading dell'operatore e quindi molti articoli, capitoli di libri e altri testi trattano tutto ciò. Ma nonostante questa prova apparentemente ovvia, ci sono solo sorprendentemente pochi casi in cui il sovraccarico dell'operatore è appropriato. Il motivo è 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.

  2. Attenersi sempre alla semantica ben nota dell'operatore.
    Il C ++ non pone limiti alla semantica degli operatori sovraccaricati. Il compilatore accetterà felicemente il codice che implementa il binario + operatore per sottrarre dal suo operando di destra. Tuttavia, gli utenti di un tale operatore non sospetterebbero mai l'espressione a + b sottrarre a a partire dal b. Naturalmente, questo suppone che la semantica dell'operatore nel dominio dell'applicazione sia indiscussa.

  3. Fornisci sempre tutto da un insieme di operazioni correlate.
    Gli operatori sono collegati tra loroe ad altre operazioni. Se il tuo tipo supporta a + b, gli utenti si aspettano di essere in grado di chiamare a += b, pure. Se supporta l'incremento del prefisso ++a, si aspetteranno a++ lavorare pure. Se possono controllare se a < bsicuramente si aspetteranno anche di poter verificare se a > b. Se riescono a copiare-costruire il tuo tipo, si aspettano che anche l'incarico funzioni.


Continuare a La decisione tra membro e non membro.


440
2017-12-12 12:45



La sintassi generale dell'overloading 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 definiti dall'utente1. Cioè, almeno uno degli operandi deve essere di un tipo definito dall'utente. Come per altre funzioni sovraccariche, 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:

  • operatori aritmetici: +  -  *  /  % e +=  -=  *=  /=  %= (tutti i binari infissi); +  - (prefisso unario); ++  -- (prefisso unificato e postfix)
  • manipolazione bit: &  |  ^  <<  >> e &=  |=  ^=  <<=  >>= (tutti i binari infissi); ~ (prefisso unario)
  • algebra booleana: ==  !=  <  >  <=  >=  ||  && (tutti i binari infissi); ! (prefisso unario)
  • gestione della memoria: new  new[]  delete  delete[]
  • operatori impliciti di conversione
  • miscellanea: =  []  ->  ->*  ,  (tutti i binari infissi); *  & (tutto il prefisso unario) () (chiamata di funzione, n-ary infix)

Tuttavia, il fatto che tu può sovraccaricare tutto questo non significa che tu dovrebbero fare così. Vedi le regole di base del sovraccarico dell'operatore.

In C ++, gli operatori sono sovraccarichi sotto forma di funziona con nomi speciali. Come con altre funzioni, gli operatori sovraccaricati possono generalmente essere implementati come a funzione membro del tipo dell'operando sinistro o come funzioni non membri. Se sei libero di scegliere o di utilizzare uno dei due dipende da diversi criteri.2 Un operatore unario @3, applicato a un oggetto x, viene invocato come operator@(x) o come x.operator@(). Un operatore di infissi binari @, applicato agli oggetti x e y, si chiama o come operator@(x,y) o come x.operator@(y).4 

Gli operatori che vengono implementati come funzioni non membri sono talvolta amici del tipo del loro operando.

1  Il termine "definito dall'utente" potrebbe essere leggermente fuorviante. C ++ fa la distinzione tra tipi built-in e tipi definiti dall'utente. Al primo appartengono per esempio int, char e double; a quest'ultimo appartengono tutti i tipi di struct, class, union ed enum, compresi quelli della libreria standard, anche se non sono, come tali, definiti dagli utenti.

2  Questo è coperto in una parte successiva di queste FAQ.

3  Il @ 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.


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


229
2017-12-12 12:46



La decisione tra membro e non membro

Gli operatori binari = (Assegnazione), [] (abbonamento array), -> (accesso membro), così come l'n-ario ()(chiamata di funzione), deve sempre essere implementato come funzioni membro, perché la sintassi della lingua li richiede.

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

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

  1. Se è un operatore unario, implementalo come a membro funzione.
  2. Se un operatore binario tratta entrambi gli operandi allo stesso modo (li lascia invariati), implementa questo operatore come a non membro funzione.
  3. Se un operatore binario lo fa non tratta entrambi i suoi operandi ugualmente (di solito cambierà il suo operando di sinistra), potrebbe essere utile renderlo a membro funzione del tipo del suo operando di sinistra, se deve accedere alle parti private dell'operando.

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

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

e si desidera sovraccaricare gli operatori di incremento e decremento per questo, non è possibile farlo come funzioni membro, poiché in C ++ i tipi enum non possono avere funzioni membro. Quindi devi sovraccaricarlo come una funzione gratuita. E operator<() per un modello di classe annidato all'interno di un modello di classe è molto più facile scrivere e leggere quando viene eseguito come una funzione membro incorporata nella definizione di classe. Ma queste sono davvero rare eccezioni.

(Però, Se fai un'eccezione, non dimenticare il problema di const-ness per l'operando che, per le funzioni membro, diventa implicito this discussione. Se l'operatore come una funzione non membro prenderebbe il suo argomento più a sinistra come a const riferimento, lo stesso operatore di una funzione membro deve avere a const alla fine per fare *this un const riferimento.)


Continuare a Operatori comuni da sovraccaricare.


211
2017-12-12 12:49



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

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

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

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

Quanto segue è 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 di un argomento, sono conversioni definite dall'utente. I compilatori concederanno una conversione definita dall'utente quando si tenta di associare una chiamata a una funzione sovraccaricata.

void f(const char*);

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

All'inizio questo sembra molto utile, ma il problema con questo è che la conversione implicita si attiva anche quando non è previsto. Nel seguente codice, void f(const char*)sarà 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 ottengono facilmente questo errore e anche i programmatori C ++ con esperienza sono a volte sorpresi perché il compilatore preleva 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 impliciti di conversione, gli operatori espliciti di conversione non entreranno mai in gioco quando non ci si aspetta che lo facciano. Quanto segue è una classe semplice con un operatore di conversione esplicito:

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

Notare il explicit. Ora quando provi ad eseguire il codice inaspettato dagli operatori di conversione impliciti, ottieni un errore del compilatore:

prog.cpp: nella funzione 'int main ()':
prog.cpp: 15: 18: errore: nessuna funzione di matching per la chiamata a 'f (my_string)'
prog.cpp: 15: 18: nota: i candidati sono:
prog.cpp: 11: 10: nota: 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: nota: void f (const char *)
prog.cpp: 12: 10: nota: nessuna conversione nota per l'argomento 1 da "my_string" a "const char *"

Per richiamare l'operatore di cast esplicito, è necessario utilizzare static_cast, un cast in stile C o un cast in stile costruttore (ad es. T(value) ).

Tuttavia, c'è un'eccezione a questo: il compilatore è autorizzato a convertire implicitamente in bool. Inoltre, il compilatore non può eseguire un'altra conversione implicita dopo la conversione in bool (un compilatore può eseguire 2 conversioni implicite alla volta, ma solo 1 conversione definita dall'utente al massimo).

Perché il compilatore non scriverà "passato" bool, gli operatori di conversione esplicita rimuovono ora la necessità di Idioma Safe Bool. Ad esempio, i puntatori intelligenti prima di C ++ 11 utilizzavano l'idioma Safe Bool per impedire conversioni a tipi interi. In C ++ 11, i puntatori intelligenti utilizzano invece un operatore esplicito perché il compilatore non può convertire implicitamente in un tipo integrale dopo aver convertito esplicitamente un tipo in bool.

Continuare a Sovraccarico new e delete.


143
2018-05-17 18:32



Sovraccarico new e delete

Nota: Questo riguarda solo il sintassi di sovraccarico new e delete, non con il implementazione di tali operatori sovraccarichi. Penso che la semantica del sovraccarico new e delete meritano le loro stesse FAQ, nel tema del sovraccarico dell'operatore non posso mai renderlo giustizia.

Nozioni di base

In C ++, quando scrivi a nuova espressione piace new T(arg) due cose accadono quando questa espressione viene valutata: in primo luogo operator new viene invocato per ottenere la memoria non elaborata e quindi il costruttore appropriato di T è invocato per trasformare questa memoria grezza in un oggetto valido. Allo stesso modo, quando si elimina un oggetto, viene prima chiamato il suo distruttore e quindi viene restituita la memoria operator delete.
C ++ ti permette di mettere a punto entrambe queste operazioni: gestione della memoria e costruzione / distruzione dell'oggetto nella memoria allocata. Quest'ultimo viene fatto scrivendo costruttori e distruttori per una classe. La gestione della memoria fine-tuning viene eseguita scrivendo la tua operator new e operator delete.

La prima delle regole di base del sovraccarico dell'operatore - non farlo - si applica in particolare al sovraccarico new e delete. Quasi gli unici motivi per sovraccaricare questi operatori sono problemi di prestazioni e vincoli di memoriae in molti casi, altre azioni, come modifiche agli algoritmi usato, fornirà molto più alto rapporto costo / guadagno che tentare di modificare la gestione della memoria.

La libreria standard C ++ viene fornita con un set di predefiniti new e delete operatori. 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 una matrice di oggetti. Se fornisci le tue versioni personali, lo faranno non sovraccaricare, ma sostituire quelli della biblioteca standard.
Se sovraccarichi operator new, dovresti sempre sovraccaricare la corrispondenza operator delete, anche se non hai mai intenzione di chiamarlo. Il motivo è che, se un costruttore lancia durante la valutazione di una nuova espressione, il sistema runtime restituirà la memoria al operator delete abbinare il operator new è stato chiamato per allocare la memoria in cui creare l'oggetto. Se non si fornisce una corrispondenza operator delete, viene chiamato quello predefinito, che è quasi sempre sbagliato.
Se sovraccarichi new e delete, dovresti considerare di sovraccaricare anche le varianti dell'array.

Posizionamento new

C ++ consente agli operatori nuovi ed eliminati di prendere ulteriori argomenti.
Il cosiddetto posizionamento nuovo consente di creare un oggetto in un determinato indirizzo 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 gli overload appropriati degli operatori new e delete 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 nuovo di cui sopra, operator delete non viene mai chiamato, a meno che il costruttore di X non lanci un'eccezione.

Puoi anche sovraccaricare new e delete con altri argomenti. Come con l'argomento aggiuntivo per il posizionamento nuovo, questi argomenti sono elencati tra parentesi dopo la parola chiave new. Semplicemente per ragioni storiche, tali varianti vengono spesso chiamate anche posizionamenti nuovi, anche se i loro argomenti non sono per collocare un oggetto in un indirizzo specifico.

Nuovo e specifico della classe

Più comunemente vorrete ottimizzare la gestione della memoria perché la misurazione ha dimostrato che le istanze di una classe specifica o di un gruppo di classi correlate sono create e distrutte spesso e che la gestione della memoria predefinita del sistema run-time, ottimizzata per prestazioni generali, si occupa in modo inefficiente in questo caso specifico. Per migliorare questo, puoi sovraccaricare 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 quindi, new e delete si comportano come funzioni membro statiche. Per oggetti di my_class, il std::size_t argomento sarà sempre sizeof(my_class). Tuttavia, questi operatori sono anche chiamati per oggetti allocati dinamicamente classi derivate, nel qual caso potrebbe essere più grande di quello.

Globale nuovo e cancella

Per sovraccaricare il globale nuovo e cancellare, semplicemente sostituire gli operatori predefiniti della libreria standard con il nostro. Tuttavia, raramente questo deve essere fatto.


130
2017-12-12 13:07



Perché non può operator<< funzione per lo streaming di oggetti in std::cout o in un file è 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 puoi usare:

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

Da operator<< è sovraccarico come funzione membro di Foo, la LHS dell'operatore deve essere a Foo oggetto. Il che significa che ti verrà richiesto di usare:

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

che è molto non 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 utilizzare:

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

che è molto intuitivo.


29
2018-01-22 19:00