L'opérateur << doit-il être implémenté en tant qu'ami ou en tant que fonction membre?

129

C'est essentiellement la question, y a-t-il une «bonne» façon de mettre en œuvre operator<<? En lisant ceci, je peux voir que quelque chose comme:

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

est préféré à quelque chose comme

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

Mais je ne vois pas vraiment pourquoi devrais-je utiliser l'un ou l'autre.

Mon cas personnel est:

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

Mais je pourrais probablement faire:

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

Sur quelle justification dois-je fonder cette décision?

Remarque :

 Paragraph::to_str = (return paragraph) 

où paragraphe est une chaîne.

Federico Builes
la source
4
BTW, vous devriez probablement ajouter const aux signatures des fonctions membres
Motti
4
Pourquoi retourner booléen de l'opérateur <<? L'utilisez-vous comme opérateur de flux ou comme surcharge du décalage de bit?
Martin York

Réponses:

120

Le problème ici est dans votre interprétation de l'article que vous liez .

Égalité

Cet article concerne une personne qui rencontre des problèmes pour définir correctement les opérateurs de relation booléenne.

L'opérateur:

  • Égalité == et! =
  • Relation <> <=> =

Ces opérateurs doivent renvoyer un booléen car ils comparent deux objets du même type. Il est généralement plus simple de définir ces opérateurs dans le cadre de la classe. En effet, une classe est automatiquement un ami d'elle-même, de sorte que les objets de type Paragraph peuvent s'examiner les uns les autres (même les membres privés).

Il existe un argument en faveur de la création de ces fonctions autonomes car cela permet à la conversion automatique de convertir les deux côtés s'ils ne sont pas du même type, tandis que les fonctions membres autorisent uniquement la conversion automatique des rhs. Je trouve que c'est un argument d'homme de papier car vous ne voulez pas vraiment que la conversion automatique se produise en premier lieu (généralement). Mais si c'est quelque chose que vous voulez (je ne le recommande pas), rendre les comparateurs autonomes peut être avantageux.

Diffusion

Les opérateurs de flux:

  • opérateur << sortie
  • opérateur >> entrée

Lorsque vous les utilisez comme opérateurs de flux (plutôt que comme décalage binaire), le premier paramètre est un flux. Puisque vous n'avez pas accès à l'objet de flux (ce n'est pas à vous de le modifier), ceux-ci ne peuvent pas être des opérateurs membres, ils doivent être externes à la classe. Ainsi, ils doivent soit être des amis de la classe, soit avoir accès à une méthode publique qui fera le streaming pour vous.

Il est également traditionnel que ces objets renvoient une référence à un objet de flux afin que vous puissiez enchaîner les opérations de flux.

#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
la source
19
Pourquoi le operator<< private:?
Matt Clarkson
47
@MattClarkson: Ce n'est pas le cas. C'est une déclaration de fonction amie qui ne fait donc pas partie de la classe et n'est donc pas affectée par les spécificateurs d'accès. Je place généralement les déclarations de fonction d'ami à côté des données auxquelles elles accèdent.
Martin York
12
Pourquoi faut-il que ce soit une fonction conviviale, si vous utilisez la fonction publique pour accéder aux données? Désolé, si la question est stupide.
Semyon Danilov
4
@SemyonDanilov: Pourquoi interrompriez-vous l'encapsulation et ajouteriez-vous des getters! freiendest un moyen d'étendre l'interface publique sans interrompre l'encapsulation. Lire programmers.stackexchange.com/a/99595/12917
Martin York
3
@LokiAstari Mais c'est sûrement un argument pour supprimer to_str ou le rendre privé. Dans l'état actuel des choses, l'opérateur de streaming n'a pas besoin d'être un ami, car il n'utilise que des fonctions publiques.
deworde
53

Vous ne pouvez pas le faire en tant que fonction membre, car le thisparamètre implicite est le côté gauche de l' <<opérateur. (Par conséquent, vous devrez l'ajouter en tant que fonction membre à la ostreamclasse -class. Pas bon :)

Pourriez-vous le faire en tant que fonction gratuite sans friendcela? C'est ce que je préfère, car cela montre clairement qu'il s'agit d'une intégration avec ostream, et non d'une fonctionnalité de base de votre classe.

Magnus Hoff
la source
1
"pas une fonctionnalité de base de votre classe." C'est ce que signifie «ami». S'il s'agissait d'une fonctionnalité de base, ce serait dans la classe, pas un ami.
xaxxon
1
@xaxxon Je pense que ma première phrase explique pourquoi il serait impossible dans ce cas d'ajouter la fonction en tant que fonction membre. Une friendfonction a les mêmes droits en tant que fonction membre ( c'est ce que signifie), donc en tant qu'utilisateur de la classe, je me demande pourquoi il faudrait que. C'est la distinction que j'essaie de faire avec le libellé «fonctionnalité de base». friend
Magnus Hoff
32

Si possible, en tant que fonctions non-membres et non-amis.

Comme décrit par Herb Sutter et Scott Meyers, préférez les fonctions non-membres non-amis aux fonctions membres, pour aider à augmenter l'encapsulation.

Dans certains cas, comme les flux C ++, vous n'aurez pas le choix et devez utiliser des fonctions non membres.

Mais encore, cela ne signifie pas que vous devez rendre ces fonctions amis de vos classes: ces fonctions peuvent toujours accéder à votre classe via vos accesseurs de classe. Si vous réussissez à écrire ces fonctions de cette façon, vous avez gagné.

À propos des prototypes d'opérateur << et >>

Je pense que les exemples que vous avez donnés dans votre question sont faux. Par exemple;

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

Je ne peux même pas commencer à penser comment cette méthode pourrait fonctionner dans un flux.

Voici les deux manières d'implémenter les opérateurs << et >>.

Supposons que vous souhaitiez utiliser un objet semblable à un flux de type T.

Et que vous souhaitez extraire / insérer de / dans T les données pertinentes de votre objet de type Paragraphe.

Prototypes de fonctions d'opérateurs génériques << et >>

Le premier étant en tant que fonctions:

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

Prototypes de méthodes d'opérateurs génériques << et >>

Le second étant comme méthodes:

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

Notez que pour utiliser cette notation, vous devez étendre la déclaration de classe de T. Pour les objets STL, ce n'est pas possible (vous n'êtes pas censé les modifier ...).

Et si T est un flux C ++?

Voici les prototypes des mêmes opérateurs << et >> pour les flux C ++.

Pour basic_istream et basic_ostream génériques

Notez que c'est le cas des flux, comme vous ne pouvez pas modifier le flux C ++, vous devez implémenter les fonctions. Ce qui veut dire quelque chose comme:

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

Pour char istream et ostream

Le code suivant fonctionnera uniquement pour les flux basés sur des caractères.

// 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 a commenté le fait que le code à base de caractères n'est qu'une "spécialisation" du code générique au-dessus. Bien sûr, Rhys a raison: je ne recommande pas l'utilisation de l'exemple basé sur char. Il n'est donné ici que parce qu'il est plus simple à lire. Comme il n'est viable que si vous travaillez uniquement avec des flux basés sur char, vous devez l'éviter sur les plates-formes où le code wchar_t est courant (c'est-à-dire sous Windows).

J'espère que cela aidera.

Paercebal
la source
Votre code générique basic_istream et basic_ostream ne couvre-t-il pas déjà les versions spécifiques à std :: ostream- et std :: istream puisque les deux derniers ne sont que des instanciations du premier utilisant des chars?
Rhys Ulerich
@Rhys Ulerich: Bien sûr. Je n'utilise que la version générique et basée sur un modèle, ne serait-ce que parce que sous Windows, vous devez gérer à la fois le code char et wchar_t. Le seul mérite de la deuxième version est de paraître plus simple que la première. Je vais clarifier mon message à ce sujet.
paercebal
10

Il devrait être implémenté en tant que fonctions gratuites et non-amis, surtout si, comme la plupart des choses de nos jours, la sortie est principalement utilisée pour les diagnostics et la journalisation. Ajoutez des accesseurs const pour toutes les choses qui doivent entrer dans la sortie, puis demandez à l'émetteur de les appeler et de faire le formatage.

En fait, j'ai commencé à collecter toutes ces fonctions libres de sortie ostream dans un en-tête et un fichier d'implémentation "ostreamhelpers", cela éloigne cette fonctionnalité secondaire de l'objectif réel des classes.

XPav
la source
7

La signature:

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

Cela semble plutôt suspect, cela ne correspond pas à la streamconvention ni à la convention au niveau du bit, donc cela ressemble à un cas d'abus de surcharge d'opérateur, operator <devrait revenir boolmais operator <<devrait probablement renvoyer autre chose.

Si vous vouliez dire ainsi, dites:

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

Ensuite, comme vous ne pouvez pas ajouter de fonctions à ostreampar nécessité, la fonction doit être une fonction libre, que cela frienddépende ou non de ce à quoi elle doit accéder (si elle n'a pas besoin d'accéder à des membres privés ou protégés, il n'est pas nécessaire de la créer ami).

Motti
la source
Il convient de mentionner que l'accès à modifier ostreamserait requis lors de l'utilisation de la ostream.operator<<(obj&)commande; d'où la fonction libre. Sinon, le type d'utilisateur doit être un type à vapeur pour permettre l'accès.
wulfgarpro
2

Juste pour compléter, j'aimerais ajouter que vous pouvez en effet créer un opérateur à l' ostream& operator << (ostream& os)intérieur d'une classe et que cela peut fonctionner. D'après ce que je sais, ce n'est pas une bonne idée de l'utiliser, car il est très compliqué et peu intuitif.

Supposons que nous ayons ce code:

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

Donc, pour résumer, vous pouvez le faire, mais vous ne devriez probablement pas :)

Ashrasmun
la source
0

opérateur ami = droits égaux en tant que classe

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

operator<< implémenté comme une fonction ami:

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

} 

SORTIE:
100 Bonjour
100 Bonjour

Cela peut être une fonction d'ami uniquement parce que l'objet est sur le côté droit de operator<<et l'argument coutest sur le côté gauche. Cela ne peut donc pas être une fonction membre de la classe, cela ne peut être qu'une fonction amie.

Rohit Vipin Mathews
la source
Je ne pense pas qu'il y ait un moyen d'écrire ceci en tant que fonction de membre !!
Rohit Vipin Mathews
Pourquoi tout est audacieux. Laissez-moi supprimer ceci.
Sebastian Mach