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




overloadable c++ (5)

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 al momento in cui sono state fornite, ecco un indice delle risposte nell'ordine in cui hanno più senso:

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


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'utente 1 . 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 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 suffisso)
  • manipolazione bit: & | ^ << >> e &= |= ^= <<= >>= (tutto infisso binario); ~ (prefisso unario)
  • algebra booleana: == != < > || >= || && (all infix binario); ! (prefisso unario)
  • gestione della memoria: new new[] delete delete[]
  • operatori impliciti di conversione
  • miscellanea: = [] -> ->* , (tutti i binari infissi); * & (tutto prefisso unario) () (chiamata funzione, n-ary infix)

Tuttavia, il fatto che tu possa sovraccaricare tutti questi non significa che dovresti farlo. Vedi le regole di base del sovraccarico dell'operatore.

In C ++, gli operatori sono sovraccarichi sotto forma di funzioni con nomi speciali . Come con altre funzioni, gli operatori sovraccaricati possono generalmente essere implementati come una funzione membro del tipo del loro operando sinistro o come funzioni non membro . 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 [email protected](x) o come [email protected]() . Un operatore binario infisso @ , applicato agli oggetti x e y , viene chiamato o come [email protected](x,y) o come [email protected](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 è trattato in una parte successiva di questa 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-ario deve sempre essere implementato come funzione membro.

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


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

Quando si tratta di sovraccaricare l'operatore in C ++, ci sono tre regole base da 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 evidenza 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 l'operatore binario + per sottrarre dal suo operando di destra. Tuttavia, gli utenti di un tale operatore non sospetterebbero mai l'espressione a + b di sottrarre a da 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 loro e ad altre operazioni. Se il tuo tipo supporta a + b , gli utenti si aspettano di poter chiamare anche a += b . Se supporta il prefisso incremento ++a , si aspettano che anche a++ funzioni. Se riescono a verificare se a < b , sicuramente si aspettano anche di essere in grado di verificare se a > b . Se riescono a copiare-costruire il tuo tipo, si aspettano che anche l'incarico funzioni.

Continuare alla decisione tra membro e non membro .


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*) 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 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 l' explicit . Ora quando provi ad eseguire il codice inaspettato dagli operatori di conversione impliciti, ottieni un errore del compilatore:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Per richiamare l'operatore di cast esplicito, devi usare static_cast , un cast in stile C o un cast di stile costruttore (cioè T(value) ).

Tuttavia, c'è un'eccezione: il compilatore può convertire implicitamente in bool . Inoltre, al compilatore non è consentito 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).

Poiché il compilatore non eseguirà il cast "passato", gli operatori di conversione esplicita rimuoveranno la necessità dell'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.

Continua su Sovraccarico new ed delete .


Sovraccarico new e delete

Nota: questo riguarda solo la sintassi di overloading new e delete , non con l' implementazione di tali operatori sovraccaricati. Penso che la semantica del sovraccarico new e delete meriti le proprie FAQ , nel tema dell'overloading dell'operatore non posso mai renderlo giustizia.

Nozioni di base

In C ++, quando scrivi una nuova espressione come new T(arg) due cose accadono quando viene valutata questa espressione: il primo operator new viene invocato per ottenere la memoria non elaborata, e quindi viene richiamato il costruttore appropriato di T per trasformare questa memoria raw in una oggetto valido. Allo stesso modo, quando si elimina un oggetto, viene prima chiamato il suo distruttore, quindi la memoria viene restituita 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 di fine-tuning viene eseguita scrivendo il proprio operator new e operator delete .

La prima delle regole di base dell'overloading dell'operatore - non farlo - si applica in particolare al sovraccarico di new e delete . Quasi gli unici motivi 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 costo / guadagno molto più elevato rispetto al tentativo di modificare la gestione della memoria.

La libreria standard C ++ viene fornita con una serie di operatori predefiniti new ed 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 una matrice di oggetti. Se fornite le vostre versioni personali, non sovraccaricheranno, ma sostituiranno quelle della libreria standard.
Se sovraccarichi l' operator new , dovresti sempre sovraccaricare anche l' operator delete corrispondente, anche se non hai intenzione di chiamarlo. Il motivo è che, se un costruttore lancia durante la valutazione di una nuova espressione, il sistema di runtime restituirà la memoria operator delete corrispondenza con l' operator new che è stato chiamato per allocare la memoria per creare l'oggetto. Se lo si fa non fornire una operator delete corrispondente, viene chiamato quello predefinito, che è quasi sempre sbagliato.
Se sovraccarichi new ed 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 indicato sopra, operator delete non viene mai chiamata, a meno che il costruttore di X non lanci un'eccezione.

Puoi anche sovraccaricare new ed 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 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.

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.


Perché non può operator<<funzionare per lo streaming di oggetti in std::couto su un file 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 puoi usare:

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

Poiché operator<<è sovraccarico come funzione membro di Foo, l'LHS dell'operatore deve essere un Foooggetto. 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.





c++-faq