Должен ли оператор << быть реализован как друг или как функция-член?

129

Это в основном вопрос, есть ли «правильный» способ реализации operator<<? Читая это, я вижу что-то вроде:

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

предпочтительнее чем-то вроде

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

Но я не совсем понимаю, почему я должен использовать тот или другой.

Мой личный случай:

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

Но я, наверное, мог бы:

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

На каком основании я должен принять это решение?

Примечание :

 Paragraph::to_str = (return paragraph) 

где абзац - это строка.

Федерико Булес
источник
4
Кстати, вам, вероятно, следует добавить const в сигнатуры функций-членов
Мотти,
4
Зачем возвращать bool из оператора <<? Вы используете его как оператор потока или как перегрузку побитового сдвига?
Мартин Йорк

Ответы:

120

Проблема здесь в вашей интерпретации статьи, на которую вы ссылаетесь. .

равенство

Эта статья о человеке, у которого возникли проблемы с правильным определением операторов отношений типа bool.

Оператор:

  • Равенство == и! =
  • Отношения <> <=> =

Эти операторы должны возвращать логическое значение, поскольку они сравнивают два объекта одного типа. Обычно эти операторы проще всего определить как часть класса. Это связано с тем, что класс автоматически является другом самого себя, поэтому объекты типа Paragraph могут проверять друг друга (даже частные члены друг друга).

Существует аргумент в пользу создания этих автономных функций, поскольку это позволяет автоматическому преобразованию преобразовывать обе стороны, если они не одного типа, в то время как функции-члены позволяют автоматически преобразовывать только правая часть. Я считаю, что это аргумент бумажного человека, поскольку вы действительно не хотите, чтобы автоматическое преобразование происходило в первую очередь (обычно). Но если это то, что вам нужно (я не рекомендую), то размещение компараторов отдельно может быть выгодным.

Потоковый

Операторы потока:

  • оператор << вывод
  • оператор >> ввод

Когда вы используете их как операторы потока (а не двоичный сдвиг), первым параметром является поток. Поскольку у вас нет доступа к объекту потока (его нельзя изменять), они не могут быть операторами-членами, они должны быть внешними по отношению к классу. Таким образом, они должны либо быть друзьями класса, либо иметь доступ к общедоступному методу, который будет выполнять потоковую передачу за вас.

Эти объекты также традиционно возвращают ссылку на объект потока, чтобы вы могли связать операции потока вместе.

#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
Почему operator<< private:?
Мэтт Кларксон
47
@MattClarkson: Это не так. Его объявление функции друга, таким образом, не является частью класса и, следовательно, не зависит от спецификаторов доступа. Обычно я помещаю объявления функций друзей рядом с данными, к которым они обращаются.
Мартин Йорк
12
Почему это должна быть удобная функция, если вы используете публичную функцию для доступа к данным? Простите, если вопрос глупый.
Семен Данилов
4
@SemyonDanilov: Зачем вам нарушать инкапсуляцию и добавлять геттеры! freiendэто способ расширить общедоступный интерфейс без нарушения инкапсуляции. Прочтите programmers.stackexchange.com/a/99595/12917
Мартин Йорк,
3
@LokiAstari Но, конечно же, это аргумент в пользу удаления to_str или его приватности. В его нынешнем виде оператор потоковой передачи не обязательно должен быть другом, поскольку он использует только общедоступные функции.
deworde
53

Вы не можете сделать это как функцию-член, потому что неявный thisпараметр - это левая часть <<оператора -оператора. (Следовательно, вам нужно будет добавить его как функцию-член к ostreamклассу. Не очень хорошо :)

Могли бы вы сделать это как бесплатную функцию без friendэтого? Это то, что я предпочитаю, потому что это дает понять, что это интеграция ostream, а не основная функциональность вашего класса.

Магнус Хофф
источник
1
«не основная функциональность вашего класса». Вот что значит «друг». Если бы это была основная функциональность, она была бы в классе, а не в друге.
xaxxon
1
@xaxxon Думаю, мое первое предложение объясняет, почему в этом случае невозможно добавить функцию как функцию-член. friendФункция имеет те же права, что и функции члена ( это то , что friendозначает), так как пользователь класса, я бы задаться вопросом , почему это нужно было бы что. Это различие, которое я пытаюсь провести с помощью формулировки «основные функции».
Магнус Хофф,
32

Если возможно, как функции, не являющиеся членами и не являющиеся друзьями.

Как описано Хербом Саттером и Скоттом Мейерсом, предпочитайте функции-члены, не являющиеся друзьями, функциям-членам, чтобы улучшить инкапсуляцию.

В некоторых случаях, например с потоками C ++, у вас не будет выбора, и вам придется использовать функции, не являющиеся членами.

Но все же это не означает, что вы должны сделать эти функции друзьями ваших классов: эти функции могут по-прежнему получать доступ к вашему классу через ваши методы доступа к классу. Если вам удастся написать эти функции таким образом, вы выиграете.

О прототипах оператора << и >>

Я считаю, что примеры, которые вы привели в своем вопросе, неверны. Например;

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

Я даже не могу представить, как этот метод может работать в потоке.

Вот два способа реализации операторов << и >>.

Допустим, вы хотите использовать объект типа T, похожий на поток.

И что вы хотите извлечь / вставить из / в T соответствующие данные вашего объекта типа Paragraph.

Универсальные прототипы функций оператора << и >>

Первый как функции:

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

Универсальные прототипы методов оператора << и >>

Второй - как методы:

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

Обратите внимание, что для использования этой записи вы должны расширить объявление класса T. Для объектов STL это невозможно (вы не должны изменять их ...).

А что, если T - это поток C ++?

Вот прототипы тех же операторов << и >> для потоков C ++.

Для общих basic_istream и basic_ostream

Обратите внимание, что это случай с потоками, поскольку вы не можете изменить поток C ++, вы должны реализовать функции. Что означает что-то вроде:

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

Для char istream и ostream

Следующий код будет работать только для потоков на основе символов.

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

Рис Улерих прокомментировал тот факт, что код на основе символов является всего лишь «специализацией» общего кода над ним. Конечно, Рис прав: я не рекомендую использовать пример на основе символов. Здесь он приводится только потому, что его проще читать. Поскольку это возможно только в том случае, если вы работаете только с потоками на основе символов, вам следует избегать его на платформах, где используется код wchar_t (например, в Windows).

Надеюсь, это поможет.

paercebal
источник
Разве ваш общий шаблонный код basic_istream и basic_ostream уже не охватывает версии, специфичные для std :: ostream- и std :: istream, поскольку последние два являются просто экземплярами первого с использованием символов?
Рис Улерих
@ Рис Улерих: Конечно. Я использую только общую, шаблонную версию, хотя бы потому, что в Windows вам приходится иметь дело как с кодом char, так и с кодом wchar_t. Единственное достоинство второй версии состоит в том, что она кажется более простой, чем первая. Я проясню свой пост по этому поводу.
paercebal
10

Он должен быть реализован как бесплатная, не дружественная функция, особенно если, как и большинство вещей в наши дни, вывод в основном используется для диагностики и ведения журнала. Добавьте константные аксессоры для всех вещей, которые должны поступать в вывод, а затем пусть средство вывода просто вызовет их и выполнит форматирование.

На самом деле я собрал все эти бесплатные функции вывода ostream в заголовке «ostreamhelpers» и файле реализации, это держит эту вторичную функциональность далеко от реальной цели классов.

XPav
источник
7

Подпись:

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

Кажется довольно подозрительным, это не соответствует streamни соглашению, ни побитовому соглашению, поэтому похоже на случай злоупотребления перегрузкой оператора, operator <должен вернуться, boolноoperator << вероятно, должен вернуть что-то еще.

Если вы имели в виду, скажите:

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

Затем, поскольку вы не можете добавлять функции ostreamпо необходимости, функция должна быть бесплатной функцией, независимо от того, friendзависит ли она от того, к чему она имеет доступ (если ей не нужен доступ к частным или защищенным членам, нет необходимости делать это друг).

Моти
источник
Стоит отметить, ostreamчто при использовании ostream.operator<<(obj&)упорядочивания потребуется доступ для изменения ; следовательно, свободная функция. В противном случае тип пользователя должен быть паровым для обеспечения доступа.
wulfgarpro
2

В заключение хочу добавить, что вы действительно можете создать операторostream& operator << (ostream& os) внутри класса, и он может работать. Насколько я знаю, использовать его - не лучшая идея, потому что он очень запутан и не интуитивен.

Предположим, у нас есть такой код:

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

Подводя итог - вы можете это сделать, но, скорее всего, не должны :)

ashrasmun
источник
0

друг оператор = равные права как класс

friend std::ostream& operator<<(std::ostream& os, const Object& object) {
    os << object._atribute1 << " " << object._atribute2 << " " << atribute._atribute3 << std::endl;
    return os;
}
Nehigienix
источник
0

operator<< реализована как функция друга:

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

} 

ВЫХОД:
100 Привет
100 Привет

Это может быть дружественная функция только потому, что объект находится справа, operator<<а аргумент cout- слева. Таким образом, это не может быть функцией-членом класса, это может быть только функция друга.

Рохит Випин Мэтьюз
источник
я не думаю, что есть способ написать это от имени участника !!
Рохит Випин Мэтьюз,
Почему все жирно. Позвольте мне это удалить.
Себастьян Мах