¿Debería el operador << implementarse como un amigo o como una función miembro?

129

Esa es básicamente la pregunta, ¿hay una forma "correcta" de implementar operator<<? Al leer esto , puedo ver que algo así como:

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

se prefiere a algo como

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

Pero no puedo entender por qué debería usar uno u otro.

Mi caso personal es:

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

Pero probablemente podría hacer:

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

¿En qué razón debo basar esta decisión?

Nota :

 Paragraph::to_str = (return paragraph) 

donde el párrafo es una cadena

Federico Builes
fuente
44
Por cierto, probablemente debería agregar constantes a las firmas de las funciones miembro
Motti
44
¿Por qué devolver bool del operador <<? ¿Lo está utilizando como operador de flujo o como sobrecarga del desplazamiento bit a bit?
Martin York

Respuestas:

120

El problema aquí está en su interpretación del artículo que vincula .

Igualdad

Este artículo trata sobre alguien que tiene problemas para definir correctamente los operadores de relación bool.

El operador:

  • Igualdad == y! =
  • Relación <> <=> =

Estos operadores deberían devolver un bool ya que están comparando dos objetos del mismo tipo. Por lo general, es más fácil definir estos operadores como parte de la clase. Esto se debe a que una clase es automáticamente amiga de sí misma, por lo que los objetos de tipo Párrafo pueden examinarse entre sí (incluso entre los miembros privados).

Existe un argumento para realizar estas funciones independientes, ya que esto permite que la conversión automática convierta ambos lados si no son del mismo tipo, mientras que las funciones miembro solo permiten que los rhs se conviertan automáticamente. Creo que este es un argumento de hombre de papel, ya que en realidad no desea que la conversión automática ocurra en primer lugar (generalmente). Pero si esto es algo que desea (no lo recomiendo), hacer que los comparadores estén libres puede ser ventajoso.

Transmisión

Los operadores de flujo:

  • operador << salida
  • operador >> entrada

Cuando los utiliza como operadores de flujo (en lugar de desplazamiento binario), el primer parámetro es un flujo. Como no tiene acceso al objeto de flujo (no es suyo modificarlo), estos no pueden ser operadores miembros, deben ser externos a la clase. Por lo tanto, deben ser amigos de la clase o tener acceso a un método público que haga la transmisión por usted.

También es tradicional que estos objetos devuelvan una referencia a un objeto de flujo para que pueda encadenar las operaciones de flujo juntas.

#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;
}
Martin York
fuente
19
¿Por qué es el operator<< private:?
Matt Clarkson el
47
@ MattClarkson: No lo es. Es una declaración de función amiga, por lo que no forma parte de la clase y, por lo tanto, no se ve afectada por los especificadores de acceso. Generalmente pongo las declaraciones de la función de amigo junto a los datos a los que acceden.
Martin York
12
¿Por qué tiene que ser una función amigable, si está utilizando una función pública para acceder a los datos? Lo siento, si la pregunta es estúpida.
Semyon Danilov
44
@SemyonDanilov: ¿Por qué rompería la encapsulación y agregaría getters? freiendes una forma de extender la interfaz pública sin romper la encapsulación. Lea programmers.stackexchange.com/a/99595/12917
Martin York el
3
@LokiAstari Pero seguramente ese es un argumento para eliminar to_str o hacerlo privado. Tal como está, el operador de transmisión no tiene que ser un amigo, ya que solo utiliza funciones públicas.
deworde
53

No puede hacerlo como una función miembro, porque el thisparámetro implícito es el lado izquierdo del <<operador. (Por lo tanto, necesitaría agregarlo como una función miembro a la ostreamclase. No es bueno :)

¿Podrías hacerlo como una función libre sin friendusarlo? Eso es lo que prefiero, porque deja en claro que se trata de una integración ostreamy no una funcionalidad central de su clase.

Magnus Hoff
fuente
1
"No es una funcionalidad central de su clase". Eso es lo que significa "amigo". Si fuera la funcionalidad principal, estaría en la clase, no en un amigo.
xaxxon
1
@xaxxon Creo que mi primera oración explica por qué sería imposible en este caso agregar la función como función miembro. Una friendfunción tiene los mismos derechos que una función miembro ( esto es lo que friendsignifica), por lo que como usuario de la clase, tendría que preguntarme por qué necesitaría eso. Esta es la distinción que estoy tratando de hacer con la frase "funcionalidad principal".
Magnus Hoff
32

Si es posible, como no miembro y no amigo funciones.

Como lo describieron Herb Sutter y Scott Meyers, prefieren las funciones no miembros no amigos a las funciones miembros, para ayudar a aumentar la encapsulación.

En algunos casos, como las transmisiones de C ++, no tendrá la opción y deberá usar funciones que no sean miembros.

Pero aún así, no significa que tenga que hacer que estas funciones sean amigas de sus clases: estas funciones aún pueden acceder a su clase a través de sus accesos de clase. Si logra escribir esas funciones de esta manera, entonces ganó.

Sobre el operador << y >> prototipos

Creo que los ejemplos que dio en su pregunta están equivocados. Por ejemplo;

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

Ni siquiera puedo empezar a pensar cómo podría funcionar este método en una secuencia.

Estas son las dos formas de implementar los operadores << y >>.

Digamos que desea utilizar un objeto similar a una secuencia del tipo T.

Y que desea extraer / insertar de / en T los datos relevantes de su objeto de tipo Párrafo.

Operador genérico << y >> prototipos de funciones

El primer ser como funciones:

// 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 ;
}

Operador genérico << y >> prototipos de métodos

El segundo ser como métodos:

// 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 ;
}

Tenga en cuenta que para usar esta notación, debe extender la declaración de clase de T. Para los objetos STL, esto no es posible (se supone que no debe modificarlos ...).

¿Y si T es un flujo de C ++?

Aquí están los prototipos de los mismos operadores << y >> para flujos C ++.

Para genérico basic_istream y basic_ostream

Tenga en cuenta que es el caso de las secuencias, ya que no puede modificar la secuencia de C ++, debe implementar las funciones. Lo que significa algo como:

// 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 ;
}

Para char istream y ostream

El siguiente código funcionará solo para transmisiones basadas en caracteres.

// 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 comentó sobre el hecho de que el código basado en caracteres no es más que una "especialización" del código genérico sobre él. Por supuesto, Rhys tiene razón: no recomiendo el uso del ejemplo basado en char. Solo se da aquí porque es más fácil de leer. Como solo es viable si solo trabaja con secuencias basadas en char, debe evitarlo en plataformas donde el código wchar_t es común (es decir, en Windows).

Espero que esto ayude.

paercebal
fuente
¿Su código genérico basic_istream y basic_ostream ya no cubre las versiones específicas de std :: ostream- y std :: istream ya que las dos últimas son solo instancias de las primeras que usan caracteres?
Rhys Ulerich
@Rhys Ulerich: Por supuesto. Solo uso la versión genérica con plantilla, aunque solo sea porque en Windows, tienes que lidiar con el código char y wchar_t. El único mérito de la segunda versión es aparecer como más simple que la primera. Aclararé mi publicación sobre eso.
paercebal
10

Debe implementarse como una función gratuita y no amiga, especialmente si, como la mayoría de las cosas en estos días, la salida se utiliza principalmente para el diagnóstico y el registro. Agregue accesos constantes para todas las cosas que necesitan ir a la salida, y luego haga que el emisor solo llame a esos y realice el formateo.

De hecho, he decidido recopilar todas estas funciones libres de salida de ostream en un encabezado "ostreamhelpers" y un archivo de implementación, mantiene esa funcionalidad secundaria lejos del propósito real de las clases.

XPav
fuente
7

La firma:

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

Parece bastante sospechoso, esto no se ajusta a la streamconvención ni a la convención bit a bit, por lo que parece un caso de abuso de sobrecarga del operador, operator <debería regresar boolpero operator <<probablemente debería devolver algo más.

Si quisiste decir eso, di:

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

Entonces, dado que no puede agregar funciones ostreamnecesariamente, la función debe ser una función libre, ya sea que frienddependa o no de a qué tiene acceso (si no necesita acceder a miembros privados o protegidos, no es necesario hacerlo) amigo).

Motti
fuente
Vale la pena mencionar ostreamque se requeriría acceso para modificar cuando se utiliza el ostream.operator<<(obj&)pedido; De ahí la función libre. De lo contrario, el tipo de usuario debe ser un tipo de vapor para acomodar el acceso.
wulfgarpro
2

Solo por completar, me gustaría agregar que de hecho puedes crear un operador ostream& operator << (ostream& os)dentro de una clase y puede funcionar. Por lo que sé, no es una buena idea usarlo, porque es muy complicado y poco intuitivo.

Supongamos que tenemos este código:

#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;
}

En resumen, puedes hacerlo, pero probablemente no deberías :)

ashrasmun
fuente
0

operador amigo = igualdad de derechos como clase

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

operator<< implementado como una función amiga:

#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;

} 

SALIDA:
100 Hola
100 Hola

Esta puede ser una función amiga solo porque el objeto está en el lado derecho operator<<y el argumento coutestá en el lado izquierdo. Entonces, esta no puede ser una función miembro de la clase, solo puede ser una función amiga.

Rohit Vipin Mathews
fuente
¡No creo que haya una manera de escribir esto como una función para miembros!
Rohit Vipin Mathews
¿Por qué todo es audaz? Déjame eliminar esto.
Sebastian Mach