6

Informazioneaffettare e l'overloading degli operatori in C++

Sono stato di programmazione in Java per un po ', e ho acceso solo verso C++ solo pochi mesi fa, quindi mi scuso se la risposta è solo una cosa stupida che mi mancava! Ora che è stato detto tutto, è giunto il momento di affrontare il problema! Sto sviluppando un motore di gioco di base basato sul testo e di recente mi sono imbattuto in un problema particolarmente specifico e improbabile. Ho provato a testarlo su una scala più piccola nel programma qui sotto, e ho deciso di mostrare solo questo (al contrario del mio codice di gioco effettivo), in modo da non soffocare lo schermo e rendere il problema meno complicato. Il problema modellato di seguito rispecchia il problema con il mio codice effettivo, solo senza i distrattori soffici.

Il problema

Essenzialmente il problema è quello di polimorfismo. Voglio sovraccaricare l'operatore di uscita "< <" per fungere da una funzione di visualizzazione che è univoca per ogni oggetto nella gerarchia. Il problema è che quando chiamo questo operatore da un elenco che memorizza questi membri della gerarchia, perdono la loro identità e chiamano l'operatore di output della classe base. Normalmente, si risolverebbe questo sostituendo i sovraccarichi dell'operatore con un semplice metodo di visualizzazione, contrassegnando il metodo di visualizzazione virtuale e procedendo con il loro felice giorno. Non mi interessa particolarmente apportare questa modifica al codice, ma ora sono semplicemente curioso. C'è un modo per sovraccaricare gli operatori in una gerarchia che risulta in ciò che sto andando qui?

La [Esempio] Codice

#include <vector> 
#include <iostream> 
#include <string> 

using namespace std; 

class Polygon { 
    friend ostream& operator<<(ostream& os, const Polygon& p); 
public: 

private: 

}; 


class Rectangle : public Polygon { 
    friend ostream& operator<<(ostream& os, const Rectangle& r); 
public: 

private: 

}; 


class Square : public Rectangle { 
    friend ostream& operator<<(ostream& os, const Square& s); 
public: 

private: 

}; 

ostream& operator<<(ostream& os, const Polygon& p) { 
    os << "Polygon!" << endl; 
    return os; 
} 
ostream& operator<<(ostream& os, const Rectangle& r) { 
    os << "Rectangle!" << endl; 
    return os; 
} 
ostream& operator<<(ostream& os, const Square& s) { 
    os << "Square!" << endl; 
    return os; 
} 


int main() { 
    vector<Polygon*> listOfPoly; 
    listOfPoly.push_back(new Polygon()); 
    listOfPoly.push_back(new Rectangle()); 
    listOfPoly.push_back(new Square()); 

    for(Polygon* p : listOfPoly) { 
     cout << *p; 
    } 
} 

uscita per [Esempio] Codice

Polygon! 
Polygon! 
Polygon! 

uscita desiderata per [Esempio] Codice

Polygon! 
Rectangle! 
Square! 
+2

A proposito, questa è una grande domanda. La maggior parte dei neofiti fa domande così schifose. – Puppy

+0

come 'operatore <<' è un amico (e non un membro), la risposta breve è no, è necessario un operatore di visualizzazione interno contrassegnato come virtuale – vsoftco

+0

Dato che sei un programmatore Java, sai che si tratta di una perdita di memoria in C++ a meno che tu non abbia deallocato la memoria: 'listOfPoly.push_back (new Polygon()); ...'Preferisco usare puntatori intelligenti invece. – PaulMcKenzie

risposta

5

C'è un modo per sovraccaricare gli operatori in una gerarchia che risulta in quello che sto andando qui?

No.

Il problema è che gli operatori non sono in gerarchia. La parola chiave friend qui avanti - dichiara una funzione gratuita e fornisce l'accesso privilegiato alla classe.Non lo rende un metodo, quindi non può essere virtuale.


Si noti che il sovraccarico dell'operatore è solo zucchero sintattico. L'espressione

os << shape; 

può o risolvere con la funzione libero (come avete qui)

ostream& operator<< (ostream&, Polygon&); 

o un membro della operando di sinistra, come ad esempio

ostream& ostream::operator<<(Polygon&); 

(ovviamente qui il secondo caso non esiste, perché dovresti modificare std::ostream). Ciò che la sintassi non può risolvere è un membro dell'operando di destra.

Quindi, è possibile avere un operatore di funzione gratuito (che è necessariamente non virtuale) o un metodo sull'operando di sinistra (che potrebbe essere essere virtuale), ma non un metodo sull'operando di destra.


La soluzione più comune è quella di avere un unico sovraccarico per il primo livello della gerarchia che invia ad un metodo virtuale. Quindi:

class Polygon { 
public: 
    virtual ostream& format(ostream&); 
}; 

ostream& operator<<(ostream& os, const Polygon& p) { 
    return p.format(os); 
} 

Ora basta implementare Polygon::format, e sovrascrivere nelle classi derivate.


Per inciso, utilizzando friend porta un odore di codice comunque. In generale è considerato uno stile migliore per fornire alla tua classe un'interfaccia pubblica sufficientemente completa che il codice esterno non ha bisogno di accesso privilegiato per lavorare con esso.

Ulteriori digressione per informazioni di fondo: multiple dispatch è una cosa, e C++ risoluzione di sovraccarico gestisce bene quando tutti i tipi di argomenti sono noti staticamente. Ciò che non viene gestito è la scoperta del tipo dinamico di ciascun argomento in fase di esecuzione e quindi che tenta di trovare il sovraccarico migliore (che, se si considerano più gerarchie di classi, è ovviamente non banale).

Se si sostituisce l'elenco polimorfico di runtime con una tupla polimorfica in fase di compilazione e si itera su tale, lo schema di sovraccarico originale verrà inviato correttamente.

+1

Per inciso, tale stream-inserter può essere una funzione 'inline'' friend'. (E non c'è ragione per cui non dovrebbe esserlo, a meno che non sia necessario per niente una funzione di "amico"). – Deduplicator

+0

'friend's non sono affatto odori di codice. È vero che 'friend' non è la funzione di linguaggio più comune da utilizzare, ma questo è un esempio di quando è abbastanza giustificato. – Puppy

3

L'operatore non è un membro virtuale. Ciò significa che è impossibile che invii alla classe derivata. Solo le funzioni virtuali possono essere inviate in modo dinamico. Una strategia tipica in questo scenario è la creazione di un normale operatore che invia una funzione virtuale all'interfaccia per eseguire il lavoro.

Tra l'altro, come bonus aggiuntivo, new è una funzionalità di linguaggio C++ praticamente inutile. Devi scoprire puntatori intelligenti, altrimenti ogni riga di codice che scrivi dovrai solo riscrivere per infiniti problemi di durata.

Rendere gli operatori virtuali è una pessima idea, di solito. Questo perché è possibile inviare solo in modo dinamico su this ma gli operatori sono frequentemente utilizzati con il tipo implementandoli sul RHS o come non membri. Gli overload degli operatori non membri sono più potenti dei membri.

+0

si utilizza 'new' in un'inizializzazione del puntatore intelligente;) – vsoftco

+2

No, si utilizza' make_unique' o 'make_shared'. – Puppy

+0

Ha ha, lo riprendo, ottima risposta, giusto :) – vsoftco

1

È possibile aggiungere una funzione Display virtuale() nella classe base Rectangle. Ogni classe nella gerarchia può sovrascrivere la funzione e implementarla in modo diverso.

È quindi necessario definire un solo operatore < < che accetta come parametro un Poligono &. La funzione stessa chiama semplicemente la funzione di visualizzazione virtuale.

class Polygon 
{ 
public:  
    virtual void Display(ostream& os) const 
    { 
     os << "Polygon" << endl; 
    } 
}; 

class Rectangle : public Polygon 
{ 
public: 
    virtual void Display(ostream&os) const override 
    { 
     os << "Rectangle" << endl; 
    } 
}; 

ostream& operator<<(ostream& os, const Polygon& p) 
{ 
    p.Display(os); 
    return os; 
}