Czy operator << powinien być zaimplementowany jako przyjaciel czy jako funkcja członkowska?

129

To jest w zasadzie pytanie, czy istnieje „właściwy” sposób wdrożenia operator<<? Czytając to widzę, że coś takiego:

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

jest lepszy od czegoś takiego jak

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

Ale nie do końca rozumiem, dlaczego powinienem używać jednego lub drugiego.

Mój osobisty przypadek to:

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

Ale prawdopodobnie mógłbym zrobić:

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

Na jakiej podstawie powinienem oprzeć tę decyzję?

Uwaga :

 Paragraph::to_str = (return paragraph) 

gdzie akapit to ciąg.

Federico Builes
źródło
4
Swoją drogą, prawdopodobnie powinieneś dodać const do sygnatur funkcji
składowych
4
Po co zwracać wartość bool z operatora <<? Czy używasz go jako operatora strumienia, czy jako przeciążenia przesunięcia bitowego?
Martin York,

Odpowiedzi:

120

Problem polega na interpretacji linku do artykułu .

Równość

Ten artykuł dotyczy kogoś, kto ma problemy z poprawnym zdefiniowaniem operatorów relacji typu bool.

Operator:

  • Równość == i! =
  • Relacja <> <=> =

Te operatory powinny zwracać wartość bool, ponieważ porównują dwa obiekty tego samego typu. Zwykle najłatwiej jest zdefiniować te operatory jako część klasy. Dzieje się tak, ponieważ klasa jest automatycznie zaprzyjaźniona z samą sobą, więc obiekty typu Paragraf mogą sprawdzać się nawzajem (nawet prywatni członkowie).

Istnieje argument przemawiający za tworzeniem tych wolnostojących funkcji, ponieważ umożliwia to automatyczną konwersję obu stron, jeśli nie są one tego samego typu, podczas gdy funkcje składowe pozwalają tylko na automatyczną konwersję prawej strony. Uważam to za argument papiernika, ponieważ tak naprawdę nie chcesz, aby automatyczna konwersja miała miejsce w pierwszej kolejności (zwykle). Ale jeśli jest to coś, czego chcesz (nie polecam tego), wtedy wolnostojące komparatory mogą być korzystne.

Streaming

Operatorzy strumieni:

  • operator << wyjście
  • operator >> wejście

Kiedy używasz ich jako operatorów strumienia (zamiast przesunięcia binarnego), pierwszym parametrem jest strumień. Ponieważ nie masz dostępu do obiektu strumienia (nie możesz go modyfikować), nie mogą być operatorami składowymi, muszą one być zewnętrzne w stosunku do klasy. W związku z tym muszą albo być przyjaciółmi klasy, albo mieć dostęp do publicznej metody, która przeprowadzi transmisję strumieniową za Ciebie.

Te obiekty również tradycyjnie zwracają odwołanie do obiektu strumienia, dzięki czemu można łączyć operacje strumieniowe.

#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
źródło
19
Dlaczego tak jest operator<< private:?
Matt Clarkson,
47
@MattClarkson: To nie jest. Dlatego deklaracja funkcji zaprzyjaźnionej nie jest częścią klasy, a zatem specyfikatory dostępu nie mają na nią wpływu. Generalnie umieszczam deklaracje funkcji znajomego obok danych, do których uzyskują dostęp.
Martin York,
12
Dlaczego musi to być funkcja przyjazna, jeśli korzystasz z funkcji publicznej, aby uzyskać dostęp do danych? Przepraszam, jeśli pytanie jest głupie.
Siemion Daniłow
4
@SemyonDanilov: Dlaczego miałbyś przerywać hermetyzację i dodawać metody pobierające? freiendjest sposobem na rozszerzenie publicznego interfejsu bez przerywania enkapsulacji. Przeczytaj programmers.stackexchange.com/a/99595/12917
Martin York
3
@LokiAstari Ale z pewnością jest to argument za usunięciem to_str lub uczynieniem go prywatnym. W obecnym stanie operator streamingu nie musi być przyjacielem, gdyż korzysta tylko z funkcji publicznych.
deworde
53

Nie możesz tego zrobić jako funkcji składowej, ponieważ niejawny thisparametr znajduje się po lewej stronie <<-operator. (W związku z tym należałoby dodać go jako funkcję ostreamskładową do -klasy. Niedobrze :)

Czy mógłbyś to zrobić jako darmową funkcję bez friendtego? Wolę to, ponieważ jest jasne, że jest to integracja z ostreampodstawową funkcjonalnością Twojej klasy, a nie podstawową funkcjonalność.

Magnus Hoff
źródło
1
„nie jest podstawową funkcjonalnością Twojej klasy”. To właśnie oznacza „przyjaciel”. Gdyby to była podstawowa funkcjonalność, byłaby w klasie, a nie przyjacielem.
xaxxon
1
@xaxxon Myślę, że moje pierwsze zdanie wyjaśnia, dlaczego w tym przypadku nie byłoby możliwe dodanie funkcji jako funkcji składowej. friendFunkcja ma takie same prawa jak funkcję elementu ( to jest to, co friendznaczy), więc jako użytkownik klasy, musiałbym się zastanawiać, dlaczego to potrzebujemy. To jest różnica, którą próbuję wprowadzić, używając sformułowania „podstawowa funkcjonalność”.
Magnus Hoff,
32

Jeśli to możliwe, jako osoby niebędące członkami i nieprzyjazne.

Jak opisali Herb Sutter i Scott Meyers, preferuj nieprzyjazne funkcje niebędące członkami od funkcji składowych, aby pomóc zwiększyć hermetyzację.

W niektórych przypadkach, takich jak strumienie C ++, nie będziesz mieć wyboru i musisz używać funkcji niebędących składowymi.

Nie oznacza to jednak, że musisz uczynić te funkcje przyjaciółmi swoich klas: te funkcje mogą nadal uzyskiwać dostęp do Twojej klasy za pośrednictwem metod dostępu do Twojej klasy. Jeśli uda ci się napisać te funkcje w ten sposób, wygrałeś.

O prototypach operatorów << i >>

Uważam, że przykłady, które podałeś w swoim pytaniu, są błędne. Na przykład;

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

Nie mogę nawet zacząć myśleć, jak ta metoda mogłaby działać w strumieniu.

Oto dwa sposoby implementacji operatorów << i >>.

Załóżmy, że chcesz użyć obiektu podobnego do strumienia typu T.

I chcesz wyodrębnić / wstawić z / do T odpowiednie dane swojego obiektu typu Paragraf.

Prototypy funkcji operatora ogólnego << i >>

Pierwsza istota jako funkcje:

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

Operator ogólny << i >> prototypy metod

Druga istota jako metody:

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

Zauważ, że aby użyć tej notacji, musisz rozszerzyć deklarację klasy T. W przypadku obiektów STL nie jest to możliwe (nie należy ich modyfikować ...).

A co, jeśli T jest strumieniem w C ++?

Oto prototypy tych samych operatorów << i >> dla strumieni C ++.

Dla ogólnych basic_istream i basic_ostream

Zauważ, że jest to przypadek strumieni, ponieważ nie możesz modyfikować strumienia C ++, musisz zaimplementować funkcje. Co oznacza coś takiego:

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

Dla węgli i ostremów

Poniższy kod będzie działał tylko w przypadku strumieni opartych na znakach.

// 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 skomentował fakt, że kod oparty na znakach jest tylko „specjalizacją” kodu ogólnego nad nim. Oczywiście Rhys ma rację: nie polecam używania przykładu opartego na znakach. Jest tu podane tylko dlatego, że jest łatwiejsze do odczytania. Ponieważ jest to możliwe tylko wtedy, gdy pracujesz tylko ze strumieniami opartymi na znakach, powinieneś unikać go na platformach, na których kod wchar_t jest powszechny (np. W systemie Windows).

Mam nadzieję, że to pomoże.

paercebal
źródło
Czy twój ogólny kod bazujący na szablonach basic_istream i basic_ostream nie obejmuje już wersji specyficznych dla std :: ostream- i std :: istream, ponieważ dwie ostatnie są po prostu instancjami pierwszego z użyciem znaków?
Rhys Ulerich
@Rhys Ulerich: Oczywiście. Używam tylko generycznej wersji szablonu, choćby dlatego, że w systemie Windows musisz radzić sobie zarówno z kodem char, jak i wchar_t. Jedyną zaletą drugiej wersji jest wyglądanie na prostszą niż pierwsza. Wyjaśnię o tym mój post.
paercebal
10

Powinien zostać zaimplementowany jako bezpłatne, nieprzyjazne funkcje, zwłaszcza jeśli, jak większość rzeczy w dzisiejszych czasach, dane wyjściowe są głównie używane do diagnostyki i logowania. Dodaj metody dostępu const do wszystkich rzeczy, które mają trafić do wyniku, a następnie niech moduł wyjściowy po prostu je wywoła i sformatuje.

Właściwie zabrałem się do zebrania wszystkich tych wolnych funkcji wyjściowych ostream w nagłówku i pliku implementacji „ostreamhelpers”, dzięki czemu ta dodatkowa funkcjonalność jest daleko od rzeczywistego celu klas.

XPav
źródło
7

Podpis:

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

Wydaje się raczej podejrzane, to nie pasuje do streamkonwencji ani do konwencji bitowej, więc wygląda na to, że nadużycie operatora operator <powinno powrócić, boolale operator <<prawdopodobnie powinno zwrócić coś innego.

Jeśli tak chciałeś, powiedz:

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

Ponieważ nie możesz z ostreamkonieczności dodawać funkcji, funkcja musi być funkcją wolną, niezależnie od tego, czy jest friendona zależna od tego, do czego ma dostęp (jeśli nie ma dostępu do członków prywatnych lub chronionych, nie ma potrzeby przyjaciel).

Motti
źródło
Warto wspomnieć, ostreamże przy korzystaniu z ostream.operator<<(obj&)zamówienia wymagany byłby dostęp do modyfikacji ; stąd wolna funkcja. W przeciwnym razie użytkownik musi być typem pary, aby zapewnić dostęp.
wulfgarpro
2

Na koniec chciałbym dodać, że rzeczywiście możesz stworzyć operator ostream& operator << (ostream& os)wewnątrz klasy i to może działać. Z tego co wiem, używanie go nie jest dobrym pomysłem, ponieważ jest bardzo zawiłe i nieintuicyjne.

Załóżmy, że mamy ten kod:

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

Podsumowując - dasz radę, ale raczej nie powinieneś :)

ashrasmun
źródło
0

operator przyjaciel = równe prawa jak klasa

friend std::ostream& operator<<(std::ostream& os, const Object& object) {
    os << object._atribute1 << " " << object._atribute2 << " " << atribute._atribute3 << std::endl;
    return os;
}
Nehigienix
źródło
0

operator<< zaimplementowana jako funkcja znajomego:

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

} 

WYDAJNOŚĆ:
100 Hello
100 Hello

Może to być funkcja zaprzyjaźniona tylko dlatego, że obiekt znajduje się po prawej stronie, operator<<a argument coutpo lewej stronie. Więc nie może to być funkcja składowa klasy, może to być tylko funkcja zaprzyjaźniona.

Rohit Vipin Mathews
źródło
nie sądzę, aby można było napisać to jako funkcję członka!
Rohit Vipin Mathews
Dlaczego wszystko jest odważne. Pozwól mi to usunąć.
Sebastian Mach