L'operatore << dovrebbe essere implementato come amico o come funzione membro?


129

Questa è fondamentalmente la domanda, c'è un modo "giusto" per implementare operator<<? Leggendo questo posso vedere che qualcosa come:

friend bool operator<<(obj const& lhs, obj const& rhs);

è preferito a qualcosa di simile

ostream& operator<<(obj const& rhs);

Ma non riesco a capire perché dovrei usare l'uno o l'altro.

Il mio caso personale è:

friend ostream & operator<<(ostream &os, const Paragraph& p) {
    return os << p.to_str();
}

Ma probabilmente potrei fare:

ostream & operator<<(ostream &os) {
    return os << paragraph;
}

Su quale logica devo basare questa decisione?

Nota :

 Paragraph::to_str = (return paragraph) 

dove il paragrafo è una stringa.


4
BTW probabilmente dovresti aggiungere const alle firme delle funzioni membro
Motti

4
Perché restituire bool dall'operatore <<? Lo stai usando come operatore di flusso o come sovraccarico dello spostamento bit per bit?
Martin York

Risposte:


120

Il problema qui è nella tua interpretazione dell'articolo che colleghi .

Uguaglianza

Questo articolo riguarda qualcuno che ha problemi a definire correttamente gli operatori di relazione bool.

L'operatore:

  • Uguaglianza == e! =
  • Relazione <> <=> =

Questi operatori dovrebbero restituire un valore bool in quanto confrontano due oggetti dello stesso tipo. Di solito è più semplice definire questi operatori come parte della classe. Questo perché una classe è automaticamente amica di se stessa, quindi gli oggetti di tipo Paragraph possono esaminarsi a vicenda (anche i membri privati ​​degli altri).

C'è un argomento per rendere queste funzioni indipendenti in quanto ciò consente alla conversione automatica di convertire entrambi i lati se non sono dello stesso tipo, mentre le funzioni membro consentono solo la conversione automatica delle rhs. Trovo che questo sia un argomento da uomo di carta perché non vuoi davvero che la conversione automatica avvenga in primo luogo (di solito). Ma se questo è qualcosa che desideri (non lo consiglio), rendere i comparatori indipendenti può essere vantaggioso.

Streaming

Gli operatori di flusso:

  • operatore << output
  • operatore >> input

Quando li usi come operatori di flusso (piuttosto che spostamento binario) il primo parametro è un flusso. Poiché non hai accesso all'oggetto stream (non è tuo da modificare), questi non possono essere operatori membri, devono essere esterni alla classe. Quindi devono essere amici della classe o avere accesso a un metodo pubblico che eseguirà lo streaming per te.

È anche tradizionale che questi oggetti restituiscano un riferimento a un oggetto flusso in modo da poter concatenare le operazioni del flusso insieme.

#include <iostream>

class Paragraph
{
    public:
        explicit Paragraph(std::string const& init)
            :m_para(init)
        {}

        std::string const&  to_str() const
        {
            return m_para;
        }

        bool operator==(Paragraph const& rhs) const
        {
            return m_para == rhs.m_para;
        }
        bool operator!=(Paragraph const& rhs) const
        {
            // Define != operator in terms of the == operator
            return !(this->operator==(rhs));
        }
        bool operator<(Paragraph const& rhs) const
        {
            return  m_para < rhs.m_para;
        }
    private:
        friend std::ostream & operator<<(std::ostream &os, const Paragraph& p);
        std::string     m_para;
};

std::ostream & operator<<(std::ostream &os, const Paragraph& p)
{
    return os << p.to_str();
}


int main()
{
    Paragraph   p("Plop");
    Paragraph   q(p);

    std::cout << p << std::endl << (p == q) << std::endl;
}

19
Perché il operator<< private:?
Matt Clarkson

47
@ MattClarkson: non è così. È una dichiarazione di funzione amico quindi non fa parte della classe e quindi non è influenzata dagli specificatori di accesso. In genere metto le dichiarazioni della funzione amico accanto ai dati a cui accedono.
Martin York

12
Perché deve essere una funzione amichevole, se utilizzi la funzione pubblica per accedere ai dati? Scusa, se la domanda è stupida.
Semyon Danilov

4
@SemyonDanilov: Perché dovresti rompere l'incapsulamento e aggiungere getter! freiendè un modo per estendere l'interfaccia pubblica senza interrompere l'incapsulamento. Leggi programmers.stackexchange.com/a/99595/12917
Martin York

3
@LokiAstari Ma sicuramente questo è un argomento per rimuovere to_str o renderlo privato. Allo stato attuale, l'operatore di streaming non deve essere un amico, poiché utilizza solo funzioni pubbliche.
deworde

53

Non puoi farlo come funzione membro, perché il thisparametro implicito è il lato sinistro <<dell'operatore-. (Quindi, dovresti aggiungerlo come funzione membro alla ostream-class. Non va bene :)

Potresti farlo come una funzione gratuita senza friending? Questo è quello che preferisco, perché chiarisce che si tratta di un'integrazione con ostreame non di una funzionalità fondamentale della tua classe.


1
"non è una funzionalità fondamentale della tua classe." Questo è ciò che significa "amico". Se fosse funzionalità di base, sarebbe nella classe, non in un amico.
xaxxon

1
@xaxxon Penso che la mia prima frase spieghi perché in questo caso sarebbe impossibile aggiungere la funzione come funzione membro. Una friendfunzione ha gli stessi diritti di una funzione membro ( questo è ciò che friendsignifica), quindi come utente della classe, dovrei chiedermi perché ne avrebbe bisogno. Questa è la distinzione che sto cercando di fare con la formulazione "funzionalità di base".
Magnus Hoff

32

Se possibile, come funzioni non membro e non amico.

Come descritto da Herb Sutter e Scott Meyers, preferisci le funzioni non membro non amiche alle funzioni membro, per aumentare l'incapsulamento.

In alcuni casi, come gli stream C ++, non avrai scelta e dovrai usare funzioni non membri.

Tuttavia, ciò non significa che devi rendere queste funzioni amiche delle tue classi: queste funzioni possono ancora accedere alla tua classe tramite le funzioni di accesso alle classi. Se riesci a scrivere quelle funzioni in questo modo, hai vinto.

Informazioni sui prototipi << e >> dell'operatore

Credo che gli esempi che hai fornito nella tua domanda siano sbagliati. Per esempio;

ostream & operator<<(ostream &os) {
    return os << paragraph;
}

Non riesco nemmeno a pensare a come potrebbe funzionare questo metodo in un flusso.

Ecco i due modi per implementare gli operatori << e >>.

Supponiamo che tu voglia utilizzare un oggetto simile a un flusso di tipo T.

E che vuoi estrarre / inserire da / in T i dati rilevanti del tuo oggetto di tipo Paragrafo.

Prototipi di funzione operatore generico << e >>

Il primo è come funzioni:

// T << Paragraph
T & operator << (T & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// T >> Paragraph
T & operator >> (T & p_oInputStream, const Paragraph & p_oParagraph)
{
   // do the extraction of p_oParagraph
   return p_oInputStream ;
}

Operatore generico << e >> prototipi di metodi

Il secondo è come metodi:

// T << Paragraph
T & T::operator << (const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return *this ;
}

// T >> Paragraph
T & T::operator >> (const Paragraph & p_oParagraph)
{
   // do the extraction of p_oParagraph
   return *this ;
}

Nota che per usare questa notazione, devi estendere la dichiarazione di classe di T. Per gli oggetti STL, questo non è possibile (non dovresti modificarli ...).

E se T fosse uno stream C ++?

Di seguito sono riportati i prototipi degli stessi operatori << e >> per i flussi C ++.

Per basic_istream e basic_ostream generici

Nota che è il caso dei flussi, poiché non puoi modificare il flusso C ++, devi implementare le funzioni. Che significa qualcosa come:

// OUTPUT << Paragraph
template <typename charT, typename traits>
std::basic_ostream<charT,traits> & operator << (std::basic_ostream<charT,traits> & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// INPUT >> Paragraph
template <typename charT, typename traits>
std::basic_istream<charT,traits> & operator >> (std::basic_istream<charT,traits> & p_oInputStream, const CMyObject & p_oParagraph)
{
   // do the extract of p_oParagraph
   return p_oInputStream ;
}

Per char istream e ostream

Il codice seguente funzionerà solo per i flussi basati su caratteri.

// OUTPUT << A
std::ostream & operator << (std::ostream & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// INPUT >> A
std::istream & operator >> (std::istream & p_oInputStream, const Paragraph & p_oParagraph)
{
   // do the extract of p_oParagraph
   return p_oInputStream ;
}

Rhys Ulerich ha commentato sul fatto che il codice basato sui caratteri non è che una "specializzazione" del codice generico sopra di esso. Ovviamente Rhys ha ragione: non consiglio l'uso dell'esempio basato sui caratteri. Viene fornito qui solo perché è più semplice da leggere. Poiché è fattibile solo se lavori solo con flussi basati su caratteri, dovresti evitarlo su piattaforme in cui il codice wchar_t è comune (ad esempio su Windows).

Spero che questo ti aiuti.


Il tuo codice generico basic_istream e basic_ostream non copre già le versioni specifiche di std :: ostream- e std :: istream poiché le ultime due sono solo istanze del primo usando i caratteri?
Rhys Ulerich

@Rhys Ulerich: Certamente. Uso solo la versione generica, basata su modelli, se non altro perché su Windows devi gestire sia il codice char che il codice wchar_t. L'unico merito della seconda versione è di apparire più semplice della prima. Chiarirò il mio post a riguardo.
paercebal

10

Dovrebbe essere implementato come funzioni gratuite e non amichevoli, soprattutto se, come la maggior parte delle cose in questi giorni, l'output viene utilizzato principalmente per la diagnostica e il log. Aggiungi le funzioni di accesso const per tutte le cose che devono essere inserite nell'output, quindi fai in modo che l'outputter le chiami ed esegua la formattazione.

In realtà ho iniziato a raccogliere tutte queste funzioni libere di output di ostream in un file di intestazione e implementazione "ostreamhelpers", che mantiene quella funzionalità secondaria lontana dal vero scopo delle classi.


7

La firma:

bool operator<<(const obj&, const obj&);

Sembra piuttosto sospetto, questo non si adatta alla streamconvenzione né alla convenzione bit per bit, quindi sembra un caso di abuso di sovraccarico dell'operatore, operator <dovrebbe tornare boolma operator <<probabilmente dovrebbe restituire qualcos'altro.

Se intendevi così, dì:

ostream& operator<<(ostream&, const obj&); 

Quindi poiché non è possibile aggiungere funzioni per ostreamnecessità, la funzione deve essere una funzione libera, indipendentemente dal fatto che frienddipenda o meno da ciò a cui deve accedere (se non ha bisogno di accedere a membri privati ​​o protetti non c'è bisogno di farlo amico).


Vale la pena ricordare che l'accesso alla modifica ostreamsarebbe richiesto quando si utilizza l' ostream.operator<<(obj&)ordine; da qui la funzione free. In caso contrario, il tipo di utente deve essere un tipo a vapore per consentire l'accesso.
wulfgarpro

2

Solo per amor di completamento, vorrei aggiungere che è davvero possibile creare un operatoreostream& operator << (ostream& os) all'interno di una classe e può funzionare. Da quello che so non è una buona idea usarlo, perché è molto contorto e poco intuitivo.

Supponiamo di avere questo codice:

#include <iostream>
#include <string>

using namespace std;

struct Widget
{
    string name;

    Widget(string _name) : name(_name) {}

    ostream& operator << (ostream& os)
    {
        return os << name;
    }
};

int main()
{
    Widget w1("w1");
    Widget w2("w2");

    // These two won't work
    {
        // Error: operand types are std::ostream << std::ostream
        // cout << w1.operator<<(cout) << '\n';

        // Error: operand types are std::ostream << Widget
        // cout << w1 << '\n';
    }

    // However these two work
    {
        w1 << cout << '\n';

        // Call to w1.operator<<(cout) returns a reference to ostream&
        w2 << w1.operator<<(cout) << '\n';
    }

    return 0;
}

Quindi, per riassumere, puoi farlo, ma molto probabilmente non dovresti :)


0

operatore amico = pari diritti come classe

friend std::ostream& operator<<(std::ostream& os, const Object& object) {
    os << object._atribute1 << " " << object._atribute2 << " " << atribute._atribute3 << std::endl;
    return os;
}

0

operator<< implementato come funzione amico:

#include <iostream>
#include <string>
using namespace std;

class Samp
{
public:
    int ID;
    string strName; 
    friend std::ostream& operator<<(std::ostream &os, const Samp& obj);
};
 std::ostream& operator<<(std::ostream &os, const Samp& obj)
    {
        os << obj.ID<<   << obj.strName;
        return os;
    }

int main()
{
   Samp obj, obj1;
    obj.ID = 100;
    obj.strName = "Hello";
    obj1=obj;
    cout << obj <<endl<< obj1;

} 

USCITA:
100 Ciao
100 Ciao

Questa può essere una funzione amico solo perché l'oggetto si trova a destra operator<<e l'argomento couta sinistra. Quindi questa non può essere una funzione membro della classe, può essere solo una funzione amico.


non penso che ci sia un modo per scrivere questo come funzione membro !!
Rohit Vipin Mathews

Perché è tutto audace. Lasciami rimuovere questo.
Sebastian Mach
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.