O operador << deve ser implementado como amigo ou como membro?

129

Essa é basicamente a questão: existe uma maneira "correta" de implementar operator<<? Lendo isso , vejo que algo como:

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

é preferível a algo como

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

Mas não consigo entender por que devo usar um ou outro.

Meu caso pessoal é:

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

Mas eu provavelmente poderia fazer:

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

Em que justificativa devo basear essa decisão?

Nota :

 Paragraph::to_str = (return paragraph) 

onde o parágrafo é uma string.

Federico Builes
fonte
4
BTW, você provavelmente deve adicionar const às assinaturas das funções de membro
Motti
4
Por que retornar bool do operador <<? Você o está usando como um operador de fluxo ou como uma sobrecarga do deslocamento bit a bit?
Martin York

Respostas:

120

O problema aqui está na sua interpretação do artigo que você vincula .

Igualdade

Este artigo é sobre alguém que está tendo problemas para definir corretamente os operadores de relacionamento bool.

O operador:

  • Igualdade == e! =
  • Relacionamento <> <=> =

Esses operadores devem retornar um bool, pois estão comparando dois objetos do mesmo tipo. Geralmente é mais fácil definir esses operadores como parte da classe. Isso ocorre porque uma classe é automaticamente amiga de si mesma, portanto objetos do tipo Parágrafo podem se examinar (até mesmo os membros privados).

Há um argumento para tornar essas funções independentes, pois isso permite que a conversão automática converta ambos os lados se não forem do mesmo tipo, enquanto as funções de membro permitem apenas que os rhs sejam convertidos automaticamente. Acho isso um argumento do homem do papel, pois você realmente não quer que a conversão automática aconteça em primeiro lugar (geralmente). Mas se isso é algo que você deseja (eu não recomendo), tornar os comparadores independentes pode ser vantajoso.

Transmissão

Os operadores de fluxo:

  • operador << saída
  • operador >> entrada

Quando você os usa como operadores de fluxo (em vez de deslocamento binário), o primeiro parâmetro é um fluxo. Como você não tem acesso ao objeto de fluxo (não é seu para modificar), eles não podem ser operadores membros; eles precisam ser externos à classe. Portanto, eles devem ser amigos da classe ou ter acesso a um método público que fará o streaming para você.

Também é tradicional que esses objetos retornem uma referência a um objeto de fluxo para que você possa encadear operações de fluxo juntos.

#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
fonte
19
Por que é o operator<< private:?
Matt Clarkson
47
@ MattClarkson: Não é. Sua declaração de função de amigo, portanto, não faz parte da classe e, portanto, não é afetada pelos especificadores de acesso. Geralmente, coloco as declarações de função de amigo próximas aos dados que eles acessam.
Martin York
12
Por que precisa ser uma função amigável, se você estiver usando uma função pública para acessar dados? Desculpe, se a pergunta é estúpida.
Semyon Danilov 29/03
4
@SemyonDanilov: Por que você quebraria o encapsulamento e adicionaria getters! freiendé uma maneira de estender a interface pública sem interromper o encapsulamento. Leia programmers.stackexchange.com/a/99595/12917
Martin York
3
@LokiAstari Mas certamente esse é um argumento para remover o to_str ou torná-lo privado. Tal como está, o operador de streaming não precisa ser um amigo, pois usa apenas funções públicas.
deworde
53

Você não pode fazer isso como uma função de membro, porque o thisparâmetro implícito é o lado esquerdo do <<operador. (Portanto, você precisaria adicioná-lo como uma função de membro à ostreamclasse-. Não é bom :)

Você poderia fazê-lo como uma função livre sem friendusá-lo? É isso que eu prefiro, porque deixa claro que essa é uma integração ostreame não uma funcionalidade principal da sua classe.

Magnus Hoff
fonte
1
"não é uma funcionalidade essencial da sua turma." É isso que "amigo" significa. Se fosse a funcionalidade principal, seria da classe, não um amigo.
Xaxxon
1
@xaxxon Acho que minha primeira frase explica por que seria impossível, neste caso, adicionar a função como uma função membro. Uma friendfunção tem os mesmos direitos que uma função membro ( é isso que friendsignifica); portanto, como usuário da classe, eu teria que me perguntar por que precisaria disso. Esta é a distinção que estou tentando fazer com a expressão "funcionalidade principal".
Magnus Hoff
32

Se possível, como funções de não membro e não amigo.

Conforme descrito por Herb Sutter e Scott Meyers, preferem funções não pertencentes a não amigas a funções membro, para ajudar a aumentar o encapsulamento.

Em alguns casos, como fluxos C ++, você não terá a opção e deve usar funções que não são membros.

Mas, ainda assim, isso não significa que você precise tornar essas funções amigas de suas classes: Essas funções ainda podem acessar sua classe por meio de seus acessadores de classe. Se você conseguir escrever essas funções dessa maneira, você venceu.

Sobre os protótipos << e >> do operador

Eu acredito que os exemplos que você deu na sua pergunta estão errados. Por exemplo;

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

Não consigo nem pensar em como esse método poderia funcionar em um fluxo.

Aqui estão as duas maneiras de implementar os operadores << e >>.

Digamos que você queira usar um objeto semelhante a um fluxo do tipo T.

E que você deseja extrair / inserir de / em T os dados relevantes do seu objeto do tipo Parágrafo.

Operador genérico << e >> protótipos de função

O primeiro sendo como funções:

// 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 << e >> protótipos de método

O segundo sendo 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 ;
}

Observe que, para usar essa notação, você deve estender a declaração de classe de T. Para objetos STL, isso não é possível (você não deve modificá-los ...).

E se T for um fluxo C ++?

Aqui estão os protótipos dos mesmos operadores << e >> para fluxos C ++.

Para basic_istream e basic_ostream genéricos

Observe que é o caso de fluxos, como você não pode modificar o fluxo C ++, você deve implementar as funções. O 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 e ostream

O código a seguir funcionará apenas para fluxos baseados em char.

// 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 comentou sobre o fato de o código baseado em char ser apenas uma "especialização" do código genérico acima dele. Obviamente, Rhys está certo: eu não recomendo o uso do exemplo baseado em char. É fornecido apenas aqui porque é mais simples de ler. Como só é viável se você trabalhar apenas com fluxos baseados em char, evite-o em plataformas onde o código wchar_t é comum (por exemplo, no Windows).

Espero que isso ajude.

paercebal
fonte
Seu código genérico de modelo basic_istream e basic_ostream já cobre as versões específicas std :: ostream- e std :: istream, já que as duas últimas são apenas instanciações do primeiro usando chars?
Rhys Ulerich
@Rhys Ulerich: Claro. Eu uso apenas a versão genérica e com modelo, mesmo que apenas no Windows, você precise lidar com os códigos char e wchar_t. O único mérito da segunda versão é parecer mais simples que o primeiro. Vou esclarecer meu post sobre isso.
22129 paercebal
10

Ele deve ser implementado como uma função gratuita e não amiga, especialmente se, como a maioria das coisas hoje em dia, a saída for usada principalmente para diagnóstico e registro. Adicione acessadores const para todas as coisas que precisam ser inseridas na saída e faça com que o emissor chame essas informações e faça a formatação.

Eu realmente comecei a coletar todas essas funções livres de saída do ostream em um cabeçalho "ostreamhelpers" e arquivo de implementação, mantém essa funcionalidade secundária longe do objetivo real das classes.

XPav
fonte
7

A assinatura:

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

Parece bastante suspeito, isso não se encaixa na streamconvenção nem na convenção bit a bit; portanto, parece um caso de abuso de sobrecarga do operador, operator <deve retornar, boolmas operator <<provavelmente deve retornar outra coisa.

Se você quis dizer, diga:

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

Então, como você não pode adicionar funções, ostreampor necessidade, a função deve ser uma função livre, frienddependendo se depende ou não do que deve ser acessado (se não for necessário acessar membros privados ou protegidos, não há necessidade de fazê-lo). amigo).

Motti
fonte
Vale ressaltar que o acesso à modificação ostreamseria necessário ao usar o ostream.operator<<(obj&)pedido; daí a função livre. Caso contrário, o tipo de usuário precisará ser do tipo vapor para acomodar o acesso.
Wulfgarpro 01/10/19
2

Apenas para concluir, gostaria de acrescentar que você realmente pode criar um operador ostream& operator << (ostream& os)dentro de uma classe e ele pode funcionar. Pelo que sei, não é uma boa ideia usá-lo, porque é muito complicado e pouco intuitivo.

Vamos supor que temos 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;
}

Então, para resumir - você pode fazê-lo, mas provavelmente não deveria :)

ashrasmun
fonte
0

operador amigo = direitos iguais à classe

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

operator<< implementado como uma função de amigo:

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

} 

SAÍDA:
100 Olá
100 Olá

Isso pode ser uma função de amigo apenas porque o objeto está no lado direito operator<<e o argumento coutestá no lado esquerdo. Portanto, essa não pode ser uma função de membro da classe, apenas uma função de amigo.

Rohit Vipin Mathews
fonte
Eu não acho que existe uma maneira de escrever isso como uma função de membro !!
Rohit Vipin Mathews
Por que tudo é ousado. Deixe-me remover isso.
Sebastian Mach